Преглед на файлове

Merge branch 'cero-dev' into patch-1

causefx преди 9 години
родител
ревизия
a847245746
променени са 95 файла, в които са добавени 15466 реда и са изтрити 749 реда
  1. 82 54
      ajax.php
  2. 33 50
      auth.php
  3. 4 2
      bower_components/fullcalendar/dist/fullcalendar.js
  4. BIN
      bower_components/slick/ajax-loader.gif
  5. 10 0
      bower_components/slick/config.rb
  6. BIN
      bower_components/slick/fonts/slick.eot
  7. 14 0
      bower_components/slick/fonts/slick.svg
  8. BIN
      bower_components/slick/fonts/slick.ttf
  9. BIN
      bower_components/slick/fonts/slick.woff
  10. 204 0
      bower_components/slick/slick-theme.css
  11. 168 0
      bower_components/slick/slick-theme.less
  12. 194 0
      bower_components/slick/slick-theme.scss
  13. 121 0
      bower_components/slick/slick.css
  14. 2892 0
      bower_components/slick/slick.js
  15. 98 0
      bower_components/slick/slick.less
  16. 16 0
      bower_components/slick/slick.min.js
  17. 98 0
      bower_components/slick/slick.scss
  18. 0 0
      bower_components/summernote/dist/summernote.css
  19. 7300 0
      bower_components/summernote/dist/summernote.js
  20. 1 0
      bower_components/summernote/dist/summernote.min.js
  21. 144 26
      chat.php
  22. BIN
      chat/audio/newmessage.mp3
  23. 66 0
      chat/checkuser.php
  24. 7 0
      chat/connect.php
  25. 36 0
      chat/getlikes.php
  26. 26 0
      chat/getonline.php
  27. 23 0
      chat/gettyping.php
  28. BIN
      chat/img/favicon.ico
  29. BIN
      chat/img/like.png
  30. 81 0
      chat/logmessage.php
  31. 2 0
      chat/php.ini
  32. 154 0
      chat/refreshmessages.php
  33. 41 0
      chat/setlike.php
  34. 58 0
      chat/settyping.php
  35. 177 0
      chat/uploadimage.php
  36. 510 89
      chatjs.php
  37. 1 0
      check.php
  38. 15 1
      config/configDefaults.php
  39. 36 7
      css/style.css
  40. 2 1
      error.php
  41. 594 218
      functions.php
  42. 325 139
      homepage.php
  43. BIN
      images/clipboard.png
  44. BIN
      images/css.png
  45. BIN
      images/html.png
  46. BIN
      images/livetv.png
  47. BIN
      images/no-list.png
  48. BIN
      images/no-np.png
  49. BIN
      images/no-search.png
  50. BIN
      images/pf-blue.png
  51. BIN
      images/pin.png
  52. BIN
      images/platforms/android.png
  53. BIN
      images/platforms/atv.png
  54. BIN
      images/platforms/chrome.png
  55. BIN
      images/platforms/chromecast.png
  56. BIN
      images/platforms/cloudsync.png
  57. BIN
      images/platforms/default.png
  58. BIN
      images/platforms/dlna.png
  59. BIN
      images/platforms/emby.png
  60. BIN
      images/platforms/firefox.png
  61. BIN
      images/platforms/gtv.png
  62. BIN
      images/platforms/ie.png
  63. BIN
      images/platforms/ios.png
  64. BIN
      images/platforms/kodi.png
  65. BIN
      images/platforms/linux.png
  66. BIN
      images/platforms/msedge.png
  67. BIN
      images/platforms/opera.png
  68. BIN
      images/platforms/osx.png
  69. BIN
      images/platforms/pht.png
  70. BIN
      images/platforms/playstation.png
  71. BIN
      images/platforms/pmh.png
  72. BIN
      images/platforms/pmp.png
  73. BIN
      images/platforms/pms.png
  74. BIN
      images/platforms/roku.png
  75. BIN
      images/platforms/safari.png
  76. BIN
      images/platforms/samsung.png
  77. BIN
      images/platforms/wiiu.png
  78. BIN
      images/platforms/win8.png
  79. BIN
      images/platforms/wp.png
  80. BIN
      images/platforms/xbmc.png
  81. BIN
      images/platforms/xbox.png
  82. BIN
      images/security.png
  83. 201 34
      index.php
  84. 2 2
      js/notifications/notificationFx.js
  85. 581 0
      js/push.js
  86. 55 2
      lang/de.ini
  87. 57 4
      lang/en.ini
  88. 57 4
      lang/es.ini
  89. 57 4
      lang/fr.ini
  90. 57 4
      lang/it.ini
  91. 57 4
      lang/nl.ini
  92. 57 4
      lang/pl.ini
  93. 624 56
      settings.php
  94. 127 43
      user.php
  95. 1 1
      vendor/phpmailer/phpmailer/class.phpmailer.php

+ 82 - 54
ajax.php

@@ -39,12 +39,12 @@ switch ($_SERVER['REQUEST_METHOD']) {
 				break;
 			case 'emby-streams':
 				qualifyUser(EMBYHOMEAUTH, true);
-				echo getEmbyStreams(12);
+				echo getEmbyStreams(12, EMBYSHOWNAMES, $GLOBALS['USER']->role);
 				die();
 				break;
 			case 'plex-streams':
 				qualifyUser(PLEXHOMEAUTH, true);
-				echo getPlexStreams(12);
+				echo getPlexStreams(12, PLEXSHOWNAMES, $GLOBALS['USER']->role);
 				die();
 				break;
 			case 'emby-recent':
@@ -54,7 +54,7 @@ switch ($_SERVER['REQUEST_METHOD']) {
 				break;
 			case 'plex-recent':
 				qualifyUser(PLEXHOMEAUTH, true);
-				echo getPlexRecent($_GET['type'], 12);
+				echo getPlexRecent(array("movie" => PLEXRECENTMOVIE, "season" => PLEXRECENTTV, "album" => PLEXRECENTMUSIC));
 				die();
 				break;
 			case 'sabnzbd-update':
@@ -72,59 +72,87 @@ switch ($_SERVER['REQUEST_METHOD']) {
 		}
 		break;
 	case 'POST':
-		// Check if the user is an admin and is allowed to commit values
-		qualifyUser('admin', true);
-		switch ($action) {
-			case 'upload-images':
-				uploadFiles('images/', array('jpg', 'png', 'svg', 'jpeg', 'bmp'));
-				sendNotification(true);
-				break;
-			case 'remove-images':
-				removeFiles('images/'.(isset($_POST['file'])?$_POST['file']:''));
-				sendNotification(true);
-				break;
-			case 'update-config':
-				sendNotification(updateConfig($_POST));
-				break;
-			case 'update-appearance':
-				// Custom CSS Special Case START
-				if (isset($_POST['customCSS'])) {
-					if ($_POST['customCSS']) {
-						write_ini_file($_POST['customCSS'], 'custom.css');
-					} else {
-						unlink('custom.css');
-					}
-					$response['parent']['reload'] = true;
+        // Check if the user is an admin and is allowed to commit values
+        switch ($action) {
+            case 'search-plex':
+			 	$response = searchPlex($_POST['searchtitle']);
+			 	break;
+			case 'validate-invite':
+				$response = inviteCodes("check", $_POST['invitecode']);
+				$response['notify'] = sendResult($response, "check", $_POST['checkurl'], "CODE_SUCCESS", "CODE_ERROR");
+				break;
+			case 'use-invite':
+				//$response = inviteCodes("check", $_POST['invitecode']);
+				//$response = inviteCodes("use", $_POST['invitecode']);
+				if(inviteCodes("check", $_POST['invitecode'])){
+					$response = inviteCodes("use", $_POST['invitecode'], $_POST['inviteuser']);
+					$response['notify'] = sendResult(plexUserShare($_POST['inviteuser']), "check", $_POST['checkurl'], "INVITE_SUCCESS", "INVITE_ERROR");
 				}
-				unset($_POST['customCSS']);
-				// Custom CSS Special Case END
-				$response['notify'] = sendNotification(updateDBOptions($_POST),false,false);
-				break;
-			case 'deleteDB':
-				deleteDatabase();
-				sendNotification(true, 'Database Deleted!');
-				break;
-			case 'upgradeInstall':
-				upgradeInstall();
-				$response['notify'] = sendNotification(true, 'Performing Checks', false);
-				$response['tab']['goto'] = 'updatedb.php';
-				break;
-			case 'forceBranchInstall':
-				upgradeInstall(GIT_BRANCH);
-				$response['notify'] = sendNotification(true, 'Performing Checks', false);
-				$response['tab']['goto'] = 'updatedb.php';
 				break;
-			case 'deleteLog':
-				sendNotification(unlink(FAIL_LOG));
-				break;
-			case 'submit-tabs':
-				$response['notify'] = sendNotification(updateTabs($_POST) , false, false);
-				$response['show_apply'] = true;
-				break;
-			default:
-				sendNotification(false, 'Unsupported Action!');
-		}
-		break;
+			case 'join-plex':
+				$response = plexJoin($_POST['joinuser'], $_POST['joinemail'], $_POST['joinpassword']);
+				$response['notify'] = sendResult($response, "check", $_POST['checkurl'], "JOIN_SUCCESS", "JOIN_ERROR");
+				break;
+            default: // Stuff that you need admin for
+                qualifyUser('admin', true);
+                switch ($action) {
+                    case 'check-url':
+                        sendResult(frameTest($_POST['checkurl']), "flask", $_POST['checkurl'], "IFRAME_CAN_BE_FRAMED", "IFRAME_CANNOT_BE_FRAMED");
+                        break;
+                    case 'upload-images':
+                        uploadFiles('images/', array('jpg', 'png', 'svg', 'jpeg', 'bmp'));
+                        sendNotification(true);
+                        break;
+                    case 'remove-images':
+                        removeFiles('images/'.(isset($_POST['file'])?$_POST['file']:''));
+                        sendNotification(true);
+                        break;
+                    case 'update-config':
+                        sendNotification(updateConfig($_POST));
+                        break;
+                    case 'update-appearance':
+                        // Custom CSS Special Case START
+                        if (isset($_POST['customCSS'])) {
+                            if ($_POST['customCSS']) {
+                                write_ini_file($_POST['customCSS'], 'custom.css');
+                            } else {
+                                unlink('custom.css');
+                            }
+                            $response['parent']['reload'] = true;
+                        }
+                        unset($_POST['customCSS']);
+                        // Custom CSS Special Case END
+                        $response['notify'] = sendNotification(updateDBOptions($_POST),false,false);
+                        break;
+                    case 'deleteDB':
+                        deleteDatabase();
+                        sendNotification(true, 'Database Deleted!');
+                        break;
+                    case 'upgradeInstall':
+                        upgradeInstall();
+                        $response['notify'] = sendNotification(true, 'Performing Checks', false);
+                        $response['tab']['goto'] = 'updatedb.php';
+                        break;
+                    case 'forceBranchInstall':
+                        upgradeInstall(GIT_BRANCH);
+                        $response['notify'] = sendNotification(true, 'Performing Checks', false);
+                        $response['tab']['goto'] = 'updatedb.php';
+                        break;
+                    case 'deleteLog':
+                        sendNotification(unlink(FAIL_LOG));
+                        break;
+                    case 'deleteOrgLog':
+                        sendNotification(unlink("org.log"));
+                        break;
+                    case 'submit-tabs':
+                        $response['notify'] = sendNotification(updateTabs($_POST) , false, false);
+                        $response['show_apply'] = true;
+                        break;
+                    default:
+                        sendNotification(false, 'Unsupported Action!');
+                }
+        }
+        break;
 	case 'PUT':
 		sendNotification(false, 'Unsupported Action!');
 		break;

+ 33 - 50
auth.php

@@ -1,56 +1,39 @@
 <?php
-
-$data = false;
-
-function getBannedUsers($string){
-    
-    if (strpos($string, ',') !== false) {
-    
-        $banned = explode(",", $string);
-        
-    }elseif (strpos($string, ',') == false) {
-    
-        $banned = array($string);
-        
-    }
-    
-    return $banned;
-    
-}
-
-if (isset($_GET['ban'])) : $ban = strtoupper($_GET['ban']); else : $ban = ""; endif;
-
+$debug = false;
 require_once("user.php");
 $USER = new User("registration_callback");
+$ban = isset($_GET['ban']) ? strtoupper($_GET['ban']) : "";
+$whitelist = isset($_GET['whitelist']) ? $_GET['whitelist'] : false;
+$currentIP = get_client_ip();
+
+if ($whitelist) {
+	$skipped = false;
+    if(in_array($currentIP, getWhitelist($whitelist))) {
+       !$debug ? exit(http_response_code(200)) : die("$currentIP Whitelist Authorized");
+	}else{
+		$skipped = true;
+	}
+}
+if (isset($_GET['admin'])) {
+    if($USER->authenticated && $USER->role == "admin" && !in_array(strtoupper($USER->username), getBannedUsers($ban))) {
+        !$debug ? exit(http_response_code(200)) : die("$USER->username on $currentIP Authorized At Admin Level");
+	} else {
+        !$debug ? exit(http_response_code(401)) : die("$USER->username on $currentIP Not Authorized At Admin Level");
+    }
+}
+if (isset($_GET['user'])) {
+    if($USER->authenticated && !in_array(strtoupper($USER->username), getBannedUsers($ban))) {
+        !$debug ? exit(http_response_code(200)) : die("$USER->username on $currentIP Authorized At User Level");
+	} else {
+        !$debug ? exit(http_response_code(401)) : die("$USER->username on $currentIP Not Authorized At User Level");
+	}
+}
+if (!isset($_GET['user']) && !isset($_GET['admin']) && !isset($_GET['whitelist'])) {
+    !$debug ? exit(http_response_code(401)) : die("Not Authorized Due To No Parameters Set");
+}
 
-if (isset($_GET['admin'])) :
-
-    if($USER->authenticated && $USER->role == "admin" && !in_array(strtoupper($USER->username), getBannedUsers($ban))) :
-
-        exit(http_response_code(200));
-
-    else :
-
-        exit(http_response_code(401));
-
-    endif;
-
-elseif (isset($_GET['user'])) :
-
-    if($USER->authenticated && !in_array(strtoupper($USER->username), getBannedUsers($ban))) :
-
-        exit(http_response_code(200));
-
-    else :
-
-        exit(http_response_code(401));
-
-    endif;
-
-elseif (!isset($_GET['user'])  && !isset($_GET['admin'])) :
-
-    exit(http_response_code(401));
-
-endif;
+if ($skipped) {
+	!$debug ? exit(http_response_code(401)) : die("$USER->username on $currentIP Not Authorized Nor On Whitelist");
+}
 
 ?>

+ 4 - 2
bower_components/fullcalendar/dist/fullcalendar.js

@@ -6656,8 +6656,10 @@ DayGrid.mixin({
 		if (seg.isStart) {
 			timeText = this.getEventTimeText(event);
 			if (timeText) {
-				timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
-			}
+				timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span><br/>';
+			}else{
+       timeHtml = '<br/>';
+   }
 		}
 
 		titleHtml =

BIN
bower_components/slick/ajax-loader.gif


+ 10 - 0
bower_components/slick/config.rb

@@ -0,0 +1,10 @@
+css_dir = "."
+sass_dir = "."
+images_dir = "."
+fonts_dir = "fonts"
+relative_assets = true
+
+output_style = :compact
+line_comments = false
+
+preferred_syntax = :scss

BIN
bower_components/slick/fonts/slick.eot


+ 14 - 0
bower_components/slick/fonts/slick.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Generated by Fontastic.me</metadata>
+<defs>
+<font id="slick" horiz-adv-x="512">
+<font-face font-family="slick" units-per-em="512" ascent="480" descent="-32"/>
+<missing-glyph horiz-adv-x="512" />
+
+<glyph unicode="&#8594;" d="M241 113l130 130c4 4 6 8 6 13 0 5-2 9-6 13l-130 130c-3 3-7 5-12 5-5 0-10-2-13-5l-29-30c-4-3-6-7-6-12 0-5 2-10 6-13l87-88-87-88c-4-3-6-8-6-13 0-5 2-9 6-12l29-30c3-3 8-5 13-5 5 0 9 2 12 5z m234 143c0-40-9-77-29-110-20-34-46-60-80-80-33-20-70-29-110-29-40 0-77 9-110 29-34 20-60 46-80 80-20 33-29 70-29 110 0 40 9 77 29 110 20 34 46 60 80 80 33 20 70 29 110 29 40 0 77-9 110-29 34-20 60-46 80-80 20-33 29-70 29-110z"/>
+<glyph unicode="&#8592;" d="M296 113l29 30c4 3 6 7 6 12 0 5-2 10-6 13l-87 88 87 88c4 3 6 8 6 13 0 5-2 9-6 12l-29 30c-3 3-8 5-13 5-5 0-9-2-12-5l-130-130c-4-4-6-8-6-13 0-5 2-9 6-13l130-130c3-3 7-5 12-5 5 0 10 2 13 5z m179 143c0-40-9-77-29-110-20-34-46-60-80-80-33-20-70-29-110-29-40 0-77 9-110 29-34 20-60 46-80 80-20 33-29 70-29 110 0 40 9 77 29 110 20 34 46 60 80 80 33 20 70 29 110 29 40 0 77-9 110-29 34-20 60-46 80-80 20-33 29-70 29-110z"/>
+<glyph unicode="&#8226;" d="M475 256c0-40-9-77-29-110-20-34-46-60-80-80-33-20-70-29-110-29-40 0-77 9-110 29-34 20-60 46-80 80-20 33-29 70-29 110 0 40 9 77 29 110 20 34 46 60 80 80 33 20 70 29 110 29 40 0 77-9 110-29 34-20 60-46 80-80 20-33 29-70 29-110z"/>
+<glyph unicode="&#97;" d="M475 439l0-128c0-5-1-9-5-13-4-4-8-5-13-5l-128 0c-8 0-13 3-17 11-3 7-2 14 4 20l40 39c-28 26-62 39-100 39-20 0-39-4-57-11-18-8-33-18-46-32-14-13-24-28-32-46-7-18-11-37-11-57 0-20 4-39 11-57 8-18 18-33 32-46 13-14 28-24 46-32 18-7 37-11 57-11 23 0 44 5 64 15 20 9 38 23 51 42 2 1 4 3 7 3 3 0 5-1 7-3l39-39c2-2 3-3 3-6 0-2-1-4-2-6-21-25-46-45-76-59-29-14-60-20-93-20-30 0-58 5-85 17-27 12-51 27-70 47-20 19-35 43-47 70-12 27-17 55-17 85 0 30 5 58 17 85 12 27 27 51 47 70 19 20 43 35 70 47 27 12 55 17 85 17 28 0 55-5 81-15 26-11 50-26 70-45l37 37c6 6 12 7 20 4 8-4 11-9 11-17z"/>
+</font></defs></svg>

BIN
bower_components/slick/fonts/slick.ttf


BIN
bower_components/slick/fonts/slick.woff


+ 204 - 0
bower_components/slick/slick-theme.css

@@ -0,0 +1,204 @@
+@charset 'UTF-8';
+/* Slider */
+.slick-loading .slick-list
+{
+    background: #fff url('./ajax-loader.gif') center center no-repeat;
+}
+
+/* Icons */
+@font-face
+{
+    font-family: 'slick';
+    font-weight: normal;
+    font-style: normal;
+
+    src: url('./fonts/slick.eot');
+    src: url('./fonts/slick.eot?#iefix') format('embedded-opentype'), url('./fonts/slick.woff') format('woff'), url('./fonts/slick.ttf') format('truetype'), url('./fonts/slick.svg#slick') format('svg');
+}
+/* Arrows */
+.slick-prev,
+.slick-next
+{
+    font-size: 0;
+    line-height: 0;
+
+    position: absolute;
+    top: 50%;
+
+    display: block;
+
+    width: 20px;
+    height: 20px;
+    padding: 0;
+    -webkit-transform: translate(0, -50%);
+    -ms-transform: translate(0, -50%);
+    transform: translate(0, -50%);
+
+    cursor: pointer;
+
+    color: transparent;
+    border: none;
+    outline: none;
+    background: transparent;
+}
+.slick-prev:hover,
+.slick-prev:focus,
+.slick-next:hover,
+.slick-next:focus
+{
+    color: transparent;
+    outline: none;
+    background: transparent;
+}
+.slick-prev:hover:before,
+.slick-prev:focus:before,
+.slick-next:hover:before,
+.slick-next:focus:before
+{
+    opacity: 1;
+}
+.slick-prev.slick-disabled:before,
+.slick-next.slick-disabled:before
+{
+    opacity: .25;
+}
+
+.slick-prev:before,
+.slick-next:before
+{
+    font-family: 'slick';
+    font-size: 20px;
+    line-height: 1;
+
+    opacity: .75;
+    color: white;
+
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+
+.slick-prev
+{
+    left: -25px;
+}
+[dir='rtl'] .slick-prev
+{
+    right: -25px;
+    left: auto;
+}
+.slick-prev:before
+{
+    content: '←';
+}
+[dir='rtl'] .slick-prev:before
+{
+    content: '→';
+}
+
+.slick-next
+{
+    right: -25px;
+}
+[dir='rtl'] .slick-next
+{
+    right: auto;
+    left: -25px;
+}
+.slick-next:before
+{
+    content: '→';
+}
+[dir='rtl'] .slick-next:before
+{
+    content: '←';
+}
+
+/* Dots */
+.slick-dotted.slick-slider
+{
+    margin-bottom: 30px;
+}
+
+.slick-dots
+{
+    position: absolute;
+    bottom: -25px;
+
+    display: block;
+
+    width: 100%;
+    padding: 0;
+    margin: 0;
+
+    list-style: none;
+
+    text-align: center;
+}
+.slick-dots li
+{
+    position: relative;
+
+    display: inline-block;
+
+    width: 20px;
+    height: 20px;
+    margin: 0 5px;
+    padding: 0;
+
+    cursor: pointer;
+}
+.slick-dots li button
+{
+    font-size: 0;
+    line-height: 0;
+
+    display: block;
+
+    width: 20px;
+    height: 20px;
+    padding: 5px;
+
+    cursor: pointer;
+
+    color: transparent;
+    border: 0;
+    outline: none;
+    background: transparent;
+}
+.slick-dots li button:hover,
+.slick-dots li button:focus
+{
+    outline: none;
+}
+.slick-dots li button:hover:before,
+.slick-dots li button:focus:before
+{
+    opacity: 1;
+}
+.slick-dots li button:before
+{
+    font-family: 'slick';
+    font-size: 6px;
+    line-height: 20px;
+
+    position: absolute;
+    top: 0;
+    left: 0;
+
+    width: 20px;
+    height: 20px;
+
+    content: '•';
+    text-align: center;
+
+    opacity: .25;
+    color: black;
+
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+.slick-dots li.slick-active button:before
+{
+    opacity: .75;
+    color: black;
+}

+ 168 - 0
bower_components/slick/slick-theme.less

@@ -0,0 +1,168 @@
+@charset "UTF-8";
+
+// Default Variables
+
+@slick-font-path: "./fonts/";
+@slick-font-family: "slick";
+@slick-loader-path: "./";
+@slick-arrow-color: white;
+@slick-dot-color: black;
+@slick-dot-color-active: @slick-dot-color;
+@slick-prev-character: "←";
+@slick-next-character: "→";
+@slick-dot-character: "•";
+@slick-dot-size: 6px;
+@slick-opacity-default: 0.75;
+@slick-opacity-on-hover: 1;
+@slick-opacity-not-active: 0.25;
+
+/* Slider */
+.slick-loading .slick-list{
+    background: #fff url('@{slick-loader-path}ajax-loader.gif') center center no-repeat;
+}
+
+/* Icons */
+@font-face{
+    font-family: 'slick';
+    font-weight: normal;
+    font-style: normal;
+
+    src: url('@{slick-font-path}slick.eot');
+    src: url('@{slick-font-path}slick.eot?#iefix') format('embedded-opentype'), url('@{slick-font-path}slick.woff') format('woff'), url('@{slick-font-path}slick.ttf') format('truetype'), url('@{slick-font-path}slick.svg#slick') format('svg');
+}
+
+/* Arrows */
+
+.slick-prev,
+.slick-next {
+    position: absolute;
+    display: block;
+    height: 20px;
+    width: 20px;
+    line-height: 0px;
+    font-size: 0px;
+    cursor: pointer;
+    background: transparent;
+    color: transparent;
+    top: 50%;
+    -webkit-transform: translate(0, -50%);
+    -ms-transform: translate(0, -50%);
+    transform: translate(0, -50%);
+    padding: 0;
+    border: none;
+    outline: none;
+    &:hover, &:focus {
+        outline: none;
+        background: transparent;
+        color: transparent;
+        &:before {
+            opacity: @slick-opacity-on-hover;
+        }
+    }
+    &.slick-disabled:before {
+        opacity: @slick-opacity-not-active;
+    }
+}
+
+.slick-prev:before, .slick-next:before {
+    font-family: @slick-font-family;
+    font-size: 20px;
+    line-height: 1;
+    color: @slick-arrow-color;
+    opacity: @slick-opacity-default;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+
+.slick-prev {
+    left: -25px;
+    &[dir="rtl"] {
+        left: auto;
+        right: -25px;
+    }
+    &:before {
+        content: @slick-prev-character;
+        &[dir="rtl"] {
+            content: @slick-next-character;
+        }
+    }
+}
+
+.slick-next {
+    right: -25px;
+    &[dir="rtl"] {
+        left: -25px;
+        right: auto;
+    }
+    &:before {
+        content: @slick-next-character;
+        &[dir="rtl"] {
+            content: @slick-prev-character;
+        }
+    }
+}
+
+/* Dots */
+
+.slick-dotted .slick-slider {
+    margin-bottom: 30px;
+}
+
+.slick-dots {
+    position: absolute;
+    bottom: -25px;
+    list-style: none;
+    display: block;
+    text-align: center;
+    padding: 0;
+    margin: 0;
+    width: 100%;
+    li {
+        position: relative;
+        display: inline-block;
+        height: 20px;
+        width: 20px;
+        margin: 0 5px;
+        padding: 0;
+        cursor: pointer;
+        button {
+            border: 0;
+            background: transparent;
+            display: block;
+            height: 20px;
+            width: 20px;
+            outline: none;
+            line-height: 0px;
+            font-size: 0px;
+            color: transparent;
+            padding: 5px;
+            cursor: pointer;
+            &:hover, &:focus {
+                outline: none;
+                &:before {
+                    opacity: @slick-opacity-on-hover;
+                }
+            }
+            &:before {
+                position: absolute;
+                top: 0;
+                left: 0;
+                content: @slick-dot-character;
+                width: 20px;
+                height: 20px;
+                font-family: @slick-font-family;
+                font-size: @slick-dot-size;
+                line-height: 20px;
+                text-align: center;
+                color: @slick-dot-color;
+                opacity: @slick-opacity-not-active;
+                -webkit-font-smoothing: antialiased;
+                -moz-osx-font-smoothing: grayscale;
+            }
+        }
+        &.slick-active button:before {
+            color: @slick-dot-color-active;
+            opacity: @slick-opacity-default;
+        }
+    }
+}

+ 194 - 0
bower_components/slick/slick-theme.scss

@@ -0,0 +1,194 @@
+@charset "UTF-8";
+
+// Default Variables
+
+// Slick icon entity codes outputs the following
+// "\2190" outputs ascii character "←"
+// "\2192" outputs ascii character "→"
+// "\2022" outputs ascii character "•"
+
+$slick-font-path: "./fonts/" !default;
+$slick-font-family: "slick" !default;
+$slick-loader-path: "./" !default;
+$slick-arrow-color: white !default;
+$slick-dot-color: black !default;
+$slick-dot-color-active: $slick-dot-color !default;
+$slick-prev-character: "\2190" !default;
+$slick-next-character: "\2192" !default;
+$slick-dot-character: "\2022" !default;
+$slick-dot-size: 6px !default;
+$slick-opacity-default: 0.75 !default;
+$slick-opacity-on-hover: 1 !default;
+$slick-opacity-not-active: 0.25 !default;
+
+@function slick-image-url($url) {
+    @if function-exists(image-url) {
+        @return image-url($url);
+    }
+    @else {
+        @return url($slick-loader-path + $url);
+    }
+}
+
+@function slick-font-url($url) {
+    @if function-exists(font-url) {
+        @return font-url($url);
+    }
+    @else {
+        @return url($slick-font-path + $url);
+    }
+}
+
+/* Slider */
+
+.slick-list {
+    .slick-loading & {
+        background: #fff slick-image-url("ajax-loader.gif") center center no-repeat;
+    }
+}
+
+/* Icons */
+@if $slick-font-family == "slick" {
+    @font-face {
+        font-family: "slick";
+        src: slick-font-url("slick.eot");
+        src: slick-font-url("slick.eot?#iefix") format("embedded-opentype"), slick-font-url("slick.woff") format("woff"), slick-font-url("slick.ttf") format("truetype"), slick-font-url("slick.svg#slick") format("svg");
+        font-weight: normal;
+        font-style: normal;
+    }
+}
+
+/* Arrows */
+
+.slick-prev,
+.slick-next {
+    position: absolute;
+    display: block;
+    height: 20px;
+    width: 20px;
+    line-height: 0px;
+    font-size: 0px;
+    cursor: pointer;
+    background: transparent;
+    color: transparent;
+    top: 50%;
+    -webkit-transform: translate(0, -50%);
+    -ms-transform: translate(0, -50%);
+    transform: translate(0, -50%);
+    padding: 0;
+    border: none;
+    outline: none;
+    &:hover, &:focus {
+        outline: none;
+        background: transparent;
+        color: transparent;
+        &:before {
+            opacity: $slick-opacity-on-hover;
+        }
+    }
+    &.slick-disabled:before {
+        opacity: $slick-opacity-not-active;
+    }
+    &:before {
+        font-family: $slick-font-family;
+        font-size: 20px;
+        line-height: 1;
+        color: $slick-arrow-color;
+        opacity: $slick-opacity-default;
+        -webkit-font-smoothing: antialiased;
+        -moz-osx-font-smoothing: grayscale;
+    }
+}
+
+.slick-prev {
+    left: -25px;
+    [dir="rtl"] & {
+        left: auto;
+        right: -25px;
+    }
+    &:before {
+        content: $slick-prev-character;
+        [dir="rtl"] & {
+            content: $slick-next-character;
+        }
+    }
+}
+
+.slick-next {
+    right: -25px;
+    [dir="rtl"] & {
+        left: -25px;
+        right: auto;
+    }
+    &:before {
+        content: $slick-next-character;
+        [dir="rtl"] & {
+            content: $slick-prev-character;
+        }
+    }
+}
+
+/* Dots */
+
+.slick-dotted.slick-slider {
+    margin-bottom: 30px;
+}
+
+.slick-dots {
+    position: absolute;
+    bottom: -25px;
+    list-style: none;
+    display: block;
+    text-align: center;
+    padding: 0;
+    margin: 0;
+    width: 100%;
+    li {
+        position: relative;
+        display: inline-block;
+        height: 20px;
+        width: 20px;
+        margin: 0 5px;
+        padding: 0;
+        cursor: pointer;
+        button {
+            border: 0;
+            background: transparent;
+            display: block;
+            height: 20px;
+            width: 20px;
+            outline: none;
+            line-height: 0px;
+            font-size: 0px;
+            color: transparent;
+            padding: 5px;
+            cursor: pointer;
+            &:hover, &:focus {
+                outline: none;
+                &:before {
+                    opacity: $slick-opacity-on-hover;
+                }
+            }
+            &:before {
+                position: absolute;
+                top: 0;
+                left: 0;
+                content: $slick-dot-character;
+                width: 20px;
+                height: 20px;
+                font-family: $slick-font-family;
+                font-size: $slick-dot-size;
+                line-height: 20px;
+                text-align: center;
+                color: $slick-dot-color;
+                opacity: $slick-opacity-not-active;
+                -webkit-font-smoothing: antialiased;
+                -moz-osx-font-smoothing: grayscale;
+            }
+        }
+        &.slick-active button:before {
+            color: $slick-dot-color-active;
+            opacity: $slick-opacity-default;
+        }
+    }
+}

+ 121 - 0
bower_components/slick/slick.css

@@ -0,0 +1,121 @@
+/* Slider */
+.slick-slider
+{
+    position: relative;
+width: 1px;
+  min-width: 100%;
+    display: block;
+    box-sizing: border-box;
+
+    -webkit-user-select: none;
+       -moz-user-select: none;
+        -ms-user-select: none;
+            user-select: none;
+
+    -webkit-touch-callout: none;
+    -khtml-user-select: none;
+    -ms-touch-action: pan-y;
+        touch-action: pan-y;
+    -webkit-tap-highlight-color: transparent;
+}
+
+.slick-list
+{
+    position: relative;
+
+    display: block;
+    overflow: hidden;
+width: 1px;
+  min-width: 100%;
+    margin: 0;
+    padding: 0;
+}
+.slick-list:focus
+{
+    outline: none;
+}
+.slick-list.dragging
+{
+    cursor: pointer;
+    cursor: hand;
+}
+
+.slick-slider .slick-track,
+.slick-slider .slick-list
+{
+    -webkit-transform: translate3d(0, 0, 0);
+       -moz-transform: translate3d(0, 0, 0);
+        -ms-transform: translate3d(0, 0, 0);
+         -o-transform: translate3d(0, 0, 0);
+            transform: translate3d(0, 0, 0);
+}
+
+.slick-track
+{
+    position: relative;
+    top: 0;
+    left: 0;
+
+    display: block;
+    width: 1px;
+  min-width: 100%;
+}
+.slick-track:before,
+.slick-track:after
+{
+    display: table;
+
+    content: '';
+}
+.slick-track:after
+{
+    clear: both;
+}
+.slick-loading .slick-track
+{
+    visibility: hidden;
+}
+
+.slick-slide
+{
+    display: none;
+    float: left;
+
+    height: 100%;
+    min-height: 1px;
+}
+[dir='rtl'] .slick-slide
+{
+    float: right;
+}
+.slick-slide img
+{
+    display: block;
+}
+.slick-slide.slick-loading img
+{
+    display: none;
+}
+.slick-slide.dragging img
+{
+    pointer-events: none;
+}
+.slick-initialized .slick-slide
+{
+    display: block;
+}
+.slick-loading .slick-slide
+{
+    visibility: hidden;
+}
+.slick-vertical .slick-slide
+{
+    display: block;
+
+    height: auto;
+
+    border: 1px solid transparent;
+}
+.slick-arrow.slick-hidden {
+    display: none;
+}

+ 2892 - 0
bower_components/slick/slick.js

@@ -0,0 +1,2892 @@
+/*
+     _ _      _       _
+ ___| (_) ___| | __  (_)___
+/ __| | |/ __| |/ /  | / __|
+\__ \ | | (__|   < _ | \__ \
+|___/_|_|\___|_|\_(_)/ |___/
+                   |__/
+
+ Version: 1.6.0
+  Author: Ken Wheeler
+ Website: http://kenwheeler.github.io
+    Docs: http://kenwheeler.github.io/slick
+    Repo: http://github.com/kenwheeler/slick
+  Issues: http://github.com/kenwheeler/slick/issues
+
+ */
+/* global window, document, define, jQuery, setInterval, clearInterval */
+(function(factory) {
+    'use strict';
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery'], factory);
+    } else if (typeof exports !== 'undefined') {
+        module.exports = factory(require('jquery'));
+    } else {
+        factory(jQuery);
+    }
+
+}(function($) {
+    'use strict';
+    var Slick = window.Slick || {};
+
+    Slick = (function() {
+
+        var instanceUid = 0;
+
+        function Slick(element, settings) {
+
+            var _ = this, dataSettings;
+
+            _.defaults = {
+                accessibility: true,
+                adaptiveHeight: false,
+                appendArrows: $(element),
+                appendDots: $(element),
+                arrows: true,
+                asNavFor: null,
+                prevArrow: '<button type="button" data-role="none" class="slick-prev" aria-label="Previous" tabindex="0" role="button">Previous</button>',
+                nextArrow: '<button type="button" data-role="none" class="slick-next" aria-label="Next" tabindex="0" role="button">Next</button>',
+                autoplay: false,
+                autoplaySpeed: 3000,
+                centerMode: false,
+                centerPadding: '50px',
+                cssEase: 'ease',
+                customPaging: function(slider, i) {
+                    return $('<button type="button" data-role="none" role="button" tabindex="0" />').text(i + 1);
+                },
+                dots: false,
+                dotsClass: 'slick-dots',
+                draggable: true,
+                easing: 'linear',
+                edgeFriction: 0.35,
+                fade: false,
+                focusOnSelect: false,
+                infinite: true,
+                initialSlide: 0,
+                lazyLoad: 'ondemand',
+                mobileFirst: false,
+                pauseOnHover: true,
+                pauseOnFocus: true,
+                pauseOnDotsHover: false,
+                respondTo: 'window',
+                responsive: null,
+                rows: 1,
+                rtl: false,
+                slide: '',
+                slidesPerRow: 1,
+                slidesToShow: 1,
+                slidesToScroll: 1,
+                speed: 500,
+                swipe: true,
+                swipeToSlide: false,
+                touchMove: true,
+                touchThreshold: 5,
+                useCSS: true,
+                useTransform: true,
+                variableWidth: false,
+                vertical: false,
+                verticalSwiping: false,
+                waitForAnimate: true,
+                zIndex: 1000
+            };
+
+            _.initials = {
+                animating: false,
+                dragging: false,
+                autoPlayTimer: null,
+                currentDirection: 0,
+                currentLeft: null,
+                currentSlide: 0,
+                direction: 1,
+                $dots: null,
+                listWidth: null,
+                listHeight: null,
+                loadIndex: 0,
+                $nextArrow: null,
+                $prevArrow: null,
+                slideCount: null,
+                slideWidth: null,
+                $slideTrack: null,
+                $slides: null,
+                sliding: false,
+                slideOffset: 0,
+                swipeLeft: null,
+                $list: null,
+                touchObject: {},
+                transformsEnabled: false,
+                unslicked: false
+            };
+
+            $.extend(_, _.initials);
+
+            _.activeBreakpoint = null;
+            _.animType = null;
+            _.animProp = null;
+            _.breakpoints = [];
+            _.breakpointSettings = [];
+            _.cssTransitions = false;
+            _.focussed = false;
+            _.interrupted = false;
+            _.hidden = 'hidden';
+            _.paused = true;
+            _.positionProp = null;
+            _.respondTo = null;
+            _.rowCount = 1;
+            _.shouldClick = true;
+            _.$slider = $(element);
+            _.$slidesCache = null;
+            _.transformType = null;
+            _.transitionType = null;
+            _.visibilityChange = 'visibilitychange';
+            _.windowWidth = 0;
+            _.windowTimer = null;
+
+            dataSettings = $(element).data('slick') || {};
+
+            _.options = $.extend({}, _.defaults, settings, dataSettings);
+
+            _.currentSlide = _.options.initialSlide;
+
+            _.originalSettings = _.options;
+
+            if (typeof document.mozHidden !== 'undefined') {
+                _.hidden = 'mozHidden';
+                _.visibilityChange = 'mozvisibilitychange';
+            } else if (typeof document.webkitHidden !== 'undefined') {
+                _.hidden = 'webkitHidden';
+                _.visibilityChange = 'webkitvisibilitychange';
+            }
+
+            _.autoPlay = $.proxy(_.autoPlay, _);
+            _.autoPlayClear = $.proxy(_.autoPlayClear, _);
+            _.autoPlayIterator = $.proxy(_.autoPlayIterator, _);
+            _.changeSlide = $.proxy(_.changeSlide, _);
+            _.clickHandler = $.proxy(_.clickHandler, _);
+            _.selectHandler = $.proxy(_.selectHandler, _);
+            _.setPosition = $.proxy(_.setPosition, _);
+            _.swipeHandler = $.proxy(_.swipeHandler, _);
+            _.dragHandler = $.proxy(_.dragHandler, _);
+            _.keyHandler = $.proxy(_.keyHandler, _);
+
+            _.instanceUid = instanceUid++;
+
+            // A simple way to check for HTML strings
+            // Strict HTML recognition (must start with <)
+            // Extracted from jQuery v1.11 source
+            _.htmlExpr = /^(?:\s*(<[\w\W]+>)[^>]*)$/;
+
+
+            _.registerBreakpoints();
+            _.init(true);
+
+        }
+
+        return Slick;
+
+    }());
+
+    Slick.prototype.activateADA = function() {
+        var _ = this;
+
+        _.$slideTrack.find('.slick-active').attr({
+            'aria-hidden': 'false'
+        }).find('a, input, button, select').attr({
+            'tabindex': '0'
+        });
+
+    };
+
+    Slick.prototype.addSlide = Slick.prototype.slickAdd = function(markup, index, addBefore) {
+
+        var _ = this;
+
+        if (typeof(index) === 'boolean') {
+            addBefore = index;
+            index = null;
+        } else if (index < 0 || (index >= _.slideCount)) {
+            return false;
+        }
+
+        _.unload();
+
+        if (typeof(index) === 'number') {
+            if (index === 0 && _.$slides.length === 0) {
+                $(markup).appendTo(_.$slideTrack);
+            } else if (addBefore) {
+                $(markup).insertBefore(_.$slides.eq(index));
+            } else {
+                $(markup).insertAfter(_.$slides.eq(index));
+            }
+        } else {
+            if (addBefore === true) {
+                $(markup).prependTo(_.$slideTrack);
+            } else {
+                $(markup).appendTo(_.$slideTrack);
+            }
+        }
+
+        _.$slides = _.$slideTrack.children(this.options.slide);
+
+        _.$slideTrack.children(this.options.slide).detach();
+
+        _.$slideTrack.append(_.$slides);
+
+        _.$slides.each(function(index, element) {
+            $(element).attr('data-slick-index', index);
+        });
+
+        _.$slidesCache = _.$slides;
+
+        _.reinit();
+
+    };
+
+    Slick.prototype.animateHeight = function() {
+        var _ = this;
+        if (_.options.slidesToShow === 1 && _.options.adaptiveHeight === true && _.options.vertical === false) {
+            var targetHeight = _.$slides.eq(_.currentSlide).outerHeight(true);
+            _.$list.animate({
+                height: targetHeight
+            }, _.options.speed);
+        }
+    };
+
+    Slick.prototype.animateSlide = function(targetLeft, callback) {
+
+        var animProps = {},
+            _ = this;
+
+        _.animateHeight();
+
+        if (_.options.rtl === true && _.options.vertical === false) {
+            targetLeft = -targetLeft;
+        }
+        if (_.transformsEnabled === false) {
+            if (_.options.vertical === false) {
+                _.$slideTrack.animate({
+                    left: targetLeft
+                }, _.options.speed, _.options.easing, callback);
+            } else {
+                _.$slideTrack.animate({
+                    top: targetLeft
+                }, _.options.speed, _.options.easing, callback);
+            }
+
+        } else {
+
+            if (_.cssTransitions === false) {
+                if (_.options.rtl === true) {
+                    _.currentLeft = -(_.currentLeft);
+                }
+                $({
+                    animStart: _.currentLeft
+                }).animate({
+                    animStart: targetLeft
+                }, {
+                    duration: _.options.speed,
+                    easing: _.options.easing,
+                    step: function(now) {
+                        now = Math.ceil(now);
+                        if (_.options.vertical === false) {
+                            animProps[_.animType] = 'translate(' +
+                                now + 'px, 0px)';
+                            _.$slideTrack.css(animProps);
+                        } else {
+                            animProps[_.animType] = 'translate(0px,' +
+                                now + 'px)';
+                            _.$slideTrack.css(animProps);
+                        }
+                    },
+                    complete: function() {
+                        if (callback) {
+                            callback.call();
+                        }
+                    }
+                });
+
+            } else {
+
+                _.applyTransition();
+                targetLeft = Math.ceil(targetLeft);
+
+                if (_.options.vertical === false) {
+                    animProps[_.animType] = 'translate3d(' + targetLeft + 'px, 0px, 0px)';
+                } else {
+                    animProps[_.animType] = 'translate3d(0px,' + targetLeft + 'px, 0px)';
+                }
+                _.$slideTrack.css(animProps);
+
+                if (callback) {
+                    setTimeout(function() {
+
+                        _.disableTransition();
+
+                        callback.call();
+                    }, _.options.speed);
+                }
+
+            }
+
+        }
+
+    };
+
+    Slick.prototype.getNavTarget = function() {
+
+        var _ = this,
+            asNavFor = _.options.asNavFor;
+
+        if ( asNavFor && asNavFor !== null ) {
+            asNavFor = $(asNavFor).not(_.$slider);
+        }
+
+        return asNavFor;
+
+    };
+
+    Slick.prototype.asNavFor = function(index) {
+
+        var _ = this,
+            asNavFor = _.getNavTarget();
+
+        if ( asNavFor !== null && typeof asNavFor === 'object' ) {
+            asNavFor.each(function() {
+                var target = $(this).slick('getSlick');
+                if(!target.unslicked) {
+                    target.slideHandler(index, true);
+                }
+            });
+        }
+
+    };
+
+    Slick.prototype.applyTransition = function(slide) {
+
+        var _ = this,
+            transition = {};
+
+        if (_.options.fade === false) {
+            transition[_.transitionType] = _.transformType + ' ' + _.options.speed + 'ms ' + _.options.cssEase;
+        } else {
+            transition[_.transitionType] = 'opacity ' + _.options.speed + 'ms ' + _.options.cssEase;
+        }
+
+        if (_.options.fade === false) {
+            _.$slideTrack.css(transition);
+        } else {
+            _.$slides.eq(slide).css(transition);
+        }
+
+    };
+
+    Slick.prototype.autoPlay = function() {
+
+        var _ = this;
+
+        _.autoPlayClear();
+
+        if ( _.slideCount > _.options.slidesToShow ) {
+            _.autoPlayTimer = setInterval( _.autoPlayIterator, _.options.autoplaySpeed );
+        }
+
+    };
+
+    Slick.prototype.autoPlayClear = function() {
+
+        var _ = this;
+
+        if (_.autoPlayTimer) {
+            clearInterval(_.autoPlayTimer);
+        }
+
+    };
+
+    Slick.prototype.autoPlayIterator = function() {
+
+        var _ = this,
+            slideTo = _.currentSlide + _.options.slidesToScroll;
+
+        if ( !_.paused && !_.interrupted && !_.focussed ) {
+
+            if ( _.options.infinite === false ) {
+
+                if ( _.direction === 1 && ( _.currentSlide + 1 ) === ( _.slideCount - 1 )) {
+                    _.direction = 0;
+                }
+
+                else if ( _.direction === 0 ) {
+
+                    slideTo = _.currentSlide - _.options.slidesToScroll;
+
+                    if ( _.currentSlide - 1 === 0 ) {
+                        _.direction = 1;
+                    }
+
+                }
+
+            }
+
+            _.slideHandler( slideTo );
+
+        }
+
+    };
+
+    Slick.prototype.buildArrows = function() {
+
+        var _ = this;
+
+        if (_.options.arrows === true ) {
+
+            _.$prevArrow = $(_.options.prevArrow).addClass('slick-arrow');
+            _.$nextArrow = $(_.options.nextArrow).addClass('slick-arrow');
+
+            if( _.slideCount > _.options.slidesToShow ) {
+
+                _.$prevArrow.removeClass('slick-hidden').removeAttr('aria-hidden tabindex');
+                _.$nextArrow.removeClass('slick-hidden').removeAttr('aria-hidden tabindex');
+
+                if (_.htmlExpr.test(_.options.prevArrow)) {
+                    _.$prevArrow.prependTo(_.options.appendArrows);
+                }
+
+                if (_.htmlExpr.test(_.options.nextArrow)) {
+                    _.$nextArrow.appendTo(_.options.appendArrows);
+                }
+
+                if (_.options.infinite !== true) {
+                    _.$prevArrow
+                        .addClass('slick-disabled')
+                        .attr('aria-disabled', 'true');
+                }
+
+            } else {
+
+                _.$prevArrow.add( _.$nextArrow )
+
+                    .addClass('slick-hidden')
+                    .attr({
+                        'aria-disabled': 'true',
+                        'tabindex': '-1'
+                    });
+
+            }
+
+        }
+
+    };
+
+    Slick.prototype.buildDots = function() {
+
+        var _ = this,
+            i, dot;
+
+        if (_.options.dots === true && _.slideCount > _.options.slidesToShow) {
+
+            _.$slider.addClass('slick-dotted');
+
+            dot = $('<ul />').addClass(_.options.dotsClass);
+
+            for (i = 0; i <= _.getDotCount(); i += 1) {
+                dot.append($('<li />').append(_.options.customPaging.call(this, _, i)));
+            }
+
+            _.$dots = dot.appendTo(_.options.appendDots);
+
+            _.$dots.find('li').first().addClass('slick-active').attr('aria-hidden', 'false');
+
+        }
+
+    };
+
+    Slick.prototype.buildOut = function() {
+
+        var _ = this;
+
+        _.$slides =
+            _.$slider
+                .children( _.options.slide + ':not(.slick-cloned)')
+                .addClass('slick-slide');
+
+        _.slideCount = _.$slides.length;
+
+        _.$slides.each(function(index, element) {
+            $(element)
+                .attr('data-slick-index', index)
+                .data('originalStyling', $(element).attr('style') || '');
+        });
+
+        _.$slider.addClass('slick-slider');
+
+        _.$slideTrack = (_.slideCount === 0) ?
+            $('<div class="slick-track"/>').appendTo(_.$slider) :
+            _.$slides.wrapAll('<div class="slick-track"/>').parent();
+
+        _.$list = _.$slideTrack.wrap(
+            '<div aria-live="polite" class="slick-list"/>').parent();
+        _.$slideTrack.css('opacity', 0);
+
+        if (_.options.centerMode === true || _.options.swipeToSlide === true) {
+            _.options.slidesToScroll = 1;
+        }
+
+        $('img[data-lazy]', _.$slider).not('[src]').addClass('slick-loading');
+
+        _.setupInfinite();
+
+        _.buildArrows();
+
+        _.buildDots();
+
+        _.updateDots();
+
+
+        _.setSlideClasses(typeof _.currentSlide === 'number' ? _.currentSlide : 0);
+
+        if (_.options.draggable === true) {
+            _.$list.addClass('draggable');
+        }
+
+    };
+
+    Slick.prototype.buildRows = function() {
+
+        var _ = this, a, b, c, newSlides, numOfSlides, originalSlides,slidesPerSection;
+
+        newSlides = document.createDocumentFragment();
+        originalSlides = _.$slider.children();
+
+        if(_.options.rows > 1) {
+
+            slidesPerSection = _.options.slidesPerRow * _.options.rows;
+            numOfSlides = Math.ceil(
+                originalSlides.length / slidesPerSection
+            );
+
+            for(a = 0; a < numOfSlides; a++){
+                var slide = document.createElement('div');
+                for(b = 0; b < _.options.rows; b++) {
+                    var row = document.createElement('div');
+                    for(c = 0; c < _.options.slidesPerRow; c++) {
+                        var target = (a * slidesPerSection + ((b * _.options.slidesPerRow) + c));
+                        if (originalSlides.get(target)) {
+                            row.appendChild(originalSlides.get(target));
+                        }
+                    }
+                    slide.appendChild(row);
+                }
+                newSlides.appendChild(slide);
+            }
+
+            _.$slider.empty().append(newSlides);
+            _.$slider.children().children().children()
+                .css({
+                    'width':(100 / _.options.slidesPerRow) + '%',
+                    'display': 'inline-block'
+                });
+
+        }
+
+    };
+
+    Slick.prototype.checkResponsive = function(initial, forceUpdate) {
+
+        var _ = this,
+            breakpoint, targetBreakpoint, respondToWidth, triggerBreakpoint = false;
+        var sliderWidth = _.$slider.width();
+        var windowWidth = window.innerWidth || $(window).width();
+
+        if (_.respondTo === 'window') {
+            respondToWidth = windowWidth;
+        } else if (_.respondTo === 'slider') {
+            respondToWidth = sliderWidth;
+        } else if (_.respondTo === 'min') {
+            respondToWidth = Math.min(windowWidth, sliderWidth);
+        }
+
+        if ( _.options.responsive &&
+            _.options.responsive.length &&
+            _.options.responsive !== null) {
+
+            targetBreakpoint = null;
+
+            for (breakpoint in _.breakpoints) {
+                if (_.breakpoints.hasOwnProperty(breakpoint)) {
+                    if (_.originalSettings.mobileFirst === false) {
+                        if (respondToWidth < _.breakpoints[breakpoint]) {
+                            targetBreakpoint = _.breakpoints[breakpoint];
+                        }
+                    } else {
+                        if (respondToWidth > _.breakpoints[breakpoint]) {
+                            targetBreakpoint = _.breakpoints[breakpoint];
+                        }
+                    }
+                }
+            }
+
+            if (targetBreakpoint !== null) {
+                if (_.activeBreakpoint !== null) {
+                    if (targetBreakpoint !== _.activeBreakpoint || forceUpdate) {
+                        _.activeBreakpoint =
+                            targetBreakpoint;
+                        if (_.breakpointSettings[targetBreakpoint] === 'unslick') {
+                            _.unslick(targetBreakpoint);
+                        } else {
+                            _.options = $.extend({}, _.originalSettings,
+                                _.breakpointSettings[
+                                    targetBreakpoint]);
+                            if (initial === true) {
+                                _.currentSlide = _.options.initialSlide;
+                            }
+                            _.refresh(initial);
+                        }
+                        triggerBreakpoint = targetBreakpoint;
+                    }
+                } else {
+                    _.activeBreakpoint = targetBreakpoint;
+                    if (_.breakpointSettings[targetBreakpoint] === 'unslick') {
+                        _.unslick(targetBreakpoint);
+                    } else {
+                        _.options = $.extend({}, _.originalSettings,
+                            _.breakpointSettings[
+                                targetBreakpoint]);
+                        if (initial === true) {
+                            _.currentSlide = _.options.initialSlide;
+                        }
+                        _.refresh(initial);
+                    }
+                    triggerBreakpoint = targetBreakpoint;
+                }
+            } else {
+                if (_.activeBreakpoint !== null) {
+                    _.activeBreakpoint = null;
+                    _.options = _.originalSettings;
+                    if (initial === true) {
+                        _.currentSlide = _.options.initialSlide;
+                    }
+                    _.refresh(initial);
+                    triggerBreakpoint = targetBreakpoint;
+                }
+            }
+
+            // only trigger breakpoints during an actual break. not on initialize.
+            if( !initial && triggerBreakpoint !== false ) {
+                _.$slider.trigger('breakpoint', [_, triggerBreakpoint]);
+            }
+        }
+
+    };
+
+    Slick.prototype.changeSlide = function(event, dontAnimate) {
+
+        var _ = this,
+            $target = $(event.currentTarget),
+            indexOffset, slideOffset, unevenOffset;
+
+        // If target is a link, prevent default action.
+        if($target.is('a')) {
+            event.preventDefault();
+        }
+
+        // If target is not the <li> element (ie: a child), find the <li>.
+        if(!$target.is('li')) {
+            $target = $target.closest('li');
+        }
+
+        unevenOffset = (_.slideCount % _.options.slidesToScroll !== 0);
+        indexOffset = unevenOffset ? 0 : (_.slideCount - _.currentSlide) % _.options.slidesToScroll;
+
+        switch (event.data.message) {
+
+            case 'previous':
+                slideOffset = indexOffset === 0 ? _.options.slidesToScroll : _.options.slidesToShow - indexOffset;
+                if (_.slideCount > _.options.slidesToShow) {
+                    _.slideHandler(_.currentSlide - slideOffset, false, dontAnimate);
+                }
+                break;
+
+            case 'next':
+                slideOffset = indexOffset === 0 ? _.options.slidesToScroll : indexOffset;
+                if (_.slideCount > _.options.slidesToShow) {
+                    _.slideHandler(_.currentSlide + slideOffset, false, dontAnimate);
+                }
+                break;
+
+            case 'index':
+                var index = event.data.index === 0 ? 0 :
+                    event.data.index || $target.index() * _.options.slidesToScroll;
+
+                _.slideHandler(_.checkNavigable(index), false, dontAnimate);
+                $target.children().trigger('focus');
+                break;
+
+            default:
+                return;
+        }
+
+    };
+
+    Slick.prototype.checkNavigable = function(index) {
+
+        var _ = this,
+            navigables, prevNavigable;
+
+        navigables = _.getNavigableIndexes();
+        prevNavigable = 0;
+        if (index > navigables[navigables.length - 1]) {
+            index = navigables[navigables.length - 1];
+        } else {
+            for (var n in navigables) {
+                if (index < navigables[n]) {
+                    index = prevNavigable;
+                    break;
+                }
+                prevNavigable = navigables[n];
+            }
+        }
+
+        return index;
+    };
+
+    Slick.prototype.cleanUpEvents = function() {
+
+        var _ = this;
+
+        if (_.options.dots && _.$dots !== null) {
+
+            $('li', _.$dots)
+                .off('click.slick', _.changeSlide)
+                .off('mouseenter.slick', $.proxy(_.interrupt, _, true))
+                .off('mouseleave.slick', $.proxy(_.interrupt, _, false));
+
+        }
+
+        _.$slider.off('focus.slick blur.slick');
+
+        if (_.options.arrows === true && _.slideCount > _.options.slidesToShow) {
+            _.$prevArrow && _.$prevArrow.off('click.slick', _.changeSlide);
+            _.$nextArrow && _.$nextArrow.off('click.slick', _.changeSlide);
+        }
+
+        _.$list.off('touchstart.slick mousedown.slick', _.swipeHandler);
+        _.$list.off('touchmove.slick mousemove.slick', _.swipeHandler);
+        _.$list.off('touchend.slick mouseup.slick', _.swipeHandler);
+        _.$list.off('touchcancel.slick mouseleave.slick', _.swipeHandler);
+
+        _.$list.off('click.slick', _.clickHandler);
+
+        $(document).off(_.visibilityChange, _.visibility);
+
+        _.cleanUpSlideEvents();
+
+        if (_.options.accessibility === true) {
+            _.$list.off('keydown.slick', _.keyHandler);
+        }
+
+        if (_.options.focusOnSelect === true) {
+            $(_.$slideTrack).children().off('click.slick', _.selectHandler);
+        }
+
+        $(window).off('orientationchange.slick.slick-' + _.instanceUid, _.orientationChange);
+
+        $(window).off('resize.slick.slick-' + _.instanceUid, _.resize);
+
+        $('[draggable!=true]', _.$slideTrack).off('dragstart', _.preventDefault);
+
+        $(window).off('load.slick.slick-' + _.instanceUid, _.setPosition);
+        $(document).off('ready.slick.slick-' + _.instanceUid, _.setPosition);
+
+    };
+
+    Slick.prototype.cleanUpSlideEvents = function() {
+
+        var _ = this;
+
+        _.$list.off('mouseenter.slick', $.proxy(_.interrupt, _, true));
+        _.$list.off('mouseleave.slick', $.proxy(_.interrupt, _, false));
+
+    };
+
+    Slick.prototype.cleanUpRows = function() {
+
+        var _ = this, originalSlides;
+
+        if(_.options.rows > 1) {
+            originalSlides = _.$slides.children().children();
+            originalSlides.removeAttr('style');
+            _.$slider.empty().append(originalSlides);
+        }
+
+    };
+
+    Slick.prototype.clickHandler = function(event) {
+
+        var _ = this;
+
+        if (_.shouldClick === false) {
+            event.stopImmediatePropagation();
+            event.stopPropagation();
+            event.preventDefault();
+        }
+
+    };
+
+    Slick.prototype.destroy = function(refresh) {
+
+        var _ = this;
+
+        _.autoPlayClear();
+
+        _.touchObject = {};
+
+        _.cleanUpEvents();
+
+        $('.slick-cloned', _.$slider).detach();
+
+        if (_.$dots) {
+            _.$dots.remove();
+        }
+
+
+        if ( _.$prevArrow && _.$prevArrow.length ) {
+
+            _.$prevArrow
+                .removeClass('slick-disabled slick-arrow slick-hidden')
+                .removeAttr('aria-hidden aria-disabled tabindex')
+                .css('display','');
+
+            if ( _.htmlExpr.test( _.options.prevArrow )) {
+                _.$prevArrow.remove();
+            }
+        }
+
+        if ( _.$nextArrow && _.$nextArrow.length ) {
+
+            _.$nextArrow
+                .removeClass('slick-disabled slick-arrow slick-hidden')
+                .removeAttr('aria-hidden aria-disabled tabindex')
+                .css('display','');
+
+            if ( _.htmlExpr.test( _.options.nextArrow )) {
+                _.$nextArrow.remove();
+            }
+
+        }
+
+
+        if (_.$slides) {
+
+            _.$slides
+                .removeClass('slick-slide slick-active slick-center slick-visible slick-current')
+                .removeAttr('aria-hidden')
+                .removeAttr('data-slick-index')
+                .each(function(){
+                    $(this).attr('style', $(this).data('originalStyling'));
+                });
+
+            _.$slideTrack.children(this.options.slide).detach();
+
+            _.$slideTrack.detach();
+
+            _.$list.detach();
+
+            _.$slider.append(_.$slides);
+        }
+
+        _.cleanUpRows();
+
+        _.$slider.removeClass('slick-slider');
+        _.$slider.removeClass('slick-initialized');
+        _.$slider.removeClass('slick-dotted');
+
+        _.unslicked = true;
+
+        if(!refresh) {
+            _.$slider.trigger('destroy', [_]);
+        }
+
+    };
+
+    Slick.prototype.disableTransition = function(slide) {
+
+        var _ = this,
+            transition = {};
+
+        transition[_.transitionType] = '';
+
+        if (_.options.fade === false) {
+            _.$slideTrack.css(transition);
+        } else {
+            _.$slides.eq(slide).css(transition);
+        }
+
+    };
+
+    Slick.prototype.fadeSlide = function(slideIndex, callback) {
+
+        var _ = this;
+
+        if (_.cssTransitions === false) {
+
+            _.$slides.eq(slideIndex).css({
+                zIndex: _.options.zIndex
+            });
+
+            _.$slides.eq(slideIndex).animate({
+                opacity: 1
+            }, _.options.speed, _.options.easing, callback);
+
+        } else {
+
+            _.applyTransition(slideIndex);
+
+            _.$slides.eq(slideIndex).css({
+                opacity: 1,
+                zIndex: _.options.zIndex
+            });
+
+            if (callback) {
+                setTimeout(function() {
+
+                    _.disableTransition(slideIndex);
+
+                    callback.call();
+                }, _.options.speed);
+            }
+
+        }
+
+    };
+
+    Slick.prototype.fadeSlideOut = function(slideIndex) {
+
+        var _ = this;
+
+        if (_.cssTransitions === false) {
+
+            _.$slides.eq(slideIndex).animate({
+                opacity: 0,
+                zIndex: _.options.zIndex - 2
+            }, _.options.speed, _.options.easing);
+
+        } else {
+
+            _.applyTransition(slideIndex);
+
+            _.$slides.eq(slideIndex).css({
+                opacity: 0,
+                zIndex: _.options.zIndex - 2
+            });
+
+        }
+
+    };
+
+    Slick.prototype.filterSlides = Slick.prototype.slickFilter = function(filter) {
+
+        var _ = this;
+
+        if (filter !== null) {
+
+            _.$slidesCache = _.$slides;
+
+            _.unload();
+
+            _.$slideTrack.children(this.options.slide).detach();
+
+            _.$slidesCache.filter(filter).appendTo(_.$slideTrack);
+
+            _.reinit();
+
+        }
+
+    };
+
+    Slick.prototype.focusHandler = function() {
+
+        var _ = this;
+
+        _.$slider
+            .off('focus.slick blur.slick')
+            .on('focus.slick blur.slick',
+                '*:not(.slick-arrow)', function(event) {
+
+            event.stopImmediatePropagation();
+            var $sf = $(this);
+
+            setTimeout(function() {
+
+                if( _.options.pauseOnFocus ) {
+                    _.focussed = $sf.is(':focus');
+                    _.autoPlay();
+                }
+
+            }, 0);
+
+        });
+    };
+
+    Slick.prototype.getCurrent = Slick.prototype.slickCurrentSlide = function() {
+
+        var _ = this;
+        return _.currentSlide;
+
+    };
+
+    Slick.prototype.getDotCount = function() {
+
+        var _ = this;
+
+        var breakPoint = 0;
+        var counter = 0;
+        var pagerQty = 0;
+
+        if (_.options.infinite === true) {
+            while (breakPoint < _.slideCount) {
+                ++pagerQty;
+                breakPoint = counter + _.options.slidesToScroll;
+                counter += _.options.slidesToScroll <= _.options.slidesToShow ? _.options.slidesToScroll : _.options.slidesToShow;
+            }
+        } else if (_.options.centerMode === true) {
+            pagerQty = _.slideCount;
+        } else if(!_.options.asNavFor) {
+            pagerQty = 1 + Math.ceil((_.slideCount - _.options.slidesToShow) / _.options.slidesToScroll);
+        }else {
+            while (breakPoint < _.slideCount) {
+                ++pagerQty;
+                breakPoint = counter + _.options.slidesToScroll;
+                counter += _.options.slidesToScroll <= _.options.slidesToShow ? _.options.slidesToScroll : _.options.slidesToShow;
+            }
+        }
+
+        return pagerQty - 1;
+
+    };
+
+    Slick.prototype.getLeft = function(slideIndex) {
+
+        var _ = this,
+            targetLeft,
+            verticalHeight,
+            verticalOffset = 0,
+            targetSlide;
+
+        _.slideOffset = 0;
+        verticalHeight = _.$slides.first().outerHeight(true);
+
+        if (_.options.infinite === true) {
+            if (_.slideCount > _.options.slidesToShow) {
+                _.slideOffset = (_.slideWidth * _.options.slidesToShow) * -1;
+                verticalOffset = (verticalHeight * _.options.slidesToShow) * -1;
+            }
+            if (_.slideCount % _.options.slidesToScroll !== 0) {
+                if (slideIndex + _.options.slidesToScroll > _.slideCount && _.slideCount > _.options.slidesToShow) {
+                    if (slideIndex > _.slideCount) {
+                        _.slideOffset = ((_.options.slidesToShow - (slideIndex - _.slideCount)) * _.slideWidth) * -1;
+                        verticalOffset = ((_.options.slidesToShow - (slideIndex - _.slideCount)) * verticalHeight) * -1;
+                    } else {
+                        _.slideOffset = ((_.slideCount % _.options.slidesToScroll) * _.slideWidth) * -1;
+                        verticalOffset = ((_.slideCount % _.options.slidesToScroll) * verticalHeight) * -1;
+                    }
+                }
+            }
+        } else {
+            if (slideIndex + _.options.slidesToShow > _.slideCount) {
+                _.slideOffset = ((slideIndex + _.options.slidesToShow) - _.slideCount) * _.slideWidth;
+                verticalOffset = ((slideIndex + _.options.slidesToShow) - _.slideCount) * verticalHeight;
+            }
+        }
+
+        if (_.slideCount <= _.options.slidesToShow) {
+            _.slideOffset = 0;
+            verticalOffset = 0;
+        }
+
+        if (_.options.centerMode === true && _.options.infinite === true) {
+            _.slideOffset += _.slideWidth * Math.floor(_.options.slidesToShow / 2) - _.slideWidth;
+        } else if (_.options.centerMode === true) {
+            _.slideOffset = 0;
+            _.slideOffset += _.slideWidth * Math.floor(_.options.slidesToShow / 2);
+        }
+
+        if (_.options.vertical === false) {
+            targetLeft = ((slideIndex * _.slideWidth) * -1) + _.slideOffset;
+        } else {
+            targetLeft = ((slideIndex * verticalHeight) * -1) + verticalOffset;
+        }
+
+        if (_.options.variableWidth === true) {
+
+            if (_.slideCount <= _.options.slidesToShow || _.options.infinite === false) {
+                targetSlide = _.$slideTrack.children('.slick-slide').eq(slideIndex);
+            } else {
+                targetSlide = _.$slideTrack.children('.slick-slide').eq(slideIndex + _.options.slidesToShow);
+            }
+
+            if (_.options.rtl === true) {
+                if (targetSlide[0]) {
+                    targetLeft = (_.$slideTrack.width() - targetSlide[0].offsetLeft - targetSlide.width()) * -1;
+                } else {
+                    targetLeft =  0;
+                }
+            } else {
+                targetLeft = targetSlide[0] ? targetSlide[0].offsetLeft * -1 : 0;
+            }
+
+            if (_.options.centerMode === true) {
+                if (_.slideCount <= _.options.slidesToShow || _.options.infinite === false) {
+                    targetSlide = _.$slideTrack.children('.slick-slide').eq(slideIndex);
+                } else {
+                    targetSlide = _.$slideTrack.children('.slick-slide').eq(slideIndex + _.options.slidesToShow + 1);
+                }
+
+                if (_.options.rtl === true) {
+                    if (targetSlide[0]) {
+                        targetLeft = (_.$slideTrack.width() - targetSlide[0].offsetLeft - targetSlide.width()) * -1;
+                    } else {
+                        targetLeft =  0;
+                    }
+                } else {
+                    targetLeft = targetSlide[0] ? targetSlide[0].offsetLeft * -1 : 0;
+                }
+
+                targetLeft += (_.$list.width() - targetSlide.outerWidth()) / 2;
+            }
+        }
+
+        return targetLeft;
+
+    };
+
+    Slick.prototype.getOption = Slick.prototype.slickGetOption = function(option) {
+
+        var _ = this;
+
+        return _.options[option];
+
+    };
+
+    Slick.prototype.getNavigableIndexes = function() {
+
+        var _ = this,
+            breakPoint = 0,
+            counter = 0,
+            indexes = [],
+            max;
+
+        if (_.options.infinite === false) {
+            max = _.slideCount;
+        } else {
+            breakPoint = _.options.slidesToScroll * -1;
+            counter = _.options.slidesToScroll * -1;
+            max = _.slideCount * 2;
+        }
+
+        while (breakPoint < max) {
+            indexes.push(breakPoint);
+            breakPoint = counter + _.options.slidesToScroll;
+            counter += _.options.slidesToScroll <= _.options.slidesToShow ? _.options.slidesToScroll : _.options.slidesToShow;
+        }
+
+        return indexes;
+
+    };
+
+    Slick.prototype.getSlick = function() {
+
+        return this;
+
+    };
+
+    Slick.prototype.getSlideCount = function() {
+
+        var _ = this,
+            slidesTraversed, swipedSlide, centerOffset;
+
+        centerOffset = _.options.centerMode === true ? _.slideWidth * Math.floor(_.options.slidesToShow / 2) : 0;
+
+        if (_.options.swipeToSlide === true) {
+            _.$slideTrack.find('.slick-slide').each(function(index, slide) {
+                if (slide.offsetLeft - centerOffset + ($(slide).outerWidth() / 2) > (_.swipeLeft * -1)) {
+                    swipedSlide = slide;
+                    return false;
+                }
+            });
+
+            slidesTraversed = Math.abs($(swipedSlide).attr('data-slick-index') - _.currentSlide) || 1;
+
+            return slidesTraversed;
+
+        } else {
+            return _.options.slidesToScroll;
+        }
+
+    };
+
+    Slick.prototype.goTo = Slick.prototype.slickGoTo = function(slide, dontAnimate) {
+
+        var _ = this;
+
+        _.changeSlide({
+            data: {
+                message: 'index',
+                index: parseInt(slide)
+            }
+        }, dontAnimate);
+
+    };
+
+    Slick.prototype.init = function(creation) {
+
+        var _ = this;
+
+        if (!$(_.$slider).hasClass('slick-initialized')) {
+
+            $(_.$slider).addClass('slick-initialized');
+
+            _.buildRows();
+            _.buildOut();
+            _.setProps();
+            _.startLoad();
+            _.loadSlider();
+            _.initializeEvents();
+            _.updateArrows();
+            _.updateDots();
+            _.checkResponsive(true);
+            _.focusHandler();
+
+        }
+
+        if (creation) {
+            _.$slider.trigger('init', [_]);
+        }
+
+        if (_.options.accessibility === true) {
+            _.initADA();
+        }
+
+        if ( _.options.autoplay ) {
+
+            _.paused = false;
+            _.autoPlay();
+
+        }
+
+    };
+
+    Slick.prototype.initADA = function() {
+        var _ = this;
+        _.$slides.add(_.$slideTrack.find('.slick-cloned')).attr({
+            'aria-hidden': 'true',
+            'tabindex': '-1'
+        }).find('a, input, button, select').attr({
+            'tabindex': '-1'
+        });
+
+        _.$slideTrack.attr('role', 'listbox');
+
+        _.$slides.not(_.$slideTrack.find('.slick-cloned')).each(function(i) {
+            $(this).attr({
+                'role': 'option',
+                'aria-describedby': 'slick-slide' + _.instanceUid + i + ''
+            });
+        });
+
+        if (_.$dots !== null) {
+            _.$dots.attr('role', 'tablist').find('li').each(function(i) {
+                $(this).attr({
+                    'role': 'presentation',
+                    'aria-selected': 'false',
+                    'aria-controls': 'navigation' + _.instanceUid + i + '',
+                    'id': 'slick-slide' + _.instanceUid + i + ''
+                });
+            })
+                .first().attr('aria-selected', 'true').end()
+                .find('button').attr('role', 'button').end()
+                .closest('div').attr('role', 'toolbar');
+        }
+        _.activateADA();
+
+    };
+
+    Slick.prototype.initArrowEvents = function() {
+
+        var _ = this;
+
+        if (_.options.arrows === true && _.slideCount > _.options.slidesToShow) {
+            _.$prevArrow
+               .off('click.slick')
+               .on('click.slick', {
+                    message: 'previous'
+               }, _.changeSlide);
+            _.$nextArrow
+               .off('click.slick')
+               .on('click.slick', {
+                    message: 'next'
+               }, _.changeSlide);
+        }
+
+    };
+
+    Slick.prototype.initDotEvents = function() {
+
+        var _ = this;
+
+        if (_.options.dots === true && _.slideCount > _.options.slidesToShow) {
+            $('li', _.$dots).on('click.slick', {
+                message: 'index'
+            }, _.changeSlide);
+        }
+
+        if ( _.options.dots === true && _.options.pauseOnDotsHover === true ) {
+
+            $('li', _.$dots)
+                .on('mouseenter.slick', $.proxy(_.interrupt, _, true))
+                .on('mouseleave.slick', $.proxy(_.interrupt, _, false));
+
+        }
+
+    };
+
+    Slick.prototype.initSlideEvents = function() {
+
+        var _ = this;
+
+        if ( _.options.pauseOnHover ) {
+
+            _.$list.on('mouseenter.slick', $.proxy(_.interrupt, _, true));
+            _.$list.on('mouseleave.slick', $.proxy(_.interrupt, _, false));
+
+        }
+
+    };
+
+    Slick.prototype.initializeEvents = function() {
+
+        var _ = this;
+
+        _.initArrowEvents();
+
+        _.initDotEvents();
+        _.initSlideEvents();
+
+        _.$list.on('touchstart.slick mousedown.slick', {
+            action: 'start'
+        }, _.swipeHandler);
+        _.$list.on('touchmove.slick mousemove.slick', {
+            action: 'move'
+        }, _.swipeHandler);
+        _.$list.on('touchend.slick mouseup.slick', {
+            action: 'end'
+        }, _.swipeHandler);
+        _.$list.on('touchcancel.slick mouseleave.slick', {
+            action: 'end'
+        }, _.swipeHandler);
+
+        _.$list.on('click.slick', _.clickHandler);
+
+        $(document).on(_.visibilityChange, $.proxy(_.visibility, _));
+
+        if (_.options.accessibility === true) {
+            _.$list.on('keydown.slick', _.keyHandler);
+        }
+
+        if (_.options.focusOnSelect === true) {
+            $(_.$slideTrack).children().on('click.slick', _.selectHandler);
+        }
+
+        $(window).on('orientationchange.slick.slick-' + _.instanceUid, $.proxy(_.orientationChange, _));
+
+        $(window).on('resize.slick.slick-' + _.instanceUid, $.proxy(_.resize, _));
+
+        $('[draggable!=true]', _.$slideTrack).on('dragstart', _.preventDefault);
+
+        $(window).on('load.slick.slick-' + _.instanceUid, _.setPosition);
+        $(document).on('ready.slick.slick-' + _.instanceUid, _.setPosition);
+
+    };
+
+    Slick.prototype.initUI = function() {
+
+        var _ = this;
+
+        if (_.options.arrows === true && _.slideCount > _.options.slidesToShow) {
+
+            _.$prevArrow.show();
+            _.$nextArrow.show();
+
+        }
+
+        if (_.options.dots === true && _.slideCount > _.options.slidesToShow) {
+
+            _.$dots.show();
+
+        }
+
+    };
+
+    Slick.prototype.keyHandler = function(event) {
+
+        var _ = this;
+         //Dont slide if the cursor is inside the form fields and arrow keys are pressed
+        if(!event.target.tagName.match('TEXTAREA|INPUT|SELECT')) {
+            if (event.keyCode === 37 && _.options.accessibility === true) {
+                _.changeSlide({
+                    data: {
+                        message: _.options.rtl === true ? 'next' :  'previous'
+                    }
+                });
+            } else if (event.keyCode === 39 && _.options.accessibility === true) {
+                _.changeSlide({
+                    data: {
+                        message: _.options.rtl === true ? 'previous' : 'next'
+                    }
+                });
+            }
+        }
+
+    };
+
+    Slick.prototype.lazyLoad = function() {
+
+        var _ = this,
+            loadRange, cloneRange, rangeStart, rangeEnd;
+
+        function loadImages(imagesScope) {
+
+            $('img[data-lazy]', imagesScope).each(function() {
+
+                var image = $(this),
+                    imageSource = $(this).attr('data-lazy'),
+                    imageToLoad = document.createElement('img');
+
+                imageToLoad.onload = function() {
+
+                    image
+                        .animate({ opacity: 0 }, 100, function() {
+                            image
+                                .attr('src', imageSource)
+                                .animate({ opacity: 1 }, 200, function() {
+                                    image
+                                        .removeAttr('data-lazy')
+                                        .removeClass('slick-loading');
+                                });
+                            _.$slider.trigger('lazyLoaded', [_, image, imageSource]);
+                        });
+
+                };
+
+                imageToLoad.onerror = function() {
+
+                    image
+                        .removeAttr( 'data-lazy' )
+                        .removeClass( 'slick-loading' )
+                        .addClass( 'slick-lazyload-error' );
+
+                    _.$slider.trigger('lazyLoadError', [ _, image, imageSource ]);
+
+                };
+
+                imageToLoad.src = imageSource;
+
+            });
+
+        }
+
+        if (_.options.centerMode === true) {
+            if (_.options.infinite === true) {
+                rangeStart = _.currentSlide + (_.options.slidesToShow / 2 + 1);
+                rangeEnd = rangeStart + _.options.slidesToShow + 2;
+            } else {
+                rangeStart = Math.max(0, _.currentSlide - (_.options.slidesToShow / 2 + 1));
+                rangeEnd = 2 + (_.options.slidesToShow / 2 + 1) + _.currentSlide;
+            }
+        } else {
+            rangeStart = _.options.infinite ? _.options.slidesToShow + _.currentSlide : _.currentSlide;
+            rangeEnd = Math.ceil(rangeStart + _.options.slidesToShow);
+            if (_.options.fade === true) {
+                if (rangeStart > 0) rangeStart--;
+                if (rangeEnd <= _.slideCount) rangeEnd++;
+            }
+        }
+
+        loadRange = _.$slider.find('.slick-slide').slice(rangeStart, rangeEnd);
+        loadImages(loadRange);
+
+        if (_.slideCount <= _.options.slidesToShow) {
+            cloneRange = _.$slider.find('.slick-slide');
+            loadImages(cloneRange);
+        } else
+        if (_.currentSlide >= _.slideCount - _.options.slidesToShow) {
+            cloneRange = _.$slider.find('.slick-cloned').slice(0, _.options.slidesToShow);
+            loadImages(cloneRange);
+        } else if (_.currentSlide === 0) {
+            cloneRange = _.$slider.find('.slick-cloned').slice(_.options.slidesToShow * -1);
+            loadImages(cloneRange);
+        }
+
+    };
+
+    Slick.prototype.loadSlider = function() {
+
+        var _ = this;
+
+        _.setPosition();
+
+        _.$slideTrack.css({
+            opacity: 1
+        });
+
+        _.$slider.removeClass('slick-loading');
+
+        _.initUI();
+
+        if (_.options.lazyLoad === 'progressive') {
+            _.progressiveLazyLoad();
+        }
+
+    };
+
+    Slick.prototype.next = Slick.prototype.slickNext = function() {
+
+        var _ = this;
+
+        _.changeSlide({
+            data: {
+                message: 'next'
+            }
+        });
+
+    };
+
+    Slick.prototype.orientationChange = function() {
+
+        var _ = this;
+
+        _.checkResponsive();
+        _.setPosition();
+
+    };
+
+    Slick.prototype.pause = Slick.prototype.slickPause = function() {
+
+        var _ = this;
+
+        _.autoPlayClear();
+        _.paused = true;
+
+    };
+
+    Slick.prototype.play = Slick.prototype.slickPlay = function() {
+
+        var _ = this;
+
+        _.autoPlay();
+        _.options.autoplay = true;
+        _.paused = false;
+        _.focussed = false;
+        _.interrupted = false;
+
+    };
+
+    Slick.prototype.postSlide = function(index) {
+
+        var _ = this;
+
+        if( !_.unslicked ) {
+
+            _.$slider.trigger('afterChange', [_, index]);
+
+            _.animating = false;
+
+            _.setPosition();
+
+            _.swipeLeft = null;
+
+            if ( _.options.autoplay ) {
+                _.autoPlay();
+            }
+
+            if (_.options.accessibility === true) {
+                _.initADA();
+            }
+
+        }
+
+    };
+
+    Slick.prototype.prev = Slick.prototype.slickPrev = function() {
+
+        var _ = this;
+
+        _.changeSlide({
+            data: {
+                message: 'previous'
+            }
+        });
+
+    };
+
+    Slick.prototype.preventDefault = function(event) {
+
+        event.preventDefault();
+
+    };
+
+    Slick.prototype.progressiveLazyLoad = function( tryCount ) {
+
+        tryCount = tryCount || 1;
+
+        var _ = this,
+            $imgsToLoad = $( 'img[data-lazy]', _.$slider ),
+            image,
+            imageSource,
+            imageToLoad;
+
+        if ( $imgsToLoad.length ) {
+
+            image = $imgsToLoad.first();
+            imageSource = image.attr('data-lazy');
+            imageToLoad = document.createElement('img');
+
+            imageToLoad.onload = function() {
+
+                image
+                    .attr( 'src', imageSource )
+                    .removeAttr('data-lazy')
+                    .removeClass('slick-loading');
+
+                if ( _.options.adaptiveHeight === true ) {
+                    _.setPosition();
+                }
+
+                _.$slider.trigger('lazyLoaded', [ _, image, imageSource ]);
+                _.progressiveLazyLoad();
+
+            };
+
+            imageToLoad.onerror = function() {
+
+                if ( tryCount < 3 ) {
+
+                    /**
+                     * try to load the image 3 times,
+                     * leave a slight delay so we don't get
+                     * servers blocking the request.
+                     */
+                    setTimeout( function() {
+                        _.progressiveLazyLoad( tryCount + 1 );
+                    }, 500 );
+
+                } else {
+
+                    image
+                        .removeAttr( 'data-lazy' )
+                        .removeClass( 'slick-loading' )
+                        .addClass( 'slick-lazyload-error' );
+
+                    _.$slider.trigger('lazyLoadError', [ _, image, imageSource ]);
+
+                    _.progressiveLazyLoad();
+
+                }
+
+            };
+
+            imageToLoad.src = imageSource;
+
+        } else {
+
+            _.$slider.trigger('allImagesLoaded', [ _ ]);
+
+        }
+
+    };
+
+    Slick.prototype.refresh = function( initializing ) {
+
+        var _ = this, currentSlide, lastVisibleIndex;
+
+        lastVisibleIndex = _.slideCount - _.options.slidesToShow;
+
+        // in non-infinite sliders, we don't want to go past the
+        // last visible index.
+        if( !_.options.infinite && ( _.currentSlide > lastVisibleIndex )) {
+            _.currentSlide = lastVisibleIndex;
+        }
+
+        // if less slides than to show, go to start.
+        if ( _.slideCount <= _.options.slidesToShow ) {
+            _.currentSlide = 0;
+
+        }
+
+        currentSlide = _.currentSlide;
+
+        _.destroy(true);
+
+        $.extend(_, _.initials, { currentSlide: currentSlide });
+
+        _.init();
+
+        if( !initializing ) {
+
+            _.changeSlide({
+                data: {
+                    message: 'index',
+                    index: currentSlide
+                }
+            }, false);
+
+        }
+
+    };
+
+    Slick.prototype.registerBreakpoints = function() {
+
+        var _ = this, breakpoint, currentBreakpoint, l,
+            responsiveSettings = _.options.responsive || null;
+
+        if ( $.type(responsiveSettings) === 'array' && responsiveSettings.length ) {
+
+            _.respondTo = _.options.respondTo || 'window';
+
+            for ( breakpoint in responsiveSettings ) {
+
+                l = _.breakpoints.length-1;
+                currentBreakpoint = responsiveSettings[breakpoint].breakpoint;
+
+                if (responsiveSettings.hasOwnProperty(breakpoint)) {
+
+                    // loop through the breakpoints and cut out any existing
+                    // ones with the same breakpoint number, we don't want dupes.
+                    while( l >= 0 ) {
+                        if( _.breakpoints[l] && _.breakpoints[l] === currentBreakpoint ) {
+                            _.breakpoints.splice(l,1);
+                        }
+                        l--;
+                    }
+
+                    _.breakpoints.push(currentBreakpoint);
+                    _.breakpointSettings[currentBreakpoint] = responsiveSettings[breakpoint].settings;
+
+                }
+
+            }
+
+            _.breakpoints.sort(function(a, b) {
+                return ( _.options.mobileFirst ) ? a-b : b-a;
+            });
+
+        }
+
+    };
+
+    Slick.prototype.reinit = function() {
+
+        var _ = this;
+
+        _.$slides =
+            _.$slideTrack
+                .children(_.options.slide)
+                .addClass('slick-slide');
+
+        _.slideCount = _.$slides.length;
+
+        if (_.currentSlide >= _.slideCount && _.currentSlide !== 0) {
+            _.currentSlide = _.currentSlide - _.options.slidesToScroll;
+        }
+
+        if (_.slideCount <= _.options.slidesToShow) {
+            _.currentSlide = 0;
+        }
+
+        _.registerBreakpoints();
+
+        _.setProps();
+        _.setupInfinite();
+        _.buildArrows();
+        _.updateArrows();
+        _.initArrowEvents();
+        _.buildDots();
+        _.updateDots();
+        _.initDotEvents();
+        _.cleanUpSlideEvents();
+        _.initSlideEvents();
+
+        _.checkResponsive(false, true);
+
+        if (_.options.focusOnSelect === true) {
+            $(_.$slideTrack).children().on('click.slick', _.selectHandler);
+        }
+
+        _.setSlideClasses(typeof _.currentSlide === 'number' ? _.currentSlide : 0);
+
+        _.setPosition();
+        _.focusHandler();
+
+        _.paused = !_.options.autoplay;
+        _.autoPlay();
+
+        _.$slider.trigger('reInit', [_]);
+
+    };
+
+    Slick.prototype.resize = function() {
+
+        var _ = this;
+
+        if ($(window).width() !== _.windowWidth) {
+            clearTimeout(_.windowDelay);
+            _.windowDelay = window.setTimeout(function() {
+                _.windowWidth = $(window).width();
+                _.checkResponsive();
+                if( !_.unslicked ) { _.setPosition(); }
+            }, 50);
+        }
+    };
+
+    Slick.prototype.removeSlide = Slick.prototype.slickRemove = function(index, removeBefore, removeAll) {
+
+        var _ = this;
+
+        if (typeof(index) === 'boolean') {
+            removeBefore = index;
+            index = removeBefore === true ? 0 : _.slideCount - 1;
+        } else {
+            index = removeBefore === true ? --index : index;
+        }
+
+        if (_.slideCount < 1 || index < 0 || index > _.slideCount - 1) {
+            return false;
+        }
+
+        _.unload();
+
+        if (removeAll === true) {
+            _.$slideTrack.children().remove();
+        } else {
+            _.$slideTrack.children(this.options.slide).eq(index).remove();
+        }
+
+        _.$slides = _.$slideTrack.children(this.options.slide);
+
+        _.$slideTrack.children(this.options.slide).detach();
+
+        _.$slideTrack.append(_.$slides);
+
+        _.$slidesCache = _.$slides;
+
+        _.reinit();
+
+    };
+
+    Slick.prototype.setCSS = function(position) {
+
+        var _ = this,
+            positionProps = {},
+            x, y;
+
+        if (_.options.rtl === true) {
+            position = -position;
+        }
+        x = _.positionProp == 'left' ? Math.ceil(position) + 'px' : '0px';
+        y = _.positionProp == 'top' ? Math.ceil(position) + 'px' : '0px';
+
+        positionProps[_.positionProp] = position;
+
+        if (_.transformsEnabled === false) {
+            _.$slideTrack.css(positionProps);
+        } else {
+            positionProps = {};
+            if (_.cssTransitions === false) {
+                positionProps[_.animType] = 'translate(' + x + ', ' + y + ')';
+                _.$slideTrack.css(positionProps);
+            } else {
+                positionProps[_.animType] = 'translate3d(' + x + ', ' + y + ', 0px)';
+                _.$slideTrack.css(positionProps);
+            }
+        }
+
+    };
+
+    Slick.prototype.setDimensions = function() {
+
+        var _ = this;
+
+        if (_.options.vertical === false) {
+            if (_.options.centerMode === true) {
+                _.$list.css({
+                    padding: ('0px ' + _.options.centerPadding)
+                });
+            }
+        } else {
+            _.$list.height(_.$slides.first().outerHeight(true) * _.options.slidesToShow);
+            if (_.options.centerMode === true) {
+                _.$list.css({
+                    padding: (_.options.centerPadding + ' 0px')
+                });
+            }
+        }
+
+        _.listWidth = _.$list.width();
+        _.listHeight = _.$list.height();
+
+
+        if (_.options.vertical === false && _.options.variableWidth === false) {
+            _.slideWidth = Math.ceil(_.listWidth / _.options.slidesToShow);
+            _.$slideTrack.width(Math.ceil((_.slideWidth * _.$slideTrack.children('.slick-slide').length)));
+
+        } else if (_.options.variableWidth === true) {
+            _.$slideTrack.width(5000 * _.slideCount);
+        } else {
+            _.slideWidth = Math.ceil(_.listWidth);
+            _.$slideTrack.height(Math.ceil((_.$slides.first().outerHeight(true) * _.$slideTrack.children('.slick-slide').length)));
+        }
+
+        var offset = _.$slides.first().outerWidth(true) - _.$slides.first().width();
+        if (_.options.variableWidth === false) _.$slideTrack.children('.slick-slide').width(_.slideWidth - offset);
+
+    };
+
+    Slick.prototype.setFade = function() {
+
+        var _ = this,
+            targetLeft;
+
+        _.$slides.each(function(index, element) {
+            targetLeft = (_.slideWidth * index) * -1;
+            if (_.options.rtl === true) {
+                $(element).css({
+                    position: 'relative',
+                    right: targetLeft,
+                    top: 0,
+                    zIndex: _.options.zIndex - 2,
+                    opacity: 0
+                });
+            } else {
+                $(element).css({
+                    position: 'relative',
+                    left: targetLeft,
+                    top: 0,
+                    zIndex: _.options.zIndex - 2,
+                    opacity: 0
+                });
+            }
+        });
+
+        _.$slides.eq(_.currentSlide).css({
+            zIndex: _.options.zIndex - 1,
+            opacity: 1
+        });
+
+    };
+
+    Slick.prototype.setHeight = function() {
+
+        var _ = this;
+
+        if (_.options.slidesToShow === 1 && _.options.adaptiveHeight === true && _.options.vertical === false) {
+            var targetHeight = _.$slides.eq(_.currentSlide).outerHeight(true);
+            _.$list.css('height', targetHeight);
+        }
+
+    };
+
+    Slick.prototype.setOption =
+    Slick.prototype.slickSetOption = function() {
+
+        /**
+         * accepts arguments in format of:
+         *
+         *  - for changing a single option's value:
+         *     .slick("setOption", option, value, refresh )
+         *
+         *  - for changing a set of responsive options:
+         *     .slick("setOption", 'responsive', [{}, ...], refresh )
+         *
+         *  - for updating multiple values at once (not responsive)
+         *     .slick("setOption", { 'option': value, ... }, refresh )
+         */
+
+        var _ = this, l, item, option, value, refresh = false, type;
+
+        if( $.type( arguments[0] ) === 'object' ) {
+
+            option =  arguments[0];
+            refresh = arguments[1];
+            type = 'multiple';
+
+        } else if ( $.type( arguments[0] ) === 'string' ) {
+
+            option =  arguments[0];
+            value = arguments[1];
+            refresh = arguments[2];
+
+            if ( arguments[0] === 'responsive' && $.type( arguments[1] ) === 'array' ) {
+
+                type = 'responsive';
+
+            } else if ( typeof arguments[1] !== 'undefined' ) {
+
+                type = 'single';
+
+            }
+
+        }
+
+        if ( type === 'single' ) {
+
+            _.options[option] = value;
+
+
+        } else if ( type === 'multiple' ) {
+
+            $.each( option , function( opt, val ) {
+
+                _.options[opt] = val;
+
+            });
+
+
+        } else if ( type === 'responsive' ) {
+
+            for ( item in value ) {
+
+                if( $.type( _.options.responsive ) !== 'array' ) {
+
+                    _.options.responsive = [ value[item] ];
+
+                } else {
+
+                    l = _.options.responsive.length-1;
+
+                    // loop through the responsive object and splice out duplicates.
+                    while( l >= 0 ) {
+
+                        if( _.options.responsive[l].breakpoint === value[item].breakpoint ) {
+
+                            _.options.responsive.splice(l,1);
+
+                        }
+
+                        l--;
+
+                    }
+
+                    _.options.responsive.push( value[item] );
+
+                }
+
+            }
+
+        }
+
+        if ( refresh ) {
+
+            _.unload();
+            _.reinit();
+
+        }
+
+    };
+
+    Slick.prototype.setPosition = function() {
+
+        var _ = this;
+
+        _.setDimensions();
+
+        _.setHeight();
+
+        if (_.options.fade === false) {
+            _.setCSS(_.getLeft(_.currentSlide));
+        } else {
+            _.setFade();
+        }
+
+        _.$slider.trigger('setPosition', [_]);
+
+    };
+
+    Slick.prototype.setProps = function() {
+
+        var _ = this,
+            bodyStyle = document.body.style;
+
+        _.positionProp = _.options.vertical === true ? 'top' : 'left';
+
+        if (_.positionProp === 'top') {
+            _.$slider.addClass('slick-vertical');
+        } else {
+            _.$slider.removeClass('slick-vertical');
+        }
+
+        if (bodyStyle.WebkitTransition !== undefined ||
+            bodyStyle.MozTransition !== undefined ||
+            bodyStyle.msTransition !== undefined) {
+            if (_.options.useCSS === true) {
+                _.cssTransitions = true;
+            }
+        }
+
+        if ( _.options.fade ) {
+            if ( typeof _.options.zIndex === 'number' ) {
+                if( _.options.zIndex < 3 ) {
+                    _.options.zIndex = 3;
+                }
+            } else {
+                _.options.zIndex = _.defaults.zIndex;
+            }
+        }
+
+        if (bodyStyle.OTransform !== undefined) {
+            _.animType = 'OTransform';
+            _.transformType = '-o-transform';
+            _.transitionType = 'OTransition';
+            if (bodyStyle.perspectiveProperty === undefined && bodyStyle.webkitPerspective === undefined) _.animType = false;
+        }
+        if (bodyStyle.MozTransform !== undefined) {
+            _.animType = 'MozTransform';
+            _.transformType = '-moz-transform';
+            _.transitionType = 'MozTransition';
+            if (bodyStyle.perspectiveProperty === undefined && bodyStyle.MozPerspective === undefined) _.animType = false;
+        }
+        if (bodyStyle.webkitTransform !== undefined) {
+            _.animType = 'webkitTransform';
+            _.transformType = '-webkit-transform';
+            _.transitionType = 'webkitTransition';
+            if (bodyStyle.perspectiveProperty === undefined && bodyStyle.webkitPerspective === undefined) _.animType = false;
+        }
+        if (bodyStyle.msTransform !== undefined) {
+            _.animType = 'msTransform';
+            _.transformType = '-ms-transform';
+            _.transitionType = 'msTransition';
+            if (bodyStyle.msTransform === undefined) _.animType = false;
+        }
+        if (bodyStyle.transform !== undefined && _.animType !== false) {
+            _.animType = 'transform';
+            _.transformType = 'transform';
+            _.transitionType = 'transition';
+        }
+        _.transformsEnabled = _.options.useTransform && (_.animType !== null && _.animType !== false);
+    };
+
+
+    Slick.prototype.setSlideClasses = function(index) {
+
+        var _ = this,
+            centerOffset, allSlides, indexOffset, remainder;
+
+        allSlides = _.$slider
+            .find('.slick-slide')
+            .removeClass('slick-active slick-center slick-current')
+            .attr('aria-hidden', 'true');
+
+        _.$slides
+            .eq(index)
+            .addClass('slick-current');
+
+        if (_.options.centerMode === true) {
+
+            centerOffset = Math.floor(_.options.slidesToShow / 2);
+
+            if (_.options.infinite === true) {
+
+                if (index >= centerOffset && index <= (_.slideCount - 1) - centerOffset) {
+
+                    _.$slides
+                        .slice(index - centerOffset, index + centerOffset + 1)
+                        .addClass('slick-active')
+                        .attr('aria-hidden', 'false');
+
+                } else {
+
+                    indexOffset = _.options.slidesToShow + index;
+                    allSlides
+                        .slice(indexOffset - centerOffset + 1, indexOffset + centerOffset + 2)
+                        .addClass('slick-active')
+                        .attr('aria-hidden', 'false');
+
+                }
+
+                if (index === 0) {
+
+                    allSlides
+                        .eq(allSlides.length - 1 - _.options.slidesToShow)
+                        .addClass('slick-center');
+
+                } else if (index === _.slideCount - 1) {
+
+                    allSlides
+                        .eq(_.options.slidesToShow)
+                        .addClass('slick-center');
+
+                }
+
+            }
+
+            _.$slides
+                .eq(index)
+                .addClass('slick-center');
+
+        } else {
+
+            if (index >= 0 && index <= (_.slideCount - _.options.slidesToShow)) {
+
+                _.$slides
+                    .slice(index, index + _.options.slidesToShow)
+                    .addClass('slick-active')
+                    .attr('aria-hidden', 'false');
+
+            } else if (allSlides.length <= _.options.slidesToShow) {
+
+                allSlides
+                    .addClass('slick-active')
+                    .attr('aria-hidden', 'false');
+
+            } else {
+
+                remainder = _.slideCount % _.options.slidesToShow;
+                indexOffset = _.options.infinite === true ? _.options.slidesToShow + index : index;
+
+                if (_.options.slidesToShow == _.options.slidesToScroll && (_.slideCount - index) < _.options.slidesToShow) {
+
+                    allSlides
+                        .slice(indexOffset - (_.options.slidesToShow - remainder), indexOffset + remainder)
+                        .addClass('slick-active')
+                        .attr('aria-hidden', 'false');
+
+                } else {
+
+                    allSlides
+                        .slice(indexOffset, indexOffset + _.options.slidesToShow)
+                        .addClass('slick-active')
+                        .attr('aria-hidden', 'false');
+
+                }
+
+            }
+
+        }
+
+        if (_.options.lazyLoad === 'ondemand') {
+            _.lazyLoad();
+        }
+
+    };
+
+    Slick.prototype.setupInfinite = function() {
+
+        var _ = this,
+            i, slideIndex, infiniteCount;
+
+        if (_.options.fade === true) {
+            _.options.centerMode = false;
+        }
+
+        if (_.options.infinite === true && _.options.fade === false) {
+
+            slideIndex = null;
+
+            if (_.slideCount > _.options.slidesToShow) {
+
+                if (_.options.centerMode === true) {
+                    infiniteCount = _.options.slidesToShow + 1;
+                } else {
+                    infiniteCount = _.options.slidesToShow;
+                }
+
+                for (i = _.slideCount; i > (_.slideCount -
+                        infiniteCount); i -= 1) {
+                    slideIndex = i - 1;
+                    $(_.$slides[slideIndex]).clone(true).attr('id', '')
+                        .attr('data-slick-index', slideIndex - _.slideCount)
+                        .prependTo(_.$slideTrack).addClass('slick-cloned');
+                }
+                for (i = 0; i < infiniteCount; i += 1) {
+                    slideIndex = i;
+                    $(_.$slides[slideIndex]).clone(true).attr('id', '')
+                        .attr('data-slick-index', slideIndex + _.slideCount)
+                        .appendTo(_.$slideTrack).addClass('slick-cloned');
+                }
+                _.$slideTrack.find('.slick-cloned').find('[id]').each(function() {
+                    $(this).attr('id', '');
+                });
+
+            }
+
+        }
+
+    };
+
+    Slick.prototype.interrupt = function( toggle ) {
+
+        var _ = this;
+
+        if( !toggle ) {
+            _.autoPlay();
+        }
+        _.interrupted = toggle;
+
+    };
+
+    Slick.prototype.selectHandler = function(event) {
+
+        var _ = this;
+
+        var targetElement =
+            $(event.target).is('.slick-slide') ?
+                $(event.target) :
+                $(event.target).parents('.slick-slide');
+
+        var index = parseInt(targetElement.attr('data-slick-index'));
+
+        if (!index) index = 0;
+
+        if (_.slideCount <= _.options.slidesToShow) {
+
+            _.setSlideClasses(index);
+            _.asNavFor(index);
+            return;
+
+        }
+
+        _.slideHandler(index);
+
+    };
+
+    Slick.prototype.slideHandler = function(index, sync, dontAnimate) {
+
+        var targetSlide, animSlide, oldSlide, slideLeft, targetLeft = null,
+            _ = this, navTarget;
+
+        sync = sync || false;
+
+        if (_.animating === true && _.options.waitForAnimate === true) {
+            return;
+        }
+
+        if (_.options.fade === true && _.currentSlide === index) {
+            return;
+        }
+
+        if (_.slideCount <= _.options.slidesToShow) {
+            return;
+        }
+
+        if (sync === false) {
+            _.asNavFor(index);
+        }
+
+        targetSlide = index;
+        targetLeft = _.getLeft(targetSlide);
+        slideLeft = _.getLeft(_.currentSlide);
+
+        _.currentLeft = _.swipeLeft === null ? slideLeft : _.swipeLeft;
+
+        if (_.options.infinite === false && _.options.centerMode === false && (index < 0 || index > _.getDotCount() * _.options.slidesToScroll)) {
+            if (_.options.fade === false) {
+                targetSlide = _.currentSlide;
+                if (dontAnimate !== true) {
+                    _.animateSlide(slideLeft, function() {
+                        _.postSlide(targetSlide);
+                    });
+                } else {
+                    _.postSlide(targetSlide);
+                }
+            }
+            return;
+        } else if (_.options.infinite === false && _.options.centerMode === true && (index < 0 || index > (_.slideCount - _.options.slidesToScroll))) {
+            if (_.options.fade === false) {
+                targetSlide = _.currentSlide;
+                if (dontAnimate !== true) {
+                    _.animateSlide(slideLeft, function() {
+                        _.postSlide(targetSlide);
+                    });
+                } else {
+                    _.postSlide(targetSlide);
+                }
+            }
+            return;
+        }
+
+        if ( _.options.autoplay ) {
+            clearInterval(_.autoPlayTimer);
+        }
+
+        if (targetSlide < 0) {
+            if (_.slideCount % _.options.slidesToScroll !== 0) {
+                animSlide = _.slideCount - (_.slideCount % _.options.slidesToScroll);
+            } else {
+                animSlide = _.slideCount + targetSlide;
+            }
+        } else if (targetSlide >= _.slideCount) {
+            if (_.slideCount % _.options.slidesToScroll !== 0) {
+                animSlide = 0;
+            } else {
+                animSlide = targetSlide - _.slideCount;
+            }
+        } else {
+            animSlide = targetSlide;
+        }
+
+        _.animating = true;
+
+        _.$slider.trigger('beforeChange', [_, _.currentSlide, animSlide]);
+
+        oldSlide = _.currentSlide;
+        _.currentSlide = animSlide;
+
+        _.setSlideClasses(_.currentSlide);
+
+        if ( _.options.asNavFor ) {
+
+            navTarget = _.getNavTarget();
+            navTarget = navTarget.slick('getSlick');
+
+            if ( navTarget.slideCount <= navTarget.options.slidesToShow ) {
+                navTarget.setSlideClasses(_.currentSlide);
+            }
+
+        }
+
+        _.updateDots();
+        _.updateArrows();
+
+        if (_.options.fade === true) {
+            if (dontAnimate !== true) {
+
+                _.fadeSlideOut(oldSlide);
+
+                _.fadeSlide(animSlide, function() {
+                    _.postSlide(animSlide);
+                });
+
+            } else {
+                _.postSlide(animSlide);
+            }
+            _.animateHeight();
+            return;
+        }
+
+        if (dontAnimate !== true) {
+            _.animateSlide(targetLeft, function() {
+                _.postSlide(animSlide);
+            });
+        } else {
+            _.postSlide(animSlide);
+        }
+
+    };
+
+    Slick.prototype.startLoad = function() {
+
+        var _ = this;
+
+        if (_.options.arrows === true && _.slideCount > _.options.slidesToShow) {
+
+            _.$prevArrow.hide();
+            _.$nextArrow.hide();
+
+        }
+
+        if (_.options.dots === true && _.slideCount > _.options.slidesToShow) {
+
+            _.$dots.hide();
+
+        }
+
+        _.$slider.addClass('slick-loading');
+
+    };
+
+    Slick.prototype.swipeDirection = function() {
+
+        var xDist, yDist, r, swipeAngle, _ = this;
+
+        xDist = _.touchObject.startX - _.touchObject.curX;
+        yDist = _.touchObject.startY - _.touchObject.curY;
+        r = Math.atan2(yDist, xDist);
+
+        swipeAngle = Math.round(r * 180 / Math.PI);
+        if (swipeAngle < 0) {
+            swipeAngle = 360 - Math.abs(swipeAngle);
+        }
+
+        if ((swipeAngle <= 45) && (swipeAngle >= 0)) {
+            return (_.options.rtl === false ? 'left' : 'right');
+        }
+        if ((swipeAngle <= 360) && (swipeAngle >= 315)) {
+            return (_.options.rtl === false ? 'left' : 'right');
+        }
+        if ((swipeAngle >= 135) && (swipeAngle <= 225)) {
+            return (_.options.rtl === false ? 'right' : 'left');
+        }
+        if (_.options.verticalSwiping === true) {
+            if ((swipeAngle >= 35) && (swipeAngle <= 135)) {
+                return 'down';
+            } else {
+                return 'up';
+            }
+        }
+
+        return 'vertical';
+
+    };
+
+    Slick.prototype.swipeEnd = function(event) {
+
+        var _ = this,
+            slideCount,
+            direction;
+
+        _.dragging = false;
+        _.interrupted = false;
+        _.shouldClick = ( _.touchObject.swipeLength > 10 ) ? false : true;
+
+        if ( _.touchObject.curX === undefined ) {
+            return false;
+        }
+
+        if ( _.touchObject.edgeHit === true ) {
+            _.$slider.trigger('edge', [_, _.swipeDirection() ]);
+        }
+
+        if ( _.touchObject.swipeLength >= _.touchObject.minSwipe ) {
+
+            direction = _.swipeDirection();
+
+            switch ( direction ) {
+
+                case 'left':
+                case 'down':
+
+                    slideCount =
+                        _.options.swipeToSlide ?
+                            _.checkNavigable( _.currentSlide + _.getSlideCount() ) :
+                            _.currentSlide + _.getSlideCount();
+
+                    _.currentDirection = 0;
+
+                    break;
+
+                case 'right':
+                case 'up':
+
+                    slideCount =
+                        _.options.swipeToSlide ?
+                            _.checkNavigable( _.currentSlide - _.getSlideCount() ) :
+                            _.currentSlide - _.getSlideCount();
+
+                    _.currentDirection = 1;
+
+                    break;
+
+                default:
+
+
+            }
+
+            if( direction != 'vertical' ) {
+
+                _.slideHandler( slideCount );
+                _.touchObject = {};
+                _.$slider.trigger('swipe', [_, direction ]);
+
+            }
+
+        } else {
+
+            if ( _.touchObject.startX !== _.touchObject.curX ) {
+
+                _.slideHandler( _.currentSlide );
+                _.touchObject = {};
+
+            }
+
+        }
+
+    };
+
+    Slick.prototype.swipeHandler = function(event) {
+
+        var _ = this;
+
+        if ((_.options.swipe === false) || ('ontouchend' in document && _.options.swipe === false)) {
+            return;
+        } else if (_.options.draggable === false && event.type.indexOf('mouse') !== -1) {
+            return;
+        }
+
+        _.touchObject.fingerCount = event.originalEvent && event.originalEvent.touches !== undefined ?
+            event.originalEvent.touches.length : 1;
+
+        _.touchObject.minSwipe = _.listWidth / _.options
+            .touchThreshold;
+
+        if (_.options.verticalSwiping === true) {
+            _.touchObject.minSwipe = _.listHeight / _.options
+                .touchThreshold;
+        }
+
+        switch (event.data.action) {
+
+            case 'start':
+                _.swipeStart(event);
+                break;
+
+            case 'move':
+                _.swipeMove(event);
+                break;
+
+            case 'end':
+                _.swipeEnd(event);
+                break;
+
+        }
+
+    };
+
+    Slick.prototype.swipeMove = function(event) {
+
+        var _ = this,
+            edgeWasHit = false,
+            curLeft, swipeDirection, swipeLength, positionOffset, touches;
+
+        touches = event.originalEvent !== undefined ? event.originalEvent.touches : null;
+
+        if (!_.dragging || touches && touches.length !== 1) {
+            return false;
+        }
+
+        curLeft = _.getLeft(_.currentSlide);
+
+        _.touchObject.curX = touches !== undefined ? touches[0].pageX : event.clientX;
+        _.touchObject.curY = touches !== undefined ? touches[0].pageY : event.clientY;
+
+        _.touchObject.swipeLength = Math.round(Math.sqrt(
+            Math.pow(_.touchObject.curX - _.touchObject.startX, 2)));
+
+        if (_.options.verticalSwiping === true) {
+            _.touchObject.swipeLength = Math.round(Math.sqrt(
+                Math.pow(_.touchObject.curY - _.touchObject.startY, 2)));
+        }
+
+        swipeDirection = _.swipeDirection();
+
+        if (swipeDirection === 'vertical') {
+            return;
+        }
+
+        if (event.originalEvent !== undefined && _.touchObject.swipeLength > 4) {
+            event.preventDefault();
+        }
+
+        positionOffset = (_.options.rtl === false ? 1 : -1) * (_.touchObject.curX > _.touchObject.startX ? 1 : -1);
+        if (_.options.verticalSwiping === true) {
+            positionOffset = _.touchObject.curY > _.touchObject.startY ? 1 : -1;
+        }
+
+
+        swipeLength = _.touchObject.swipeLength;
+
+        _.touchObject.edgeHit = false;
+
+        if (_.options.infinite === false) {
+            if ((_.currentSlide === 0 && swipeDirection === 'right') || (_.currentSlide >= _.getDotCount() && swipeDirection === 'left')) {
+                swipeLength = _.touchObject.swipeLength * _.options.edgeFriction;
+                _.touchObject.edgeHit = true;
+            }
+        }
+
+        if (_.options.vertical === false) {
+            _.swipeLeft = curLeft + swipeLength * positionOffset;
+        } else {
+            _.swipeLeft = curLeft + (swipeLength * (_.$list.height() / _.listWidth)) * positionOffset;
+        }
+        if (_.options.verticalSwiping === true) {
+            _.swipeLeft = curLeft + swipeLength * positionOffset;
+        }
+
+        if (_.options.fade === true || _.options.touchMove === false) {
+            return false;
+        }
+
+        if (_.animating === true) {
+            _.swipeLeft = null;
+            return false;
+        }
+
+        _.setCSS(_.swipeLeft);
+
+    };
+
+    Slick.prototype.swipeStart = function(event) {
+
+        var _ = this,
+            touches;
+
+        _.interrupted = true;
+
+        if (_.touchObject.fingerCount !== 1 || _.slideCount <= _.options.slidesToShow) {
+            _.touchObject = {};
+            return false;
+        }
+
+        if (event.originalEvent !== undefined && event.originalEvent.touches !== undefined) {
+            touches = event.originalEvent.touches[0];
+        }
+
+        _.touchObject.startX = _.touchObject.curX = touches !== undefined ? touches.pageX : event.clientX;
+        _.touchObject.startY = _.touchObject.curY = touches !== undefined ? touches.pageY : event.clientY;
+
+        _.dragging = true;
+
+    };
+
+    Slick.prototype.unfilterSlides = Slick.prototype.slickUnfilter = function() {
+
+        var _ = this;
+
+        if (_.$slidesCache !== null) {
+
+            _.unload();
+
+            _.$slideTrack.children(this.options.slide).detach();
+
+            _.$slidesCache.appendTo(_.$slideTrack);
+
+            _.reinit();
+
+        }
+
+    };
+
+    Slick.prototype.unload = function() {
+
+        var _ = this;
+
+        $('.slick-cloned', _.$slider).remove();
+
+        if (_.$dots) {
+            _.$dots.remove();
+        }
+
+        if (_.$prevArrow && _.htmlExpr.test(_.options.prevArrow)) {
+            _.$prevArrow.remove();
+        }
+
+        if (_.$nextArrow && _.htmlExpr.test(_.options.nextArrow)) {
+            _.$nextArrow.remove();
+        }
+
+        _.$slides
+            .removeClass('slick-slide slick-active slick-visible slick-current')
+            .attr('aria-hidden', 'true')
+            .css('width', '');
+
+    };
+
+    Slick.prototype.unslick = function(fromBreakpoint) {
+
+        var _ = this;
+        _.$slider.trigger('unslick', [_, fromBreakpoint]);
+        _.destroy();
+
+    };
+
+    Slick.prototype.updateArrows = function() {
+
+        var _ = this,
+            centerOffset;
+
+        centerOffset = Math.floor(_.options.slidesToShow / 2);
+
+        if ( _.options.arrows === true &&
+            _.slideCount > _.options.slidesToShow &&
+            !_.options.infinite ) {
+
+            _.$prevArrow.removeClass('slick-disabled').attr('aria-disabled', 'false');
+            _.$nextArrow.removeClass('slick-disabled').attr('aria-disabled', 'false');
+
+            if (_.currentSlide === 0) {
+
+                _.$prevArrow.addClass('slick-disabled').attr('aria-disabled', 'true');
+                _.$nextArrow.removeClass('slick-disabled').attr('aria-disabled', 'false');
+
+            } else if (_.currentSlide >= _.slideCount - _.options.slidesToShow && _.options.centerMode === false) {
+
+                _.$nextArrow.addClass('slick-disabled').attr('aria-disabled', 'true');
+                _.$prevArrow.removeClass('slick-disabled').attr('aria-disabled', 'false');
+
+            } else if (_.currentSlide >= _.slideCount - 1 && _.options.centerMode === true) {
+
+                _.$nextArrow.addClass('slick-disabled').attr('aria-disabled', 'true');
+                _.$prevArrow.removeClass('slick-disabled').attr('aria-disabled', 'false');
+
+            }
+
+        }
+
+    };
+
+    Slick.prototype.updateDots = function() {
+
+        var _ = this;
+
+        if (_.$dots !== null) {
+
+            _.$dots
+                .find('li')
+                .removeClass('slick-active')
+                .attr('aria-hidden', 'true');
+
+            _.$dots
+                .find('li')
+                .eq(Math.floor(_.currentSlide / _.options.slidesToScroll))
+                .addClass('slick-active')
+                .attr('aria-hidden', 'false');
+
+        }
+
+    };
+
+    Slick.prototype.visibility = function() {
+
+        var _ = this;
+
+        if ( _.options.autoplay ) {
+
+            if ( document[_.hidden] ) {
+
+                _.interrupted = true;
+
+            } else {
+
+                _.interrupted = false;
+
+            }
+
+        }
+
+    };
+
+    $.fn.slick = function() {
+        var _ = this,
+            opt = arguments[0],
+            args = Array.prototype.slice.call(arguments, 1),
+            l = _.length,
+            i,
+            ret;
+        for (i = 0; i < l; i++) {
+            if (typeof opt == 'object' || typeof opt == 'undefined')
+                _[i].slick = new Slick(_[i], opt);
+            else
+                ret = _[i].slick[opt].apply(_[i].slick, args);
+            if (typeof ret != 'undefined') return ret;
+        }
+        return _;
+    };
+
+}));

+ 98 - 0
bower_components/slick/slick.less

@@ -0,0 +1,98 @@
+/* Slider */
+
+.slick-slider {
+    position: relative;
+    display: block;
+    box-sizing: border-box;
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    -ms-touch-action: pan-y;
+    touch-action: pan-y;
+    -webkit-tap-highlight-color: transparent;
+}
+.slick-list {
+    position: relative;
+    overflow: hidden;
+    display: block;
+    margin: 0;
+    padding: 0;
+
+    &:focus {
+        outline: none;
+    }
+
+    &.dragging {
+        cursor: pointer;
+        cursor: hand;
+    }
+}
+.slick-slider .slick-track,
+.slick-slider .slick-list {
+    -webkit-transform: translate3d(0, 0, 0);
+    -moz-transform: translate3d(0, 0, 0);
+    -ms-transform: translate3d(0, 0, 0);
+    -o-transform: translate3d(0, 0, 0);
+    transform: translate3d(0, 0, 0);
+}
+
+.slick-track {
+    position: relative;
+    left: 0;
+    top: 0;
+    display: block;
+
+    &:before,
+    &:after {
+        content: "";
+        display: table;
+    }
+
+    &:after {
+        clear: both;
+    }
+
+    .slick-loading & {
+        visibility: hidden;
+    }
+}
+.slick-slide {
+    float: left;
+    height: 100%;
+    min-height: 1px;
+    [dir="rtl"] & {
+        float: right;
+    }
+    img {
+        display: block;
+    }
+    &.slick-loading img {
+        display: none;
+    }
+
+    display: none;
+
+    &.dragging img {
+        pointer-events: none;
+    }
+
+    .slick-initialized & {
+        display: block;
+    }
+
+    .slick-loading & {
+        visibility: hidden;
+    }
+
+    .slick-vertical & {
+        display: block;
+        height: auto;
+        border: 1px solid transparent;
+    }
+}
+.slick-arrow.slick-hidden {
+    display: none;
+}

Файловите разлики са ограничени, защото са твърде много
+ 16 - 0
bower_components/slick/slick.min.js


+ 98 - 0
bower_components/slick/slick.scss

@@ -0,0 +1,98 @@
+/* Slider */
+
+.slick-slider {
+    position: relative;
+    display: block;
+    box-sizing: border-box;
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    -ms-touch-action: pan-y;
+    touch-action: pan-y;
+    -webkit-tap-highlight-color: transparent;
+}
+.slick-list {
+    position: relative;
+    overflow: hidden;
+    display: block;
+    margin: 0;
+    padding: 0;
+
+    &:focus {
+        outline: none;
+    }
+
+    &.dragging {
+        cursor: pointer;
+        cursor: hand;
+    }
+}
+.slick-slider .slick-track,
+.slick-slider .slick-list {
+    -webkit-transform: translate3d(0, 0, 0);
+    -moz-transform: translate3d(0, 0, 0);
+    -ms-transform: translate3d(0, 0, 0);
+    -o-transform: translate3d(0, 0, 0);
+    transform: translate3d(0, 0, 0);
+}
+
+.slick-track {
+    position: relative;
+    left: 0;
+    top: 0;
+    display: block;
+
+    &:before,
+    &:after {
+        content: "";
+        display: table;
+    }
+
+    &:after {
+        clear: both;
+    }
+
+    .slick-loading & {
+        visibility: hidden;
+    }
+}
+.slick-slide {
+    float: left;
+    height: 100%;
+    min-height: 1px;
+    [dir="rtl"] & {
+        float: right;
+    }
+    img {
+        display: block;
+    }
+    &.slick-loading img {
+        display: none;
+    }
+
+    display: none;
+
+    &.dragging img {
+        pointer-events: none;
+    }
+
+    .slick-initialized & {
+        display: block;
+    }
+
+    .slick-loading & {
+        visibility: hidden;
+    }
+
+    .slick-vertical & {
+        display: block;
+        height: auto;
+        border: 1px solid transparent;
+    }
+}
+.slick-arrow.slick-hidden {
+    display: none;
+}

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
bower_components/summernote/dist/summernote.css


+ 7300 - 0
bower_components/summernote/dist/summernote.js

@@ -0,0 +1,7300 @@
+/**
+ * Super simple wysiwyg editor on Bootstrap v0.6.16
+ * http://summernote.org/
+ *
+ * summernote.js
+ * Copyright 2013-2015 Alan Hong. and other contributors
+ * summernote may be freely distributed under the MIT license./
+ *
+ * Date: 2015-08-03T16:41Z
+ */
+(function (factory) {
+  /* global define */
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else {
+    // Browser globals: jQuery
+    factory(window.jQuery);
+  }
+}(function ($) {
+  
+
+
+  if (!Array.prototype.reduce) {
+    /**
+     * Array.prototype.reduce polyfill
+     *
+     * @param {Function} callback
+     * @param {Value} [initialValue]
+     * @return {Value}
+     *
+     * @see http://goo.gl/WNriQD
+     */
+    Array.prototype.reduce = function (callback) {
+      var t = Object(this), len = t.length >>> 0, k = 0, value;
+      if (arguments.length === 2) {
+        value = arguments[1];
+      } else {
+        while (k < len && !(k in t)) {
+          k++;
+        }
+        if (k >= len) {
+          throw new TypeError('Reduce of empty array with no initial value');
+        }
+        value = t[k++];
+      }
+      for (; k < len; k++) {
+        if (k in t) {
+          value = callback(value, t[k], k, t);
+        }
+      }
+      return value;
+    };
+  }
+
+  if ('function' !== typeof Array.prototype.filter) {
+    /**
+     * Array.prototype.filter polyfill
+     *
+     * @param {Function} func
+     * @return {Array}
+     *
+     * @see http://goo.gl/T1KFnq
+     */
+    Array.prototype.filter = function (func) {
+      var t = Object(this), len = t.length >>> 0;
+
+      var res = [];
+      var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
+      for (var i = 0; i < len; i++) {
+        if (i in t) {
+          var val = t[i];
+          if (func.call(thisArg, val, i, t)) {
+            res.push(val);
+          }
+        }
+      }
+  
+      return res;
+    };
+  }
+
+  if (!Array.prototype.map) {
+    /**
+     * Array.prototype.map polyfill
+     *
+     * @param {Function} callback
+     * @return {Array}
+     *
+     * @see https://goo.gl/SMWaMK
+     */
+    Array.prototype.map = function (callback, thisArg) {
+      var T, A, k;
+      if (this === null) {
+        throw new TypeError(' this is null or not defined');
+      }
+
+      var O = Object(this);
+      var len = O.length >>> 0;
+      if (typeof callback !== 'function') {
+        throw new TypeError(callback + ' is not a function');
+      }
+  
+      if (arguments.length > 1) {
+        T = thisArg;
+      }
+  
+      A = new Array(len);
+      k = 0;
+  
+      while (k < len) {
+        var kValue, mappedValue;
+        if (k in O) {
+          kValue = O[k];
+          mappedValue = callback.call(T, kValue, k, O);
+          A[k] = mappedValue;
+        }
+        k++;
+      }
+      return A;
+    };
+  }
+
+  var isSupportAmd = typeof define === 'function' && define.amd;
+
+  /**
+   * returns whether font is installed or not.
+   *
+   * @param {String} fontName
+   * @return {Boolean}
+   */
+  var isFontInstalled = function (fontName) {
+    var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
+    var $tester = $('<div>').css({
+      position: 'absolute',
+      left: '-9999px',
+      top: '-9999px',
+      fontSize: '200px'
+    }).text('mmmmmmmmmwwwwwww').appendTo(document.body);
+
+    var originalWidth = $tester.css('fontFamily', testFontName).width();
+    var width = $tester.css('fontFamily', fontName + ',' + testFontName).width();
+
+    $tester.remove();
+
+    return originalWidth !== width;
+  };
+
+  var userAgent = navigator.userAgent;
+  var isMSIE = /MSIE|Trident/i.test(userAgent);
+  var browserVersion;
+  if (isMSIE) {
+    var matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
+    if (matches) {
+      browserVersion = parseFloat(matches[1]);
+    }
+    matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent);
+    if (matches) {
+      browserVersion = parseFloat(matches[1]);
+    }
+  }
+
+  /**
+   * @class core.agent
+   *
+   * Object which check platform and agent
+   *
+   * @singleton
+   * @alternateClassName agent
+   */
+  var agent = {
+    /** @property {Boolean} [isMac=false] true if this agent is Mac  */
+    isMac: navigator.appVersion.indexOf('Mac') > -1,
+    /** @property {Boolean} [isMSIE=false] true if this agent is a Internet Explorer  */
+    isMSIE: isMSIE,
+    /** @property {Boolean} [isFF=false] true if this agent is a Firefox  */
+    isFF: /firefox/i.test(userAgent),
+    isWebkit: /webkit/i.test(userAgent),
+    /** @property {Boolean} [isSafari=false] true if this agent is a Safari  */
+    isSafari: /safari/i.test(userAgent),
+    /** @property {Float} browserVersion current browser version  */
+    browserVersion: browserVersion,
+    /** @property {String} jqueryVersion current jQuery version string  */
+    jqueryVersion: parseFloat($.fn.jquery),
+    isSupportAmd: isSupportAmd,
+    hasCodeMirror: isSupportAmd ? require.specified('CodeMirror') : !!window.CodeMirror,
+    isFontInstalled: isFontInstalled,
+    isW3CRangeSupport: !!document.createRange
+  };
+
+  /**
+   * @class core.func
+   *
+   * func utils (for high-order func's arg)
+   *
+   * @singleton
+   * @alternateClassName func
+   */
+  var func = (function () {
+    var eq = function (itemA) {
+      return function (itemB) {
+        return itemA === itemB;
+      };
+    };
+
+    var eq2 = function (itemA, itemB) {
+      return itemA === itemB;
+    };
+
+    var peq2 = function (propName) {
+      return function (itemA, itemB) {
+        return itemA[propName] === itemB[propName];
+      };
+    };
+
+    var ok = function () {
+      return true;
+    };
+
+    var fail = function () {
+      return false;
+    };
+
+    var not = function (f) {
+      return function () {
+        return !f.apply(f, arguments);
+      };
+    };
+
+    var and = function (fA, fB) {
+      return function (item) {
+        return fA(item) && fB(item);
+      };
+    };
+
+    var self = function (a) {
+      return a;
+    };
+
+    var idCounter = 0;
+
+    /**
+     * generate a globally-unique id
+     *
+     * @param {String} [prefix]
+     */
+    var uniqueId = function (prefix) {
+      var id = ++idCounter + '';
+      return prefix ? prefix + id : id;
+    };
+
+    /**
+     * returns bnd (bounds) from rect
+     *
+     * - IE Compatability Issue: http://goo.gl/sRLOAo
+     * - Scroll Issue: http://goo.gl/sNjUc
+     *
+     * @param {Rect} rect
+     * @return {Object} bounds
+     * @return {Number} bounds.top
+     * @return {Number} bounds.left
+     * @return {Number} bounds.width
+     * @return {Number} bounds.height
+     */
+    var rect2bnd = function (rect) {
+      var $document = $(document);
+      return {
+        top: rect.top + $document.scrollTop(),
+        left: rect.left + $document.scrollLeft(),
+        width: rect.right - rect.left,
+        height: rect.bottom - rect.top
+      };
+    };
+
+    /**
+     * returns a copy of the object where the keys have become the values and the values the keys.
+     * @param {Object} obj
+     * @return {Object}
+     */
+    var invertObject = function (obj) {
+      var inverted = {};
+      for (var key in obj) {
+        if (obj.hasOwnProperty(key)) {
+          inverted[obj[key]] = key;
+        }
+      }
+      return inverted;
+    };
+
+    /**
+     * @param {String} namespace
+     * @param {String} [prefix]
+     * @return {String}
+     */
+    var namespaceToCamel = function (namespace, prefix) {
+      prefix = prefix || '';
+      return prefix + namespace.split('.').map(function (name) {
+        return name.substring(0, 1).toUpperCase() + name.substring(1);
+      }).join('');
+    };
+
+    return {
+      eq: eq,
+      eq2: eq2,
+      peq2: peq2,
+      ok: ok,
+      fail: fail,
+      self: self,
+      not: not,
+      and: and,
+      uniqueId: uniqueId,
+      rect2bnd: rect2bnd,
+      invertObject: invertObject,
+      namespaceToCamel: namespaceToCamel
+    };
+  })();
+
+  /**
+   * @class core.list
+   *
+   * list utils
+   *
+   * @singleton
+   * @alternateClassName list
+   */
+  var list = (function () {
+    /**
+     * returns the first item of an array.
+     *
+     * @param {Array} array
+     */
+    var head = function (array) {
+      return array[0];
+    };
+
+    /**
+     * returns the last item of an array.
+     *
+     * @param {Array} array
+     */
+    var last = function (array) {
+      return array[array.length - 1];
+    };
+
+    /**
+     * returns everything but the last entry of the array.
+     *
+     * @param {Array} array
+     */
+    var initial = function (array) {
+      return array.slice(0, array.length - 1);
+    };
+
+    /**
+     * returns the rest of the items in an array.
+     *
+     * @param {Array} array
+     */
+    var tail = function (array) {
+      return array.slice(1);
+    };
+
+    /**
+     * returns item of array
+     */
+    var find = function (array, pred) {
+      for (var idx = 0, len = array.length; idx < len; idx ++) {
+        var item = array[idx];
+        if (pred(item)) {
+          return item;
+        }
+      }
+    };
+
+    /**
+     * returns true if all of the values in the array pass the predicate truth test.
+     */
+    var all = function (array, pred) {
+      for (var idx = 0, len = array.length; idx < len; idx ++) {
+        if (!pred(array[idx])) {
+          return false;
+        }
+      }
+      return true;
+    };
+
+    /**
+     * returns index of item
+     */
+    var indexOf = function (array, item) {
+      return $.inArray(item, array);
+    };
+
+    /**
+     * returns true if the value is present in the list.
+     */
+    var contains = function (array, item) {
+      return indexOf(array, item) !== -1;
+    };
+
+    /**
+     * get sum from a list
+     *
+     * @param {Array} array - array
+     * @param {Function} fn - iterator
+     */
+    var sum = function (array, fn) {
+      fn = fn || func.self;
+      return array.reduce(function (memo, v) {
+        return memo + fn(v);
+      }, 0);
+    };
+  
+    /**
+     * returns a copy of the collection with array type.
+     * @param {Collection} collection - collection eg) node.childNodes, ...
+     */
+    var from = function (collection) {
+      var result = [], idx = -1, length = collection.length;
+      while (++idx < length) {
+        result[idx] = collection[idx];
+      }
+      return result;
+    };
+  
+    /**
+     * cluster elements by predicate function.
+     *
+     * @param {Array} array - array
+     * @param {Function} fn - predicate function for cluster rule
+     * @param {Array[]}
+     */
+    var clusterBy = function (array, fn) {
+      if (!array.length) { return []; }
+      var aTail = tail(array);
+      return aTail.reduce(function (memo, v) {
+        var aLast = last(memo);
+        if (fn(last(aLast), v)) {
+          aLast[aLast.length] = v;
+        } else {
+          memo[memo.length] = [v];
+        }
+        return memo;
+      }, [[head(array)]]);
+    };
+  
+    /**
+     * returns a copy of the array with all falsy values removed
+     *
+     * @param {Array} array - array
+     * @param {Function} fn - predicate function for cluster rule
+     */
+    var compact = function (array) {
+      var aResult = [];
+      for (var idx = 0, len = array.length; idx < len; idx ++) {
+        if (array[idx]) { aResult.push(array[idx]); }
+      }
+      return aResult;
+    };
+
+    /**
+     * produces a duplicate-free version of the array
+     *
+     * @param {Array} array
+     */
+    var unique = function (array) {
+      var results = [];
+
+      for (var idx = 0, len = array.length; idx < len; idx ++) {
+        if (!contains(results, array[idx])) {
+          results.push(array[idx]);
+        }
+      }
+
+      return results;
+    };
+
+    /**
+     * returns next item.
+     * @param {Array} array
+     */
+    var next = function (array, item) {
+      var idx = indexOf(array, item);
+      if (idx === -1) { return null; }
+
+      return array[idx + 1];
+    };
+
+    /**
+     * returns prev item.
+     * @param {Array} array
+     */
+    var prev = function (array, item) {
+      var idx = indexOf(array, item);
+      if (idx === -1) { return null; }
+
+      return array[idx - 1];
+    };
+  
+    return { head: head, last: last, initial: initial, tail: tail,
+             prev: prev, next: next, find: find, contains: contains,
+             all: all, sum: sum, from: from,
+             clusterBy: clusterBy, compact: compact, unique: unique };
+  })();
+
+
+  var NBSP_CHAR = String.fromCharCode(160);
+  var ZERO_WIDTH_NBSP_CHAR = '\ufeff';
+
+  /**
+   * @class core.dom
+   *
+   * Dom functions
+   *
+   * @singleton
+   * @alternateClassName dom
+   */
+  var dom = (function () {
+    /**
+     * @method isEditable
+     *
+     * returns whether node is `note-editable` or not.
+     *
+     * @param {Node} node
+     * @return {Boolean}
+     */
+    var isEditable = function (node) {
+      return node && $(node).hasClass('note-editable');
+    };
+
+    /**
+     * @method isControlSizing
+     *
+     * returns whether node is `note-control-sizing` or not.
+     *
+     * @param {Node} node
+     * @return {Boolean}
+     */
+    var isControlSizing = function (node) {
+      return node && $(node).hasClass('note-control-sizing');
+    };
+
+    /**
+     * @method  buildLayoutInfo
+     *
+     * build layoutInfo from $editor(.note-editor)
+     *
+     * @param {jQuery} $editor
+     * @return {Object}
+     * @return {Function} return.editor
+     * @return {Node} return.dropzone
+     * @return {Node} return.toolbar
+     * @return {Node} return.editable
+     * @return {Node} return.codable
+     * @return {Node} return.popover
+     * @return {Node} return.handle
+     * @return {Node} return.dialog
+     */
+    var buildLayoutInfo = function ($editor) {
+      var makeFinder;
+
+      // air mode
+      if ($editor.hasClass('note-air-editor')) {
+        var id = list.last($editor.attr('id').split('-'));
+        makeFinder = function (sIdPrefix) {
+          return function () { return $(sIdPrefix + id); };
+        };
+
+        return {
+          editor: function () { return $editor; },
+          holder : function () { return $editor.data('holder'); },
+          editable: function () { return $editor; },
+          popover: makeFinder('#note-popover-'),
+          handle: makeFinder('#note-handle-'),
+          dialog: makeFinder('#note-dialog-')
+        };
+
+        // frame mode
+      } else {
+        makeFinder = function (className, $base) {
+          $base = $base || $editor;
+          return function () { return $base.find(className); };
+        };
+
+        var options = $editor.data('options');
+        var $dialogHolder = (options && options.dialogsInBody) ? $(document.body) : null;
+
+        return {
+          editor: function () { return $editor; },
+          holder : function () { return $editor.data('holder'); },
+          dropzone: makeFinder('.note-dropzone'),
+          toolbar: makeFinder('.note-toolbar'),
+          editable: makeFinder('.note-editable'),
+          codable: makeFinder('.note-codable'),
+          statusbar: makeFinder('.note-statusbar'),
+          popover: makeFinder('.note-popover'),
+          handle: makeFinder('.note-handle'),
+          dialog: makeFinder('.note-dialog', $dialogHolder)
+        };
+      }
+    };
+
+    /**
+     * returns makeLayoutInfo from editor's descendant node.
+     *
+     * @private
+     * @param {Node} descendant
+     * @return {Object}
+     */
+    var makeLayoutInfo = function (descendant) {
+      var $target = $(descendant).closest('.note-editor, .note-air-editor, .note-air-layout');
+
+      if (!$target.length) {
+        return null;
+      }
+
+      var $editor;
+      if ($target.is('.note-editor, .note-air-editor')) {
+        $editor = $target;
+      } else {
+        $editor = $('#note-editor-' + list.last($target.attr('id').split('-')));
+      }
+
+      return buildLayoutInfo($editor);
+    };
+
+    /**
+     * @method makePredByNodeName
+     *
+     * returns predicate which judge whether nodeName is same
+     *
+     * @param {String} nodeName
+     * @return {Function}
+     */
+    var makePredByNodeName = function (nodeName) {
+      nodeName = nodeName.toUpperCase();
+      return function (node) {
+        return node && node.nodeName.toUpperCase() === nodeName;
+      };
+    };
+
+    /**
+     * @method isText
+     *
+     *
+     *
+     * @param {Node} node
+     * @return {Boolean} true if node's type is text(3)
+     */
+    var isText = function (node) {
+      return node && node.nodeType === 3;
+    };
+
+    /**
+     * ex) br, col, embed, hr, img, input, ...
+     * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
+     */
+    var isVoid = function (node) {
+      return node && /^BR|^IMG|^HR|^IFRAME|^BUTTON/.test(node.nodeName.toUpperCase());
+    };
+
+    var isPara = function (node) {
+      if (isEditable(node)) {
+        return false;
+      }
+
+      // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph
+      return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
+    };
+
+    var isLi = makePredByNodeName('LI');
+
+    var isPurePara = function (node) {
+      return isPara(node) && !isLi(node);
+    };
+
+    var isTable = makePredByNodeName('TABLE');
+
+    var isInline = function (node) {
+      return !isBodyContainer(node) &&
+             !isList(node) &&
+             !isHr(node) &&
+             !isPara(node) &&
+             !isTable(node) &&
+             !isBlockquote(node);
+    };
+
+    var isList = function (node) {
+      return node && /^UL|^OL/.test(node.nodeName.toUpperCase());
+    };
+
+    var isHr = makePredByNodeName('HR');
+
+    var isCell = function (node) {
+      return node && /^TD|^TH/.test(node.nodeName.toUpperCase());
+    };
+
+    var isBlockquote = makePredByNodeName('BLOCKQUOTE');
+
+    var isBodyContainer = function (node) {
+      return isCell(node) || isBlockquote(node) || isEditable(node);
+    };
+
+    var isAnchor = makePredByNodeName('A');
+
+    var isParaInline = function (node) {
+      return isInline(node) && !!ancestor(node, isPara);
+    };
+
+    var isBodyInline = function (node) {
+      return isInline(node) && !ancestor(node, isPara);
+    };
+
+    var isBody = makePredByNodeName('BODY');
+
+    /**
+     * returns whether nodeB is closest sibling of nodeA
+     *
+     * @param {Node} nodeA
+     * @param {Node} nodeB
+     * @return {Boolean}
+     */
+    var isClosestSibling = function (nodeA, nodeB) {
+      return nodeA.nextSibling === nodeB ||
+             nodeA.previousSibling === nodeB;
+    };
+
+    /**
+     * returns array of closest siblings with node
+     *
+     * @param {Node} node
+     * @param {function} [pred] - predicate function
+     * @return {Node[]}
+     */
+    var withClosestSiblings = function (node, pred) {
+      pred = pred || func.ok;
+
+      var siblings = [];
+      if (node.previousSibling && pred(node.previousSibling)) {
+        siblings.push(node.previousSibling);
+      }
+      siblings.push(node);
+      if (node.nextSibling && pred(node.nextSibling)) {
+        siblings.push(node.nextSibling);
+      }
+      return siblings;
+    };
+
+    /**
+     * blank HTML for cursor position
+     * - [workaround] old IE only works with &nbsp;
+     * - [workaround] IE11 and other browser works with bogus br
+     */
+    var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? '&nbsp;' : '<br>';
+
+    /**
+     * @method nodeLength
+     *
+     * returns #text's text size or element's childNodes size
+     *
+     * @param {Node} node
+     */
+    var nodeLength = function (node) {
+      if (isText(node)) {
+        return node.nodeValue.length;
+      }
+
+      return node.childNodes.length;
+    };
+
+    /**
+     * returns whether node is empty or not.
+     *
+     * @param {Node} node
+     * @return {Boolean}
+     */
+    var isEmpty = function (node) {
+      var len = nodeLength(node);
+
+      if (len === 0) {
+        return true;
+      } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) {
+        // ex) <p><br></p>, <span><br></span>
+        return true;
+      } else if (list.all(node.childNodes, isText) && node.innerHTML === '') {
+        // ex) <p></p>, <span></span>
+        return true;
+      }
+
+      return false;
+    };
+
+    /**
+     * padding blankHTML if node is empty (for cursor position)
+     */
+    var paddingBlankHTML = function (node) {
+      if (!isVoid(node) && !nodeLength(node)) {
+        node.innerHTML = blankHTML;
+      }
+    };
+
+    /**
+     * find nearest ancestor predicate hit
+     *
+     * @param {Node} node
+     * @param {Function} pred - predicate function
+     */
+    var ancestor = function (node, pred) {
+      while (node) {
+        if (pred(node)) { return node; }
+        if (isEditable(node)) { break; }
+
+        node = node.parentNode;
+      }
+      return null;
+    };
+
+    /**
+     * find nearest ancestor only single child blood line and predicate hit
+     *
+     * @param {Node} node
+     * @param {Function} pred - predicate function
+     */
+    var singleChildAncestor = function (node, pred) {
+      node = node.parentNode;
+
+      while (node) {
+        if (nodeLength(node) !== 1) { break; }
+        if (pred(node)) { return node; }
+        if (isEditable(node)) { break; }
+
+        node = node.parentNode;
+      }
+      return null;
+    };
+
+    /**
+     * returns new array of ancestor nodes (until predicate hit).
+     *
+     * @param {Node} node
+     * @param {Function} [optional] pred - predicate function
+     */
+    var listAncestor = function (node, pred) {
+      pred = pred || func.fail;
+
+      var ancestors = [];
+      ancestor(node, function (el) {
+        if (!isEditable(el)) {
+          ancestors.push(el);
+        }
+
+        return pred(el);
+      });
+      return ancestors;
+    };
+
+    /**
+     * find farthest ancestor predicate hit
+     */
+    var lastAncestor = function (node, pred) {
+      var ancestors = listAncestor(node);
+      return list.last(ancestors.filter(pred));
+    };
+
+    /**
+     * returns common ancestor node between two nodes.
+     *
+     * @param {Node} nodeA
+     * @param {Node} nodeB
+     */
+    var commonAncestor = function (nodeA, nodeB) {
+      var ancestors = listAncestor(nodeA);
+      for (var n = nodeB; n; n = n.parentNode) {
+        if ($.inArray(n, ancestors) > -1) { return n; }
+      }
+      return null; // difference document area
+    };
+
+    /**
+     * listing all previous siblings (until predicate hit).
+     *
+     * @param {Node} node
+     * @param {Function} [optional] pred - predicate function
+     */
+    var listPrev = function (node, pred) {
+      pred = pred || func.fail;
+
+      var nodes = [];
+      while (node) {
+        if (pred(node)) { break; }
+        nodes.push(node);
+        node = node.previousSibling;
+      }
+      return nodes;
+    };
+
+    /**
+     * listing next siblings (until predicate hit).
+     *
+     * @param {Node} node
+     * @param {Function} [pred] - predicate function
+     */
+    var listNext = function (node, pred) {
+      pred = pred || func.fail;
+
+      var nodes = [];
+      while (node) {
+        if (pred(node)) { break; }
+        nodes.push(node);
+        node = node.nextSibling;
+      }
+      return nodes;
+    };
+
+    /**
+     * listing descendant nodes
+     *
+     * @param {Node} node
+     * @param {Function} [pred] - predicate function
+     */
+    var listDescendant = function (node, pred) {
+      var descendents = [];
+      pred = pred || func.ok;
+
+      // start DFS(depth first search) with node
+      (function fnWalk(current) {
+        if (node !== current && pred(current)) {
+          descendents.push(current);
+        }
+        for (var idx = 0, len = current.childNodes.length; idx < len; idx++) {
+          fnWalk(current.childNodes[idx]);
+        }
+      })(node);
+
+      return descendents;
+    };
+
+    /**
+     * wrap node with new tag.
+     *
+     * @param {Node} node
+     * @param {Node} tagName of wrapper
+     * @return {Node} - wrapper
+     */
+    var wrap = function (node, wrapperName) {
+      var parent = node.parentNode;
+      var wrapper = $('<' + wrapperName + '>')[0];
+
+      parent.insertBefore(wrapper, node);
+      wrapper.appendChild(node);
+
+      return wrapper;
+    };
+
+    /**
+     * insert node after preceding
+     *
+     * @param {Node} node
+     * @param {Node} preceding - predicate function
+     */
+    var insertAfter = function (node, preceding) {
+      var next = preceding.nextSibling, parent = preceding.parentNode;
+      if (next) {
+        parent.insertBefore(node, next);
+      } else {
+        parent.appendChild(node);
+      }
+      return node;
+    };
+
+    /**
+     * append elements.
+     *
+     * @param {Node} node
+     * @param {Collection} aChild
+     */
+    var appendChildNodes = function (node, aChild) {
+      $.each(aChild, function (idx, child) {
+        node.appendChild(child);
+      });
+      return node;
+    };
+
+    /**
+     * returns whether boundaryPoint is left edge or not.
+     *
+     * @param {BoundaryPoint} point
+     * @return {Boolean}
+     */
+    var isLeftEdgePoint = function (point) {
+      return point.offset === 0;
+    };
+
+    /**
+     * returns whether boundaryPoint is right edge or not.
+     *
+     * @param {BoundaryPoint} point
+     * @return {Boolean}
+     */
+    var isRightEdgePoint = function (point) {
+      return point.offset === nodeLength(point.node);
+    };
+
+    /**
+     * returns whether boundaryPoint is edge or not.
+     *
+     * @param {BoundaryPoint} point
+     * @return {Boolean}
+     */
+    var isEdgePoint = function (point) {
+      return isLeftEdgePoint(point) || isRightEdgePoint(point);
+    };
+
+    /**
+     * returns wheter node is left edge of ancestor or not.
+     *
+     * @param {Node} node
+     * @param {Node} ancestor
+     * @return {Boolean}
+     */
+    var isLeftEdgeOf = function (node, ancestor) {
+      while (node && node !== ancestor) {
+        if (position(node) !== 0) {
+          return false;
+        }
+        node = node.parentNode;
+      }
+
+      return true;
+    };
+
+    /**
+     * returns whether node is right edge of ancestor or not.
+     *
+     * @param {Node} node
+     * @param {Node} ancestor
+     * @return {Boolean}
+     */
+    var isRightEdgeOf = function (node, ancestor) {
+      while (node && node !== ancestor) {
+        if (position(node) !== nodeLength(node.parentNode) - 1) {
+          return false;
+        }
+        node = node.parentNode;
+      }
+
+      return true;
+    };
+
+    /**
+     * returns whether point is left edge of ancestor or not.
+     * @param {BoundaryPoint} point
+     * @param {Node} ancestor
+     * @return {Boolean}
+     */
+    var isLeftEdgePointOf = function (point, ancestor) {
+      return isLeftEdgePoint(point) && isLeftEdgeOf(point.node, ancestor);
+    };
+
+    /**
+     * returns whether point is right edge of ancestor or not.
+     * @param {BoundaryPoint} point
+     * @param {Node} ancestor
+     * @return {Boolean}
+     */
+    var isRightEdgePointOf = function (point, ancestor) {
+      return isRightEdgePoint(point) && isRightEdgeOf(point.node, ancestor);
+    };
+
+    /**
+     * returns offset from parent.
+     *
+     * @param {Node} node
+     */
+    var position = function (node) {
+      var offset = 0;
+      while ((node = node.previousSibling)) {
+        offset += 1;
+      }
+      return offset;
+    };
+
+    var hasChildren = function (node) {
+      return !!(node && node.childNodes && node.childNodes.length);
+    };
+
+    /**
+     * returns previous boundaryPoint
+     *
+     * @param {BoundaryPoint} point
+     * @param {Boolean} isSkipInnerOffset
+     * @return {BoundaryPoint}
+     */
+    var prevPoint = function (point, isSkipInnerOffset) {
+      var node, offset;
+
+      if (point.offset === 0) {
+        if (isEditable(point.node)) {
+          return null;
+        }
+
+        node = point.node.parentNode;
+        offset = position(point.node);
+      } else if (hasChildren(point.node)) {
+        node = point.node.childNodes[point.offset - 1];
+        offset = nodeLength(node);
+      } else {
+        node = point.node;
+        offset = isSkipInnerOffset ? 0 : point.offset - 1;
+      }
+
+      return {
+        node: node,
+        offset: offset
+      };
+    };
+
+    /**
+     * returns next boundaryPoint
+     *
+     * @param {BoundaryPoint} point
+     * @param {Boolean} isSkipInnerOffset
+     * @return {BoundaryPoint}
+     */
+    var nextPoint = function (point, isSkipInnerOffset) {
+      var node, offset;
+
+      if (nodeLength(point.node) === point.offset) {
+        if (isEditable(point.node)) {
+          return null;
+        }
+
+        node = point.node.parentNode;
+        offset = position(point.node) + 1;
+      } else if (hasChildren(point.node)) {
+        node = point.node.childNodes[point.offset];
+        offset = 0;
+      } else {
+        node = point.node;
+        offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1;
+      }
+
+      return {
+        node: node,
+        offset: offset
+      };
+    };
+
+    /**
+     * returns whether pointA and pointB is same or not.
+     *
+     * @param {BoundaryPoint} pointA
+     * @param {BoundaryPoint} pointB
+     * @return {Boolean}
+     */
+    var isSamePoint = function (pointA, pointB) {
+      return pointA.node === pointB.node && pointA.offset === pointB.offset;
+    };
+
+    /**
+     * returns whether point is visible (can set cursor) or not.
+     * 
+     * @param {BoundaryPoint} point
+     * @return {Boolean}
+     */
+    var isVisiblePoint = function (point) {
+      if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) {
+        return true;
+      }
+
+      var leftNode = point.node.childNodes[point.offset - 1];
+      var rightNode = point.node.childNodes[point.offset];
+      if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) {
+        return true;
+      }
+
+      return false;
+    };
+
+    /**
+     * @method prevPointUtil
+     *
+     * @param {BoundaryPoint} point
+     * @param {Function} pred
+     * @return {BoundaryPoint}
+     */
+    var prevPointUntil = function (point, pred) {
+      while (point) {
+        if (pred(point)) {
+          return point;
+        }
+
+        point = prevPoint(point);
+      }
+
+      return null;
+    };
+
+    /**
+     * @method nextPointUntil
+     *
+     * @param {BoundaryPoint} point
+     * @param {Function} pred
+     * @return {BoundaryPoint}
+     */
+    var nextPointUntil = function (point, pred) {
+      while (point) {
+        if (pred(point)) {
+          return point;
+        }
+
+        point = nextPoint(point);
+      }
+
+      return null;
+    };
+
+    /**
+     * returns whether point has character or not.
+     *
+     * @param {Point} point
+     * @return {Boolean}
+     */
+    var isCharPoint = function (point) {
+      if (!isText(point.node)) {
+        return false;
+      }
+
+      var ch = point.node.nodeValue.charAt(point.offset - 1);
+      return ch && (ch !== ' ' && ch !== NBSP_CHAR);
+    };
+
+    /**
+     * @method walkPoint
+     *
+     * @param {BoundaryPoint} startPoint
+     * @param {BoundaryPoint} endPoint
+     * @param {Function} handler
+     * @param {Boolean} isSkipInnerOffset
+     */
+    var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) {
+      var point = startPoint;
+
+      while (point) {
+        handler(point);
+
+        if (isSamePoint(point, endPoint)) {
+          break;
+        }
+
+        var isSkipOffset = isSkipInnerOffset &&
+                           startPoint.node !== point.node &&
+                           endPoint.node !== point.node;
+        point = nextPoint(point, isSkipOffset);
+      }
+    };
+
+    /**
+     * @method makeOffsetPath
+     *
+     * return offsetPath(array of offset) from ancestor
+     *
+     * @param {Node} ancestor - ancestor node
+     * @param {Node} node
+     */
+    var makeOffsetPath = function (ancestor, node) {
+      var ancestors = listAncestor(node, func.eq(ancestor));
+      return ancestors.map(position).reverse();
+    };
+
+    /**
+     * @method fromOffsetPath
+     *
+     * return element from offsetPath(array of offset)
+     *
+     * @param {Node} ancestor - ancestor node
+     * @param {array} offsets - offsetPath
+     */
+    var fromOffsetPath = function (ancestor, offsets) {
+      var current = ancestor;
+      for (var i = 0, len = offsets.length; i < len; i++) {
+        if (current.childNodes.length <= offsets[i]) {
+          current = current.childNodes[current.childNodes.length - 1];
+        } else {
+          current = current.childNodes[offsets[i]];
+        }
+      }
+      return current;
+    };
+
+    /**
+     * @method splitNode
+     *
+     * split element or #text
+     *
+     * @param {BoundaryPoint} point
+     * @param {Object} [options]
+     * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
+     * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
+     * @return {Node} right node of boundaryPoint
+     */
+    var splitNode = function (point, options) {
+      var isSkipPaddingBlankHTML = options && options.isSkipPaddingBlankHTML;
+      var isNotSplitEdgePoint = options && options.isNotSplitEdgePoint;
+
+      // edge case
+      if (isEdgePoint(point) && (isText(point.node) || isNotSplitEdgePoint)) {
+        if (isLeftEdgePoint(point)) {
+          return point.node;
+        } else if (isRightEdgePoint(point)) {
+          return point.node.nextSibling;
+        }
+      }
+
+      // split #text
+      if (isText(point.node)) {
+        return point.node.splitText(point.offset);
+      } else {
+        var childNode = point.node.childNodes[point.offset];
+        var clone = insertAfter(point.node.cloneNode(false), point.node);
+        appendChildNodes(clone, listNext(childNode));
+
+        if (!isSkipPaddingBlankHTML) {
+          paddingBlankHTML(point.node);
+          paddingBlankHTML(clone);
+        }
+
+        return clone;
+      }
+    };
+
+    /**
+     * @method splitTree
+     *
+     * split tree by point
+     *
+     * @param {Node} root - split root
+     * @param {BoundaryPoint} point
+     * @param {Object} [options]
+     * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
+     * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
+     * @return {Node} right node of boundaryPoint
+     */
+    var splitTree = function (root, point, options) {
+      // ex) [#text, <span>, <p>]
+      var ancestors = listAncestor(point.node, func.eq(root));
+
+      if (!ancestors.length) {
+        return null;
+      } else if (ancestors.length === 1) {
+        return splitNode(point, options);
+      }
+
+      return ancestors.reduce(function (node, parent) {
+        if (node === point.node) {
+          node = splitNode(point, options);
+        }
+
+        return splitNode({
+          node: parent,
+          offset: node ? dom.position(node) : nodeLength(parent)
+        }, options);
+      });
+    };
+
+    /**
+     * split point
+     *
+     * @param {Point} point
+     * @param {Boolean} isInline
+     * @return {Object}
+     */
+    var splitPoint = function (point, isInline) {
+      // find splitRoot, container
+      //  - inline: splitRoot is a child of paragraph
+      //  - block: splitRoot is a child of bodyContainer
+      var pred = isInline ? isPara : isBodyContainer;
+      var ancestors = listAncestor(point.node, pred);
+      var topAncestor = list.last(ancestors) || point.node;
+
+      var splitRoot, container;
+      if (pred(topAncestor)) {
+        splitRoot = ancestors[ancestors.length - 2];
+        container = topAncestor;
+      } else {
+        splitRoot = topAncestor;
+        container = splitRoot.parentNode;
+      }
+
+      // if splitRoot is exists, split with splitTree
+      var pivot = splitRoot && splitTree(splitRoot, point, {
+        isSkipPaddingBlankHTML: isInline,
+        isNotSplitEdgePoint: isInline
+      });
+
+      // if container is point.node, find pivot with point.offset
+      if (!pivot && container === point.node) {
+        pivot = point.node.childNodes[point.offset];
+      }
+
+      return {
+        rightNode: pivot,
+        container: container
+      };
+    };
+
+    var create = function (nodeName) {
+      return document.createElement(nodeName);
+    };
+
+    var createText = function (text) {
+      return document.createTextNode(text);
+    };
+
+    /**
+     * @method remove
+     *
+     * remove node, (isRemoveChild: remove child or not)
+     *
+     * @param {Node} node
+     * @param {Boolean} isRemoveChild
+     */
+    var remove = function (node, isRemoveChild) {
+      if (!node || !node.parentNode) { return; }
+      if (node.removeNode) { return node.removeNode(isRemoveChild); }
+
+      var parent = node.parentNode;
+      if (!isRemoveChild) {
+        var nodes = [];
+        var i, len;
+        for (i = 0, len = node.childNodes.length; i < len; i++) {
+          nodes.push(node.childNodes[i]);
+        }
+
+        for (i = 0, len = nodes.length; i < len; i++) {
+          parent.insertBefore(nodes[i], node);
+        }
+      }
+
+      parent.removeChild(node);
+    };
+
+    /**
+     * @method removeWhile
+     *
+     * @param {Node} node
+     * @param {Function} pred
+     */
+    var removeWhile = function (node, pred) {
+      while (node) {
+        if (isEditable(node) || !pred(node)) {
+          break;
+        }
+
+        var parent = node.parentNode;
+        remove(node);
+        node = parent;
+      }
+    };
+
+    /**
+     * @method replace
+     *
+     * replace node with provided nodeName
+     *
+     * @param {Node} node
+     * @param {String} nodeName
+     * @return {Node} - new node
+     */
+    var replace = function (node, nodeName) {
+      if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) {
+        return node;
+      }
+
+      var newNode = create(nodeName);
+
+      if (node.style.cssText) {
+        newNode.style.cssText = node.style.cssText;
+      }
+
+      appendChildNodes(newNode, list.from(node.childNodes));
+      insertAfter(newNode, node);
+      remove(node);
+
+      return newNode;
+    };
+
+    var isTextarea = makePredByNodeName('TEXTAREA');
+
+    /**
+     * @param {jQuery} $node
+     * @param {Boolean} [stripLinebreaks] - default: false
+     */
+    var value = function ($node, stripLinebreaks) {
+      var val = isTextarea($node[0]) ? $node.val() : $node.html();
+      if (stripLinebreaks) {
+        return val.replace(/[\n\r]/g, '');
+      }
+      return val;
+    };
+
+    /**
+     * @method html
+     *
+     * get the HTML contents of node
+     *
+     * @param {jQuery} $node
+     * @param {Boolean} [isNewlineOnBlock]
+     */
+    var html = function ($node, isNewlineOnBlock) {
+      var markup = value($node);
+
+      if (isNewlineOnBlock) {
+        var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g;
+        markup = markup.replace(regexTag, function (match, endSlash, name) {
+          name = name.toUpperCase();
+          var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) &&
+                                       !!endSlash;
+          var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name);
+
+          return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : '');
+        });
+        markup = $.trim(markup);
+      }
+
+      return markup;
+    };
+
+    return {
+      /** @property {String} NBSP_CHAR */
+      NBSP_CHAR: NBSP_CHAR,
+      /** @property {String} ZERO_WIDTH_NBSP_CHAR */
+      ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR,
+      /** @property {String} blank */
+      blank: blankHTML,
+      /** @property {String} emptyPara */
+      emptyPara: '<p>' + blankHTML + '</p>',
+      makePredByNodeName: makePredByNodeName,
+      isEditable: isEditable,
+      isControlSizing: isControlSizing,
+      buildLayoutInfo: buildLayoutInfo,
+      makeLayoutInfo: makeLayoutInfo,
+      isText: isText,
+      isVoid: isVoid,
+      isPara: isPara,
+      isPurePara: isPurePara,
+      isInline: isInline,
+      isBlock: func.not(isInline),
+      isBodyInline: isBodyInline,
+      isBody: isBody,
+      isParaInline: isParaInline,
+      isList: isList,
+      isTable: isTable,
+      isCell: isCell,
+      isBlockquote: isBlockquote,
+      isBodyContainer: isBodyContainer,
+      isAnchor: isAnchor,
+      isDiv: makePredByNodeName('DIV'),
+      isLi: isLi,
+      isBR: makePredByNodeName('BR'),
+      isSpan: makePredByNodeName('SPAN'),
+      isB: makePredByNodeName('B'),
+      isU: makePredByNodeName('U'),
+      isS: makePredByNodeName('S'),
+      isI: makePredByNodeName('I'),
+      isImg: makePredByNodeName('IMG'),
+      isTextarea: isTextarea,
+      isEmpty: isEmpty,
+      isEmptyAnchor: func.and(isAnchor, isEmpty),
+      isClosestSibling: isClosestSibling,
+      withClosestSiblings: withClosestSiblings,
+      nodeLength: nodeLength,
+      isLeftEdgePoint: isLeftEdgePoint,
+      isRightEdgePoint: isRightEdgePoint,
+      isEdgePoint: isEdgePoint,
+      isLeftEdgeOf: isLeftEdgeOf,
+      isRightEdgeOf: isRightEdgeOf,
+      isLeftEdgePointOf: isLeftEdgePointOf,
+      isRightEdgePointOf: isRightEdgePointOf,
+      prevPoint: prevPoint,
+      nextPoint: nextPoint,
+      isSamePoint: isSamePoint,
+      isVisiblePoint: isVisiblePoint,
+      prevPointUntil: prevPointUntil,
+      nextPointUntil: nextPointUntil,
+      isCharPoint: isCharPoint,
+      walkPoint: walkPoint,
+      ancestor: ancestor,
+      singleChildAncestor: singleChildAncestor,
+      listAncestor: listAncestor,
+      lastAncestor: lastAncestor,
+      listNext: listNext,
+      listPrev: listPrev,
+      listDescendant: listDescendant,
+      commonAncestor: commonAncestor,
+      wrap: wrap,
+      insertAfter: insertAfter,
+      appendChildNodes: appendChildNodes,
+      position: position,
+      hasChildren: hasChildren,
+      makeOffsetPath: makeOffsetPath,
+      fromOffsetPath: fromOffsetPath,
+      splitTree: splitTree,
+      splitPoint: splitPoint,
+      create: create,
+      createText: createText,
+      remove: remove,
+      removeWhile: removeWhile,
+      replace: replace,
+      html: html,
+      value: value
+    };
+  })();
+
+
+  var range = (function () {
+
+    /**
+     * return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js
+     *
+     * @param {TextRange} textRange
+     * @param {Boolean} isStart
+     * @return {BoundaryPoint}
+     *
+     * @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
+     */
+    var textRangeToPoint = function (textRange, isStart) {
+      var container = textRange.parentElement(), offset;
+  
+      var tester = document.body.createTextRange(), prevContainer;
+      var childNodes = list.from(container.childNodes);
+      for (offset = 0; offset < childNodes.length; offset++) {
+        if (dom.isText(childNodes[offset])) {
+          continue;
+        }
+        tester.moveToElementText(childNodes[offset]);
+        if (tester.compareEndPoints('StartToStart', textRange) >= 0) {
+          break;
+        }
+        prevContainer = childNodes[offset];
+      }
+  
+      if (offset !== 0 && dom.isText(childNodes[offset - 1])) {
+        var textRangeStart = document.body.createTextRange(), curTextNode = null;
+        textRangeStart.moveToElementText(prevContainer || container);
+        textRangeStart.collapse(!prevContainer);
+        curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild;
+  
+        var pointTester = textRange.duplicate();
+        pointTester.setEndPoint('StartToStart', textRangeStart);
+        var textCount = pointTester.text.replace(/[\r\n]/g, '').length;
+  
+        while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) {
+          textCount -= curTextNode.nodeValue.length;
+          curTextNode = curTextNode.nextSibling;
+        }
+  
+        /* jshint ignore:start */
+        var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack
+        /* jshint ignore:end */
+  
+        if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) &&
+            textCount === curTextNode.nodeValue.length) {
+          textCount -= curTextNode.nodeValue.length;
+          curTextNode = curTextNode.nextSibling;
+        }
+  
+        container = curTextNode;
+        offset = textCount;
+      }
+  
+      return {
+        cont: container,
+        offset: offset
+      };
+    };
+    
+    /**
+     * return TextRange from boundary point (inspired by google closure-library)
+     * @param {BoundaryPoint} point
+     * @return {TextRange}
+     */
+    var pointToTextRange = function (point) {
+      var textRangeInfo = function (container, offset) {
+        var node, isCollapseToStart;
+  
+        if (dom.isText(container)) {
+          var prevTextNodes = dom.listPrev(container, func.not(dom.isText));
+          var prevContainer = list.last(prevTextNodes).previousSibling;
+          node =  prevContainer || container.parentNode;
+          offset += list.sum(list.tail(prevTextNodes), dom.nodeLength);
+          isCollapseToStart = !prevContainer;
+        } else {
+          node = container.childNodes[offset] || container;
+          if (dom.isText(node)) {
+            return textRangeInfo(node, 0);
+          }
+  
+          offset = 0;
+          isCollapseToStart = false;
+        }
+  
+        return {
+          node: node,
+          collapseToStart: isCollapseToStart,
+          offset: offset
+        };
+      };
+  
+      var textRange = document.body.createTextRange();
+      var info = textRangeInfo(point.node, point.offset);
+  
+      textRange.moveToElementText(info.node);
+      textRange.collapse(info.collapseToStart);
+      textRange.moveStart('character', info.offset);
+      return textRange;
+    };
+    
+    /**
+     * Wrapped Range
+     *
+     * @constructor
+     * @param {Node} sc - start container
+     * @param {Number} so - start offset
+     * @param {Node} ec - end container
+     * @param {Number} eo - end offset
+     */
+    var WrappedRange = function (sc, so, ec, eo) {
+      this.sc = sc;
+      this.so = so;
+      this.ec = ec;
+      this.eo = eo;
+  
+      // nativeRange: get nativeRange from sc, so, ec, eo
+      var nativeRange = function () {
+        if (agent.isW3CRangeSupport) {
+          var w3cRange = document.createRange();
+          w3cRange.setStart(sc, so);
+          w3cRange.setEnd(ec, eo);
+
+          return w3cRange;
+        } else {
+          var textRange = pointToTextRange({
+            node: sc,
+            offset: so
+          });
+
+          textRange.setEndPoint('EndToEnd', pointToTextRange({
+            node: ec,
+            offset: eo
+          }));
+
+          return textRange;
+        }
+      };
+
+      this.getPoints = function () {
+        return {
+          sc: sc,
+          so: so,
+          ec: ec,
+          eo: eo
+        };
+      };
+
+      this.getStartPoint = function () {
+        return {
+          node: sc,
+          offset: so
+        };
+      };
+
+      this.getEndPoint = function () {
+        return {
+          node: ec,
+          offset: eo
+        };
+      };
+
+      /**
+       * select update visible range
+       */
+      this.select = function () {
+        var nativeRng = nativeRange();
+        if (agent.isW3CRangeSupport) {
+          var selection = document.getSelection();
+          if (selection.rangeCount > 0) {
+            selection.removeAllRanges();
+          }
+          selection.addRange(nativeRng);
+        } else {
+          nativeRng.select();
+        }
+        
+        return this;
+      };
+
+      /**
+       * @return {WrappedRange}
+       */
+      this.normalize = function () {
+
+        /**
+         * @param {BoundaryPoint} point
+         * @param {Boolean} isLeftToRight
+         * @return {BoundaryPoint}
+         */
+        var getVisiblePoint = function (point, isLeftToRight) {
+          if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) ||
+              (dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) ||
+              (dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) ||
+              (dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) {
+            return point;
+          }
+
+          // point on block's edge
+          var block = dom.ancestor(point.node, dom.isBlock);
+          if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) ||
+              ((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) {
+
+            // returns point already on visible point
+            if (dom.isVisiblePoint(point)) {
+              return point;
+            }
+            // reverse direction 
+            isLeftToRight = !isLeftToRight;
+          }
+
+          var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) :
+                                          dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint);
+          return nextPoint || point;
+        };
+
+        var endPoint = getVisiblePoint(this.getEndPoint(), false);
+        var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true);
+
+        return new WrappedRange(
+          startPoint.node,
+          startPoint.offset,
+          endPoint.node,
+          endPoint.offset
+        );
+      };
+
+      /**
+       * returns matched nodes on range
+       *
+       * @param {Function} [pred] - predicate function
+       * @param {Object} [options]
+       * @param {Boolean} [options.includeAncestor]
+       * @param {Boolean} [options.fullyContains]
+       * @return {Node[]}
+       */
+      this.nodes = function (pred, options) {
+        pred = pred || func.ok;
+
+        var includeAncestor = options && options.includeAncestor;
+        var fullyContains = options && options.fullyContains;
+
+        // TODO compare points and sort
+        var startPoint = this.getStartPoint();
+        var endPoint = this.getEndPoint();
+
+        var nodes = [];
+        var leftEdgeNodes = [];
+
+        dom.walkPoint(startPoint, endPoint, function (point) {
+          if (dom.isEditable(point.node)) {
+            return;
+          }
+
+          var node;
+          if (fullyContains) {
+            if (dom.isLeftEdgePoint(point)) {
+              leftEdgeNodes.push(point.node);
+            }
+            if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
+              node = point.node;
+            }
+          } else if (includeAncestor) {
+            node = dom.ancestor(point.node, pred);
+          } else {
+            node = point.node;
+          }
+
+          if (node && pred(node)) {
+            nodes.push(node);
+          }
+        }, true);
+
+        return list.unique(nodes);
+      };
+
+      /**
+       * returns commonAncestor of range
+       * @return {Element} - commonAncestor
+       */
+      this.commonAncestor = function () {
+        return dom.commonAncestor(sc, ec);
+      };
+
+      /**
+       * returns expanded range by pred
+       *
+       * @param {Function} pred - predicate function
+       * @return {WrappedRange}
+       */
+      this.expand = function (pred) {
+        var startAncestor = dom.ancestor(sc, pred);
+        var endAncestor = dom.ancestor(ec, pred);
+
+        if (!startAncestor && !endAncestor) {
+          return new WrappedRange(sc, so, ec, eo);
+        }
+
+        var boundaryPoints = this.getPoints();
+
+        if (startAncestor) {
+          boundaryPoints.sc = startAncestor;
+          boundaryPoints.so = 0;
+        }
+
+        if (endAncestor) {
+          boundaryPoints.ec = endAncestor;
+          boundaryPoints.eo = dom.nodeLength(endAncestor);
+        }
+
+        return new WrappedRange(
+          boundaryPoints.sc,
+          boundaryPoints.so,
+          boundaryPoints.ec,
+          boundaryPoints.eo
+        );
+      };
+
+      /**
+       * @param {Boolean} isCollapseToStart
+       * @return {WrappedRange}
+       */
+      this.collapse = function (isCollapseToStart) {
+        if (isCollapseToStart) {
+          return new WrappedRange(sc, so, sc, so);
+        } else {
+          return new WrappedRange(ec, eo, ec, eo);
+        }
+      };
+
+      /**
+       * splitText on range
+       */
+      this.splitText = function () {
+        var isSameContainer = sc === ec;
+        var boundaryPoints = this.getPoints();
+
+        if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
+          ec.splitText(eo);
+        }
+
+        if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
+          boundaryPoints.sc = sc.splitText(so);
+          boundaryPoints.so = 0;
+
+          if (isSameContainer) {
+            boundaryPoints.ec = boundaryPoints.sc;
+            boundaryPoints.eo = eo - so;
+          }
+        }
+
+        return new WrappedRange(
+          boundaryPoints.sc,
+          boundaryPoints.so,
+          boundaryPoints.ec,
+          boundaryPoints.eo
+        );
+      };
+
+      /**
+       * delete contents on range
+       * @return {WrappedRange}
+       */
+      this.deleteContents = function () {
+        if (this.isCollapsed()) {
+          return this;
+        }
+
+        var rng = this.splitText();
+        var nodes = rng.nodes(null, {
+          fullyContains: true
+        });
+
+        // find new cursor point
+        var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
+          return !list.contains(nodes, point.node);
+        });
+
+        var emptyParents = [];
+        $.each(nodes, function (idx, node) {
+          // find empty parents
+          var parent = node.parentNode;
+          if (point.node !== parent && dom.nodeLength(parent) === 1) {
+            emptyParents.push(parent);
+          }
+          dom.remove(node, false);
+        });
+
+        // remove empty parents
+        $.each(emptyParents, function (idx, node) {
+          dom.remove(node, false);
+        });
+
+        return new WrappedRange(
+          point.node,
+          point.offset,
+          point.node,
+          point.offset
+        ).normalize();
+      };
+      
+      /**
+       * makeIsOn: return isOn(pred) function
+       */
+      var makeIsOn = function (pred) {
+        return function () {
+          var ancestor = dom.ancestor(sc, pred);
+          return !!ancestor && (ancestor === dom.ancestor(ec, pred));
+        };
+      };
+  
+      // isOnEditable: judge whether range is on editable or not
+      this.isOnEditable = makeIsOn(dom.isEditable);
+      // isOnList: judge whether range is on list node or not
+      this.isOnList = makeIsOn(dom.isList);
+      // isOnAnchor: judge whether range is on anchor node or not
+      this.isOnAnchor = makeIsOn(dom.isAnchor);
+      // isOnAnchor: judge whether range is on cell node or not
+      this.isOnCell = makeIsOn(dom.isCell);
+
+      /**
+       * @param {Function} pred
+       * @return {Boolean}
+       */
+      this.isLeftEdgeOf = function (pred) {
+        if (!dom.isLeftEdgePoint(this.getStartPoint())) {
+          return false;
+        }
+
+        var node = dom.ancestor(this.sc, pred);
+        return node && dom.isLeftEdgeOf(this.sc, node);
+      };
+
+      /**
+       * returns whether range was collapsed or not
+       */
+      this.isCollapsed = function () {
+        return sc === ec && so === eo;
+      };
+
+      /**
+       * wrap inline nodes which children of body with paragraph
+       *
+       * @return {WrappedRange}
+       */
+      this.wrapBodyInlineWithPara = function () {
+        if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
+          sc.innerHTML = dom.emptyPara;
+          return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0);
+        }
+
+        /**
+         * [workaround] firefox often create range on not visible point. so normalize here.
+         *  - firefox: |<p>text</p>|
+         *  - chrome: <p>|text|</p>
+         */
+        var rng = this.normalize();
+        if (dom.isParaInline(sc) || dom.isPara(sc)) {
+          return rng;
+        }
+
+        // find inline top ancestor
+        var topAncestor;
+        if (dom.isInline(rng.sc)) {
+          var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline));
+          topAncestor = list.last(ancestors);
+          if (!dom.isInline(topAncestor)) {
+            topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so];
+          }
+        } else {
+          topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0];
+        }
+
+        // siblings not in paragraph
+        var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
+        inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
+
+        // wrap with paragraph
+        if (inlineSiblings.length) {
+          var para = dom.wrap(list.head(inlineSiblings), 'p');
+          dom.appendChildNodes(para, list.tail(inlineSiblings));
+        }
+
+        return this.normalize();
+      };
+
+      /**
+       * insert node at current cursor
+       *
+       * @param {Node} node
+       * @return {Node}
+       */
+      this.insertNode = function (node) {
+        var rng = this.wrapBodyInlineWithPara().deleteContents();
+        var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node));
+
+        if (info.rightNode) {
+          info.rightNode.parentNode.insertBefore(node, info.rightNode);
+        } else {
+          info.container.appendChild(node);
+        }
+
+        return node;
+      };
+
+      /**
+       * insert html at current cursor
+       */
+      this.pasteHTML = function (markup) {
+        var contentsContainer = $('<div></div>').html(markup)[0];
+        var childNodes = list.from(contentsContainer.childNodes);
+
+        var rng = this.wrapBodyInlineWithPara().deleteContents();
+
+        return childNodes.reverse().map(function (childNode) {
+          return rng.insertNode(childNode);
+        }).reverse();
+      };
+  
+      /**
+       * returns text in range
+       *
+       * @return {String}
+       */
+      this.toString = function () {
+        var nativeRng = nativeRange();
+        return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
+      };
+
+      /**
+       * returns range for word before cursor
+       *
+       * @param {Boolean} [findAfter] - find after cursor, default: false
+       * @return {WrappedRange}
+       */
+      this.getWordRange = function (findAfter) {
+        var endPoint = this.getEndPoint();
+
+        if (!dom.isCharPoint(endPoint)) {
+          return this;
+        }
+
+        var startPoint = dom.prevPointUntil(endPoint, function (point) {
+          return !dom.isCharPoint(point);
+        });
+
+        if (findAfter) {
+          endPoint = dom.nextPointUntil(endPoint, function (point) {
+            return !dom.isCharPoint(point);
+          });
+        }
+
+        return new WrappedRange(
+          startPoint.node,
+          startPoint.offset,
+          endPoint.node,
+          endPoint.offset
+        );
+      };
+  
+      /**
+       * create offsetPath bookmark
+       *
+       * @param {Node} editable
+       */
+      this.bookmark = function (editable) {
+        return {
+          s: {
+            path: dom.makeOffsetPath(editable, sc),
+            offset: so
+          },
+          e: {
+            path: dom.makeOffsetPath(editable, ec),
+            offset: eo
+          }
+        };
+      };
+
+      /**
+       * create offsetPath bookmark base on paragraph
+       *
+       * @param {Node[]} paras
+       */
+      this.paraBookmark = function (paras) {
+        return {
+          s: {
+            path: list.tail(dom.makeOffsetPath(list.head(paras), sc)),
+            offset: so
+          },
+          e: {
+            path: list.tail(dom.makeOffsetPath(list.last(paras), ec)),
+            offset: eo
+          }
+        };
+      };
+
+      /**
+       * getClientRects
+       * @return {Rect[]}
+       */
+      this.getClientRects = function () {
+        var nativeRng = nativeRange();
+        return nativeRng.getClientRects();
+      };
+    };
+
+  /**
+   * @class core.range
+   *
+   * Data structure
+   *  * BoundaryPoint: a point of dom tree
+   *  * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range
+   *
+   * See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position
+   *
+   * @singleton
+   * @alternateClassName range
+   */
+    return {
+      /**
+       * @method
+       * 
+       * create Range Object From arguments or Browser Selection
+       *
+       * @param {Node} sc - start container
+       * @param {Number} so - start offset
+       * @param {Node} ec - end container
+       * @param {Number} eo - end offset
+       * @return {WrappedRange}
+       */
+      create : function (sc, so, ec, eo) {
+        if (!arguments.length) { // from Browser Selection
+          if (agent.isW3CRangeSupport) {
+            var selection = document.getSelection();
+            if (!selection || selection.rangeCount === 0) {
+              return null;
+            } else if (dom.isBody(selection.anchorNode)) {
+              // Firefox: returns entire body as range on initialization. We won't never need it.
+              return null;
+            }
+  
+            var nativeRng = selection.getRangeAt(0);
+            sc = nativeRng.startContainer;
+            so = nativeRng.startOffset;
+            ec = nativeRng.endContainer;
+            eo = nativeRng.endOffset;
+          } else { // IE8: TextRange
+            var textRange = document.selection.createRange();
+            var textRangeEnd = textRange.duplicate();
+            textRangeEnd.collapse(false);
+            var textRangeStart = textRange;
+            textRangeStart.collapse(true);
+  
+            var startPoint = textRangeToPoint(textRangeStart, true),
+            endPoint = textRangeToPoint(textRangeEnd, false);
+
+            // same visible point case: range was collapsed.
+            if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) &&
+                dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) &&
+                endPoint.node.nextSibling === startPoint.node) {
+              startPoint = endPoint;
+            }
+
+            sc = startPoint.cont;
+            so = startPoint.offset;
+            ec = endPoint.cont;
+            eo = endPoint.offset;
+          }
+        } else if (arguments.length === 2) { //collapsed
+          ec = sc;
+          eo = so;
+        }
+        return new WrappedRange(sc, so, ec, eo);
+      },
+
+      /**
+       * @method 
+       * 
+       * create WrappedRange from node
+       *
+       * @param {Node} node
+       * @return {WrappedRange}
+       */
+      createFromNode: function (node) {
+        var sc = node;
+        var so = 0;
+        var ec = node;
+        var eo = dom.nodeLength(ec);
+
+        // browsers can't target a picture or void node
+        if (dom.isVoid(sc)) {
+          so = dom.listPrev(sc).length - 1;
+          sc = sc.parentNode;
+        }
+        if (dom.isBR(ec)) {
+          eo = dom.listPrev(ec).length - 1;
+          ec = ec.parentNode;
+        } else if (dom.isVoid(ec)) {
+          eo = dom.listPrev(ec).length;
+          ec = ec.parentNode;
+        }
+
+        return this.create(sc, so, ec, eo);
+      },
+
+      /**
+       * create WrappedRange from node after position
+       *
+       * @param {Node} node
+       * @return {WrappedRange}
+       */
+      createFromNodeBefore: function (node) {
+        return this.createFromNode(node).collapse(true);
+      },
+
+      /**
+       * create WrappedRange from node after position
+       *
+       * @param {Node} node
+       * @return {WrappedRange}
+       */
+      createFromNodeAfter: function (node) {
+        return this.createFromNode(node).collapse();
+      },
+
+      /**
+       * @method 
+       * 
+       * create WrappedRange from bookmark
+       *
+       * @param {Node} editable
+       * @param {Object} bookmark
+       * @return {WrappedRange}
+       */
+      createFromBookmark : function (editable, bookmark) {
+        var sc = dom.fromOffsetPath(editable, bookmark.s.path);
+        var so = bookmark.s.offset;
+        var ec = dom.fromOffsetPath(editable, bookmark.e.path);
+        var eo = bookmark.e.offset;
+        return new WrappedRange(sc, so, ec, eo);
+      },
+
+      /**
+       * @method 
+       *
+       * create WrappedRange from paraBookmark
+       *
+       * @param {Object} bookmark
+       * @param {Node[]} paras
+       * @return {WrappedRange}
+       */
+      createFromParaBookmark: function (bookmark, paras) {
+        var so = bookmark.s.offset;
+        var eo = bookmark.e.offset;
+        var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path);
+        var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path);
+
+        return new WrappedRange(sc, so, ec, eo);
+      }
+    };
+  })();
+
+  /**
+   * @class defaults 
+   * 
+   * @singleton
+   */
+  var defaults = {
+    /** @property */
+    version: '0.6.16',
+
+    /**
+     * 
+     * for event options, reference to EventHandler.attach
+     * 
+     * @property {Object} options 
+     * @property {String/Number} [options.width=null] set editor width 
+     * @property {String/Number} [options.height=null] set editor height, ex) 300
+     * @property {String/Number} options.minHeight set minimum height of editor
+     * @property {String/Number} options.maxHeight
+     * @property {String/Number} options.focus 
+     * @property {Number} options.tabsize 
+     * @property {Boolean} options.styleWithSpan
+     * @property {Object} options.codemirror
+     * @property {Object} [options.codemirror.mode='text/html']
+     * @property {Object} [options.codemirror.htmlMode=true]
+     * @property {Object} [options.codemirror.lineNumbers=true]
+     * @property {String} [options.lang=en-US] language 'en-US', 'ko-KR', ...
+     * @property {String} [options.direction=null] text direction, ex) 'rtl'
+     * @property {Array} [options.toolbar]
+     * @property {Boolean} [options.airMode=false]
+     * @property {Array} [options.airPopover]
+     * @property {Fucntion} [options.onInit] initialize
+     * @property {Fucntion} [options.onsubmit]
+     */
+    options: {
+      width: null,                  // set editor width
+      height: null,                 // set editor height, ex) 300
+
+      minHeight: null,              // set minimum height of editor
+      maxHeight: null,              // set maximum height of editor
+
+      focus: false,                 // set focus to editable area after initializing summernote
+
+      tabsize: 4,                   // size of tab ex) 2 or 4
+      styleWithSpan: true,          // style with span (Chrome and FF only)
+
+      disableLinkTarget: false,     // hide link Target Checkbox
+      disableDragAndDrop: false,    // disable drag and drop event
+      disableResizeEditor: false,   // disable resizing editor
+      disableResizeImage: false,    // disable resizing image
+
+      shortcuts: true,              // enable keyboard shortcuts
+
+      textareaAutoSync: true,       // enable textarea auto sync
+
+      placeholder: false,           // enable placeholder text
+      prettifyHtml: true,           // enable prettifying html while toggling codeview
+
+      iconPrefix: 'fa fa-',         // prefix for css icon classes
+
+      icons: {
+        font: {
+          bold: 'bold',
+          italic: 'italic',
+          underline: 'underline',
+          clear: 'eraser',
+          height: 'text-height',
+          strikethrough: 'strikethrough',
+          superscript: 'superscript',
+          subscript: 'subscript'
+        },
+        image: {
+          image: 'picture-o',
+          floatLeft: 'align-left',
+          floatRight: 'align-right',
+          floatNone: 'align-justify',
+          shapeRounded: 'square',
+          shapeCircle: 'circle-o',
+          shapeThumbnail: 'picture-o',
+          shapeNone: 'times',
+          remove: 'trash-o'
+        },
+        link: {
+          link: 'link',
+          unlink: 'unlink',
+          edit: 'edit'
+        },
+        table: {
+          table: 'table'
+        },
+        hr: {
+          insert: 'minus'
+        },
+        style: {
+          style: 'magic'
+        },
+        lists: {
+          unordered: 'list-ul',
+          ordered: 'list-ol'
+        },
+        options: {
+          help: 'question',
+          fullscreen: 'arrows-alt',
+          codeview: 'code'
+        },
+        paragraph: {
+          paragraph: 'align-left',
+          outdent: 'outdent',
+          indent: 'indent',
+          left: 'align-left',
+          center: 'align-center',
+          right: 'align-right',
+          justify: 'align-justify'
+        },
+        color: {
+          recent: 'font'
+        },
+        history: {
+          undo: 'undo',
+          redo: 'repeat'
+        },
+        misc: {
+          check: 'check'
+        }
+      },
+
+      dialogsInBody: false,          // false will add dialogs into editor
+
+      codemirror: {                 // codemirror options
+        mode: 'text/html',
+        htmlMode: true,
+        lineNumbers: true
+      },
+
+      // language
+      lang: 'en-US',                // language 'en-US', 'ko-KR', ...
+      direction: null,              // text direction, ex) 'rtl'
+
+      // toolbar
+      toolbar: [
+        ['style', ['style']],
+        ['font', ['bold', 'italic', 'underline', 'clear']],
+        // ['font', ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'clear']],
+        ['fontname', ['fontname']],
+        ['fontsize', ['fontsize']],
+        ['color', ['color']],
+        ['para', ['ul', 'ol', 'paragraph']],
+        ['height', ['height']],
+        ['table', ['table']],
+        ['insert', ['link', 'picture', 'hr']],
+        ['view', ['fullscreen', 'codeview']],
+        ['help', ['help']]
+      ],
+
+      plugin : { },
+
+      // air mode: inline editor
+      airMode: false,
+      // airPopover: [
+      //   ['style', ['style']],
+      //   ['font', ['bold', 'italic', 'underline', 'clear']],
+      //   ['fontname', ['fontname']],
+      //   ['color', ['color']],
+      //   ['para', ['ul', 'ol', 'paragraph']],
+      //   ['height', ['height']],
+      //   ['table', ['table']],
+      //   ['insert', ['link', 'picture']],
+      //   ['help', ['help']]
+      // ],
+      airPopover: [
+        ['color', ['color']],
+        ['font', ['bold', 'underline', 'clear']],
+        ['para', ['ul', 'paragraph']],
+        ['table', ['table']],
+        ['insert', ['link', 'picture']]
+      ],
+
+      // style tag
+      styleTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
+
+      // default fontName
+      defaultFontName: 'Helvetica Neue',
+
+      // fontName
+      fontNames: [
+        'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New',
+        'Helvetica Neue', 'Helvetica', 'Impact', 'Lucida Grande',
+        'Tahoma', 'Times New Roman', 'Verdana'
+      ],
+      fontNamesIgnoreCheck: [],
+
+      fontSizes: ['8', '9', '10', '11', '12', '14', '18', '24', '36'],
+
+      // pallete colors(n x n)
+      colors: [
+        ['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'],
+        ['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'],
+        ['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'],
+        ['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'],
+        ['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'],
+        ['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'],
+        ['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'],
+        ['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031']
+      ],
+
+      // lineHeight
+      lineHeights: ['1.0', '1.2', '1.4', '1.5', '1.6', '1.8', '2.0', '3.0'],
+
+      // insertTable max size
+      insertTableMaxSize: {
+        col: 10,
+        row: 10
+      },
+
+      // image
+      maximumImageFileSize: null, // size in bytes, null = no limit
+
+      // callbacks
+      oninit: null,             // initialize
+      onfocus: null,            // editable has focus
+      onblur: null,             // editable out of focus
+      onenter: null,            // enter key pressed
+      onkeyup: null,            // keyup
+      onkeydown: null,          // keydown
+      onImageUpload: null,      // imageUpload
+      onImageUploadError: null, // imageUploadError
+      onMediaDelete: null,      // media delete
+      onToolbarClick: null,
+      onsubmit: null,
+
+      /**
+       * manipulate link address when user create link
+       * @param {String} sLinkUrl
+       * @return {String}
+       */
+      onCreateLink: function (sLinkUrl) {
+        if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
+          sLinkUrl =  'mailto:' + sLinkUrl;
+        }
+
+        return sLinkUrl;
+      },
+
+      keyMap: {
+        pc: {
+          'ENTER': 'insertParagraph',
+          'CTRL+Z': 'undo',
+          'CTRL+Y': 'redo',
+          'TAB': 'tab',
+          'SHIFT+TAB': 'untab',
+          'CTRL+B': 'bold',
+          'CTRL+I': 'italic',
+          'CTRL+U': 'underline',
+          'CTRL+SHIFT+S': 'strikethrough',
+          'CTRL+BACKSLASH': 'removeFormat',
+          'CTRL+SHIFT+L': 'justifyLeft',
+          'CTRL+SHIFT+E': 'justifyCenter',
+          'CTRL+SHIFT+R': 'justifyRight',
+          'CTRL+SHIFT+J': 'justifyFull',
+          'CTRL+SHIFT+NUM7': 'insertUnorderedList',
+          'CTRL+SHIFT+NUM8': 'insertOrderedList',
+          'CTRL+LEFTBRACKET': 'outdent',
+          'CTRL+RIGHTBRACKET': 'indent',
+          'CTRL+NUM0': 'formatPara',
+          'CTRL+NUM1': 'formatH1',
+          'CTRL+NUM2': 'formatH2',
+          'CTRL+NUM3': 'formatH3',
+          'CTRL+NUM4': 'formatH4',
+          'CTRL+NUM5': 'formatH5',
+          'CTRL+NUM6': 'formatH6',
+          'CTRL+ENTER': 'insertHorizontalRule',
+          'CTRL+K': 'showLinkDialog'
+        },
+
+        mac: {
+          'ENTER': 'insertParagraph',
+          'CMD+Z': 'undo',
+          'CMD+SHIFT+Z': 'redo',
+          'TAB': 'tab',
+          'SHIFT+TAB': 'untab',
+          'CMD+B': 'bold',
+          'CMD+I': 'italic',
+          'CMD+U': 'underline',
+          'CMD+SHIFT+S': 'strikethrough',
+          'CMD+BACKSLASH': 'removeFormat',
+          'CMD+SHIFT+L': 'justifyLeft',
+          'CMD+SHIFT+E': 'justifyCenter',
+          'CMD+SHIFT+R': 'justifyRight',
+          'CMD+SHIFT+J': 'justifyFull',
+          'CMD+SHIFT+NUM7': 'insertUnorderedList',
+          'CMD+SHIFT+NUM8': 'insertOrderedList',
+          'CMD+LEFTBRACKET': 'outdent',
+          'CMD+RIGHTBRACKET': 'indent',
+          'CMD+NUM0': 'formatPara',
+          'CMD+NUM1': 'formatH1',
+          'CMD+NUM2': 'formatH2',
+          'CMD+NUM3': 'formatH3',
+          'CMD+NUM4': 'formatH4',
+          'CMD+NUM5': 'formatH5',
+          'CMD+NUM6': 'formatH6',
+          'CMD+ENTER': 'insertHorizontalRule',
+          'CMD+K': 'showLinkDialog'
+        }
+      }
+    },
+
+    // default language: en-US
+    lang: {
+      'en-US': {
+        font: {
+          bold: 'Bold',
+          italic: 'Italic',
+          underline: 'Underline',
+          clear: 'Remove Font Style',
+          height: 'Line Height',
+          name: 'Font Family',
+          strikethrough: 'Strikethrough',
+          subscript: 'Subscript',
+          superscript: 'Superscript',
+          size: 'Font Size'
+        },
+        image: {
+          image: 'Picture',
+          insert: 'Insert Image',
+          resizeFull: 'Resize Full',
+          resizeHalf: 'Resize Half',
+          resizeQuarter: 'Resize Quarter',
+          floatLeft: 'Float Left',
+          floatRight: 'Float Right',
+          floatNone: 'Float None',
+          shapeRounded: 'Shape: Rounded',
+          shapeCircle: 'Shape: Circle',
+          shapeThumbnail: 'Shape: Thumbnail',
+          shapeNone: 'Shape: None',
+          dragImageHere: 'Drag image or text here',
+          dropImage: 'Drop image or Text',
+          selectFromFiles: 'Select from files',
+          maximumFileSize: 'Maximum file size',
+          maximumFileSizeError: 'Maximum file size exceeded.',
+          url: 'Image URL',
+          remove: 'Remove Image'
+        },
+        link: {
+          link: 'Link',
+          insert: 'Insert Link',
+          unlink: 'Unlink',
+          edit: 'Edit',
+          textToDisplay: 'Text to display',
+          url: 'To what URL should this link go?',
+          openInNewWindow: 'Open in new window'
+        },
+        table: {
+          table: 'Table'
+        },
+        hr: {
+          insert: 'Insert Horizontal Rule'
+        },
+        style: {
+          style: 'Style',
+          normal: 'Normal',
+          blockquote: 'Quote',
+          pre: 'Code',
+          h1: 'Header 1',
+          h2: 'Header 2',
+          h3: 'Header 3',
+          h4: 'Header 4',
+          h5: 'Header 5',
+          h6: 'Header 6'
+        },
+        lists: {
+          unordered: 'Unordered list',
+          ordered: 'Ordered list'
+        },
+        options: {
+          help: 'Help',
+          fullscreen: 'Full Screen',
+          codeview: 'Code View'
+        },
+        paragraph: {
+          paragraph: 'Paragraph',
+          outdent: 'Outdent',
+          indent: 'Indent',
+          left: 'Align left',
+          center: 'Align center',
+          right: 'Align right',
+          justify: 'Justify full'
+        },
+        color: {
+          recent: 'Recent Color',
+          more: 'More Color',
+          background: 'Background Color',
+          foreground: 'Foreground Color',
+          transparent: 'Transparent',
+          setTransparent: 'Set transparent',
+          reset: 'Reset',
+          resetToDefault: 'Reset to default'
+        },
+        shortcut: {
+          shortcuts: 'Keyboard shortcuts',
+          close: 'Close',
+          textFormatting: 'Text formatting',
+          action: 'Action',
+          paragraphFormatting: 'Paragraph formatting',
+          documentStyle: 'Document Style',
+          extraKeys: 'Extra keys'
+        },
+        history: {
+          undo: 'Undo',
+          redo: 'Redo'
+        }
+      }
+    }
+  };
+
+  /**
+   * @class core.async
+   *
+   * Async functions which returns `Promise`
+   *
+   * @singleton
+   * @alternateClassName async
+   */
+  var async = (function () {
+    /**
+     * @method readFileAsDataURL
+     *
+     * read contents of file as representing URL
+     *
+     * @param {File} file
+     * @return {Promise} - then: sDataUrl
+     */
+    var readFileAsDataURL = function (file) {
+      return $.Deferred(function (deferred) {
+        $.extend(new FileReader(), {
+          onload: function (e) {
+            var sDataURL = e.target.result;
+            deferred.resolve(sDataURL);
+          },
+          onerror: function () {
+            deferred.reject(this);
+          }
+        }).readAsDataURL(file);
+      }).promise();
+    };
+  
+    /**
+     * @method createImage
+     *
+     * create `<image>` from url string
+     *
+     * @param {String} sUrl
+     * @param {String} filename
+     * @return {Promise} - then: $image
+     */
+    var createImage = function (sUrl, filename) {
+      return $.Deferred(function (deferred) {
+        var $img = $('<img>');
+
+        $img.one('load', function () {
+          $img.off('error abort');
+          deferred.resolve($img);
+        }).one('error abort', function () {
+          $img.off('load').detach();
+          deferred.reject($img);
+        }).css({
+          display: 'none'
+        }).appendTo(document.body).attr({
+          'src': sUrl,
+          'data-filename': filename
+        });
+      }).promise();
+    };
+
+    return {
+      readFileAsDataURL: readFileAsDataURL,
+      createImage: createImage
+    };
+  })();
+
+  /**
+   * @class core.key
+   *
+   * Object for keycodes.
+   *
+   * @singleton
+   * @alternateClassName key
+   */
+  var key = (function () {
+    var keyMap = {
+      'BACKSPACE': 8,
+      'TAB': 9,
+      'ENTER': 13,
+      'SPACE': 32,
+
+      // Number: 0-9
+      'NUM0': 48,
+      'NUM1': 49,
+      'NUM2': 50,
+      'NUM3': 51,
+      'NUM4': 52,
+      'NUM5': 53,
+      'NUM6': 54,
+      'NUM7': 55,
+      'NUM8': 56,
+
+      // Alphabet: a-z
+      'B': 66,
+      'E': 69,
+      'I': 73,
+      'J': 74,
+      'K': 75,
+      'L': 76,
+      'R': 82,
+      'S': 83,
+      'U': 85,
+      'V': 86,
+      'Y': 89,
+      'Z': 90,
+
+      'SLASH': 191,
+      'LEFTBRACKET': 219,
+      'BACKSLASH': 220,
+      'RIGHTBRACKET': 221
+    };
+
+    return {
+      /**
+       * @method isEdit
+       *
+       * @param {Number} keyCode
+       * @return {Boolean}
+       */
+      isEdit: function (keyCode) {
+        return list.contains([8, 9, 13, 32], keyCode);
+      },
+      /**
+       * @method isMove
+       *
+       * @param {Number} keyCode
+       * @return {Boolean}
+       */
+      isMove: function (keyCode) {
+        return list.contains([37, 38, 39, 40], keyCode);
+      },
+      /**
+       * @property {Object} nameFromCode
+       * @property {String} nameFromCode.8 "BACKSPACE"
+       */
+      nameFromCode: func.invertObject(keyMap),
+      code: keyMap
+    };
+  })();
+
+  /**
+   * @class editing.History
+   *
+   * Editor History
+   *
+   */
+  var History = function ($editable) {
+    var stack = [], stackOffset = -1;
+    var editable = $editable[0];
+
+    var makeSnapshot = function () {
+      var rng = range.create();
+      var emptyBookmark = {s: {path: [], offset: 0}, e: {path: [], offset: 0}};
+
+      return {
+        contents: $editable.html(),
+        bookmark: (rng ? rng.bookmark(editable) : emptyBookmark)
+      };
+    };
+
+    var applySnapshot = function (snapshot) {
+      if (snapshot.contents !== null) {
+        $editable.html(snapshot.contents);
+      }
+      if (snapshot.bookmark !== null) {
+        range.createFromBookmark(editable, snapshot.bookmark).select();
+      }
+    };
+
+    /**
+     * undo
+     */
+    this.undo = function () {
+      // Create snap shot if not yet recorded
+      if ($editable.html() !== stack[stackOffset].contents) {
+        this.recordUndo();
+      }
+
+      if (0 < stackOffset) {
+        stackOffset--;
+        applySnapshot(stack[stackOffset]);
+      }
+    };
+
+    /**
+     * redo
+     */
+    this.redo = function () {
+      if (stack.length - 1 > stackOffset) {
+        stackOffset++;
+        applySnapshot(stack[stackOffset]);
+      }
+    };
+
+    /**
+     * recorded undo
+     */
+    this.recordUndo = function () {
+      stackOffset++;
+
+      // Wash out stack after stackOffset
+      if (stack.length > stackOffset) {
+        stack = stack.slice(0, stackOffset);
+      }
+
+      // Create new snapshot and push it to the end
+      stack.push(makeSnapshot());
+    };
+
+    // Create first undo stack
+    this.recordUndo();
+  };
+
+  /**
+   * @class editing.Style
+   *
+   * Style
+   *
+   */
+  var Style = function () {
+    /**
+     * @method jQueryCSS
+     *
+     * [workaround] for old jQuery
+     * passing an array of style properties to .css()
+     * will result in an object of property-value pairs.
+     * (compability with version < 1.9)
+     *
+     * @private
+     * @param  {jQuery} $obj
+     * @param  {Array} propertyNames - An array of one or more CSS properties.
+     * @return {Object}
+     */
+    var jQueryCSS = function ($obj, propertyNames) {
+      if (agent.jqueryVersion < 1.9) {
+        var result = {};
+        $.each(propertyNames, function (idx, propertyName) {
+          result[propertyName] = $obj.css(propertyName);
+        });
+        return result;
+      }
+      return $obj.css.call($obj, propertyNames);
+    };
+
+    /**
+     * returns style object from node
+     *
+     * @param {jQuery} $node
+     * @return {Object}
+     */
+    this.fromNode = function ($node) {
+      var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height'];
+      var styleInfo = jQueryCSS($node, properties) || {};
+      styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10);
+      return styleInfo;
+    };
+
+    /**
+     * paragraph level style
+     *
+     * @param {WrappedRange} rng
+     * @param {Object} styleInfo
+     */
+    this.stylePara = function (rng, styleInfo) {
+      $.each(rng.nodes(dom.isPara, {
+        includeAncestor: true
+      }), function (idx, para) {
+        $(para).css(styleInfo);
+      });
+    };
+
+    /**
+     * insert and returns styleNodes on range.
+     *
+     * @param {WrappedRange} rng
+     * @param {Object} [options] - options for styleNodes
+     * @param {String} [options.nodeName] - default: `SPAN`
+     * @param {Boolean} [options.expandClosestSibling] - default: `false`
+     * @param {Boolean} [options.onlyPartialContains] - default: `false`
+     * @return {Node[]}
+     */
+    this.styleNodes = function (rng, options) {
+      rng = rng.splitText();
+
+      var nodeName = options && options.nodeName || 'SPAN';
+      var expandClosestSibling = !!(options && options.expandClosestSibling);
+      var onlyPartialContains = !!(options && options.onlyPartialContains);
+
+      if (rng.isCollapsed()) {
+        return [rng.insertNode(dom.create(nodeName))];
+      }
+
+      var pred = dom.makePredByNodeName(nodeName);
+      var nodes = rng.nodes(dom.isText, {
+        fullyContains: true
+      }).map(function (text) {
+        return dom.singleChildAncestor(text, pred) || dom.wrap(text, nodeName);
+      });
+
+      if (expandClosestSibling) {
+        if (onlyPartialContains) {
+          var nodesInRange = rng.nodes();
+          // compose with partial contains predication
+          pred = func.and(pred, function (node) {
+            return list.contains(nodesInRange, node);
+          });
+        }
+
+        return nodes.map(function (node) {
+          var siblings = dom.withClosestSiblings(node, pred);
+          var head = list.head(siblings);
+          var tails = list.tail(siblings);
+          $.each(tails, function (idx, elem) {
+            dom.appendChildNodes(head, elem.childNodes);
+            dom.remove(elem);
+          });
+          return list.head(siblings);
+        });
+      } else {
+        return nodes;
+      }
+    };
+
+    /**
+     * get current style on cursor
+     *
+     * @param {WrappedRange} rng
+     * @return {Object} - object contains style properties.
+     */
+    this.current = function (rng) {
+      var $cont = $(dom.isText(rng.sc) ? rng.sc.parentNode : rng.sc);
+      var styleInfo = this.fromNode($cont);
+
+      // document.queryCommandState for toggle state
+      styleInfo['font-bold'] = document.queryCommandState('bold') ? 'bold' : 'normal';
+      styleInfo['font-italic'] = document.queryCommandState('italic') ? 'italic' : 'normal';
+      styleInfo['font-underline'] = document.queryCommandState('underline') ? 'underline' : 'normal';
+      styleInfo['font-strikethrough'] = document.queryCommandState('strikeThrough') ? 'strikethrough' : 'normal';
+      styleInfo['font-superscript'] = document.queryCommandState('superscript') ? 'superscript' : 'normal';
+      styleInfo['font-subscript'] = document.queryCommandState('subscript') ? 'subscript' : 'normal';
+
+      // list-style-type to list-style(unordered, ordered)
+      if (!rng.isOnList()) {
+        styleInfo['list-style'] = 'none';
+      } else {
+        var aOrderedType = ['circle', 'disc', 'disc-leading-zero', 'square'];
+        var isUnordered = $.inArray(styleInfo['list-style-type'], aOrderedType) > -1;
+        styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered';
+      }
+
+      var para = dom.ancestor(rng.sc, dom.isPara);
+      if (para && para.style['line-height']) {
+        styleInfo['line-height'] = para.style.lineHeight;
+      } else {
+        var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10);
+        styleInfo['line-height'] = lineHeight.toFixed(1);
+      }
+
+      styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor);
+      styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable);
+      styleInfo.range = rng;
+
+      return styleInfo;
+    };
+  };
+
+
+  /**
+   * @class editing.Bullet
+   *
+   * @alternateClassName Bullet
+   */
+  var Bullet = function () {
+    /**
+     * @method insertOrderedList
+     *
+     * toggle ordered list
+     *
+     * @type command
+     */
+    this.insertOrderedList = function () {
+      this.toggleList('OL');
+    };
+
+    /**
+     * @method insertUnorderedList
+     *
+     * toggle unordered list
+     *
+     * @type command
+     */
+    this.insertUnorderedList = function () {
+      this.toggleList('UL');
+    };
+
+    /**
+     * @method indent
+     *
+     * indent
+     *
+     * @type command
+     */
+    this.indent = function () {
+      var self = this;
+      var rng = range.create().wrapBodyInlineWithPara();
+
+      var paras = rng.nodes(dom.isPara, { includeAncestor: true });
+      var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
+
+      $.each(clustereds, function (idx, paras) {
+        var head = list.head(paras);
+        if (dom.isLi(head)) {
+          self.wrapList(paras, head.parentNode.nodeName);
+        } else {
+          $.each(paras, function (idx, para) {
+            $(para).css('marginLeft', function (idx, val) {
+              return (parseInt(val, 10) || 0) + 25;
+            });
+          });
+        }
+      });
+
+      rng.select();
+    };
+
+    /**
+     * @method outdent
+     *
+     * outdent
+     *
+     * @type command
+     */
+    this.outdent = function () {
+      var self = this;
+      var rng = range.create().wrapBodyInlineWithPara();
+
+      var paras = rng.nodes(dom.isPara, { includeAncestor: true });
+      var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
+
+      $.each(clustereds, function (idx, paras) {
+        var head = list.head(paras);
+        if (dom.isLi(head)) {
+          self.releaseList([paras]);
+        } else {
+          $.each(paras, function (idx, para) {
+            $(para).css('marginLeft', function (idx, val) {
+              val = (parseInt(val, 10) || 0);
+              return val > 25 ? val - 25 : '';
+            });
+          });
+        }
+      });
+
+      rng.select();
+    };
+
+    /**
+     * @method toggleList
+     *
+     * toggle list
+     *
+     * @param {String} listName - OL or UL
+     */
+    this.toggleList = function (listName) {
+      var self = this;
+      var rng = range.create().wrapBodyInlineWithPara();
+
+      var paras = rng.nodes(dom.isPara, { includeAncestor: true });
+      var bookmark = rng.paraBookmark(paras);
+      var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
+
+      // paragraph to list
+      if (list.find(paras, dom.isPurePara)) {
+        var wrappedParas = [];
+        $.each(clustereds, function (idx, paras) {
+          wrappedParas = wrappedParas.concat(self.wrapList(paras, listName));
+        });
+        paras = wrappedParas;
+      // list to paragraph or change list style
+      } else {
+        var diffLists = rng.nodes(dom.isList, {
+          includeAncestor: true
+        }).filter(function (listNode) {
+          return !$.nodeName(listNode, listName);
+        });
+
+        if (diffLists.length) {
+          $.each(diffLists, function (idx, listNode) {
+            dom.replace(listNode, listName);
+          });
+        } else {
+          paras = this.releaseList(clustereds, true);
+        }
+      }
+
+      range.createFromParaBookmark(bookmark, paras).select();
+    };
+
+    /**
+     * @method wrapList
+     *
+     * @param {Node[]} paras
+     * @param {String} listName
+     * @return {Node[]}
+     */
+    this.wrapList = function (paras, listName) {
+      var head = list.head(paras);
+      var last = list.last(paras);
+
+      var prevList = dom.isList(head.previousSibling) && head.previousSibling;
+      var nextList = dom.isList(last.nextSibling) && last.nextSibling;
+
+      var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last);
+
+      // P to LI
+      paras = paras.map(function (para) {
+        return dom.isPurePara(para) ? dom.replace(para, 'LI') : para;
+      });
+
+      // append to list(<ul>, <ol>)
+      dom.appendChildNodes(listNode, paras);
+
+      if (nextList) {
+        dom.appendChildNodes(listNode, list.from(nextList.childNodes));
+        dom.remove(nextList);
+      }
+
+      return paras;
+    };
+
+    /**
+     * @method releaseList
+     *
+     * @param {Array[]} clustereds
+     * @param {Boolean} isEscapseToBody
+     * @return {Node[]}
+     */
+    this.releaseList = function (clustereds, isEscapseToBody) {
+      var releasedParas = [];
+
+      $.each(clustereds, function (idx, paras) {
+        var head = list.head(paras);
+        var last = list.last(paras);
+
+        var headList = isEscapseToBody ? dom.lastAncestor(head, dom.isList) :
+                                         head.parentNode;
+        var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, {
+          node: last.parentNode,
+          offset: dom.position(last) + 1
+        }, {
+          isSkipPaddingBlankHTML: true
+        }) : null;
+
+        var middleList = dom.splitTree(headList, {
+          node: head.parentNode,
+          offset: dom.position(head)
+        }, {
+          isSkipPaddingBlankHTML: true
+        });
+
+        paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) :
+                                  list.from(middleList.childNodes).filter(dom.isLi);
+
+        // LI to P
+        if (isEscapseToBody || !dom.isList(headList.parentNode)) {
+          paras = paras.map(function (para) {
+            return dom.replace(para, 'P');
+          });
+        }
+
+        $.each(list.from(paras).reverse(), function (idx, para) {
+          dom.insertAfter(para, headList);
+        });
+
+        // remove empty lists
+        var rootLists = list.compact([headList, middleList, lastList]);
+        $.each(rootLists, function (idx, rootList) {
+          var listNodes = [rootList].concat(dom.listDescendant(rootList, dom.isList));
+          $.each(listNodes.reverse(), function (idx, listNode) {
+            if (!dom.nodeLength(listNode)) {
+              dom.remove(listNode, true);
+            }
+          });
+        });
+
+        releasedParas = releasedParas.concat(paras);
+      });
+
+      return releasedParas;
+    };
+  };
+
+
+  /**
+   * @class editing.Typing
+   *
+   * Typing
+   *
+   */
+  var Typing = function () {
+
+    // a Bullet instance to toggle lists off
+    var bullet = new Bullet();
+
+    /**
+     * insert tab
+     *
+     * @param {jQuery} $editable
+     * @param {WrappedRange} rng
+     * @param {Number} tabsize
+     */
+    this.insertTab = function ($editable, rng, tabsize) {
+      var tab = dom.createText(new Array(tabsize + 1).join(dom.NBSP_CHAR));
+      rng = rng.deleteContents();
+      rng.insertNode(tab, true);
+
+      rng = range.create(tab, tabsize);
+      rng.select();
+    };
+
+    /**
+     * insert paragraph
+     */
+    this.insertParagraph = function () {
+      var rng = range.create();
+
+      // deleteContents on range.
+      rng = rng.deleteContents();
+
+      // Wrap range if it needs to be wrapped by paragraph
+      rng = rng.wrapBodyInlineWithPara();
+
+      // finding paragraph
+      var splitRoot = dom.ancestor(rng.sc, dom.isPara);
+
+      var nextPara;
+      // on paragraph: split paragraph
+      if (splitRoot) {
+        // if it is an empty line with li
+        if (dom.isEmpty(splitRoot) && dom.isLi(splitRoot)) {
+          // disable UL/OL and escape!
+          bullet.toggleList(splitRoot.parentNode.nodeName);
+          return;
+        // if new line has content (not a line break)
+        } else {
+          nextPara = dom.splitTree(splitRoot, rng.getStartPoint());
+
+          var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor);
+          emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor));
+
+          $.each(emptyAnchors, function (idx, anchor) {
+            dom.remove(anchor);
+          });
+        }
+      // no paragraph: insert empty paragraph
+      } else {
+        var next = rng.sc.childNodes[rng.so];
+        nextPara = $(dom.emptyPara)[0];
+        if (next) {
+          rng.sc.insertBefore(nextPara, next);
+        } else {
+          rng.sc.appendChild(nextPara);
+        }
+      }
+
+      range.create(nextPara, 0).normalize().select();
+
+    };
+
+  };
+
+  /**
+   * @class editing.Table
+   *
+   * Table
+   *
+   */
+  var Table = function () {
+    /**
+     * handle tab key
+     *
+     * @param {WrappedRange} rng
+     * @param {Boolean} isShift
+     */
+    this.tab = function (rng, isShift) {
+      var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
+      var table = dom.ancestor(cell, dom.isTable);
+      var cells = dom.listDescendant(table, dom.isCell);
+
+      var nextCell = list[isShift ? 'prev' : 'next'](cells, cell);
+      if (nextCell) {
+        range.create(nextCell, 0).select();
+      }
+    };
+
+    /**
+     * create empty table element
+     *
+     * @param {Number} rowCount
+     * @param {Number} colCount
+     * @return {Node}
+     */
+    this.createTable = function (colCount, rowCount) {
+      var tds = [], tdHTML;
+      for (var idxCol = 0; idxCol < colCount; idxCol++) {
+        tds.push('<td>' + dom.blank + '</td>');
+      }
+      tdHTML = tds.join('');
+
+      var trs = [], trHTML;
+      for (var idxRow = 0; idxRow < rowCount; idxRow++) {
+        trs.push('<tr>' + tdHTML + '</tr>');
+      }
+      trHTML = trs.join('');
+      return $('<table class="table table-bordered">' + trHTML + '</table>')[0];
+    };
+  };
+
+
+  var KEY_BOGUS = 'bogus';
+
+  /**
+   * @class editing.Editor
+   *
+   * Editor
+   *
+   */
+  var Editor = function (handler) {
+
+    var self = this;
+    var style = new Style();
+    var table = new Table();
+    var typing = new Typing();
+    var bullet = new Bullet();
+
+    /**
+     * @method createRange
+     *
+     * create range
+     *
+     * @param {jQuery} $editable
+     * @return {WrappedRange}
+     */
+    this.createRange = function ($editable) {
+      this.focus($editable);
+      return range.create();
+    };
+
+    /**
+     * @method saveRange
+     *
+     * save current range
+     *
+     * @param {jQuery} $editable
+     * @param {Boolean} [thenCollapse=false]
+     */
+    this.saveRange = function ($editable, thenCollapse) {
+      this.focus($editable);
+      $editable.data('range', range.create());
+      if (thenCollapse) {
+        range.create().collapse().select();
+      }
+    };
+
+    /**
+     * @method saveRange
+     *
+     * save current node list to $editable.data('childNodes')
+     *
+     * @param {jQuery} $editable
+     */
+    this.saveNode = function ($editable) {
+      // copy child node reference
+      var copy = [];
+      for (var key  = 0, len = $editable[0].childNodes.length; key < len; key++) {
+        copy.push($editable[0].childNodes[key]);
+      }
+      $editable.data('childNodes', copy);
+    };
+
+    /**
+     * @method restoreRange
+     *
+     * restore lately range
+     *
+     * @param {jQuery} $editable
+     */
+    this.restoreRange = function ($editable) {
+      var rng = $editable.data('range');
+      if (rng) {
+        rng.select();
+        this.focus($editable);
+      }
+    };
+
+    /**
+     * @method restoreNode
+     *
+     * restore lately node list
+     *
+     * @param {jQuery} $editable
+     */
+    this.restoreNode = function ($editable) {
+      $editable.html('');
+      var child = $editable.data('childNodes');
+      for (var index = 0, len = child.length; index < len; index++) {
+        $editable[0].appendChild(child[index]);
+      }
+    };
+
+    /**
+     * @method currentStyle
+     *
+     * current style
+     *
+     * @param {Node} target
+     * @return {Object|Boolean} unfocus
+     */
+    this.currentStyle = function (target) {
+      var rng = range.create();
+      var styleInfo =  rng && rng.isOnEditable() ? style.current(rng.normalize()) : {};
+      if (dom.isImg(target)) {
+        styleInfo.image = target;
+      }
+      return styleInfo;
+    };
+
+    /**
+     * style from node
+     *
+     * @param {jQuery} $node
+     * @return {Object}
+     */
+    this.styleFromNode = function ($node) {
+      return style.fromNode($node);
+    };
+
+    var triggerOnBeforeChange = function ($editable) {
+      var $holder = dom.makeLayoutInfo($editable).holder();
+      handler.bindCustomEvent(
+        $holder, $editable.data('callbacks'), 'before.command'
+      )($editable.html(), $editable);
+    };
+
+    var triggerOnChange = function ($editable) {
+      var $holder = dom.makeLayoutInfo($editable).holder();
+      handler.bindCustomEvent(
+        $holder, $editable.data('callbacks'), 'change'
+      )($editable.html(), $editable);
+    };
+
+    /**
+     * @method undo
+     * undo
+     * @param {jQuery} $editable
+     */
+    this.undo = function ($editable) {
+      triggerOnBeforeChange($editable);
+      $editable.data('NoteHistory').undo();
+      triggerOnChange($editable);
+    };
+
+    /**
+     * @method redo
+     * redo
+     * @param {jQuery} $editable
+     */
+    this.redo = function ($editable) {
+      triggerOnBeforeChange($editable);
+      $editable.data('NoteHistory').redo();
+      triggerOnChange($editable);
+    };
+
+    /**
+     * @method beforeCommand
+     * before command
+     * @param {jQuery} $editable
+     */
+    var beforeCommand = this.beforeCommand = function ($editable) {
+      triggerOnBeforeChange($editable);
+      // keep focus on editable before command execution
+      self.focus($editable);
+    };
+
+    /**
+     * @method afterCommand
+     * after command
+     * @param {jQuery} $editable
+     * @param {Boolean} isPreventTrigger
+     */
+    var afterCommand = this.afterCommand = function ($editable, isPreventTrigger) {
+      $editable.data('NoteHistory').recordUndo();
+      if (!isPreventTrigger) {
+        triggerOnChange($editable);
+      }
+    };
+
+    /**
+     * @method bold
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method italic
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method underline
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method strikethrough
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method formatBlock
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method superscript
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method subscript
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method justifyLeft
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method justifyCenter
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method justifyRight
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method justifyFull
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method formatBlock
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method removeFormat
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method backColor
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method foreColor
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method insertHorizontalRule
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /**
+     * @method fontName
+     *
+     * change font name
+     *
+     * @param {jQuery} $editable
+     * @param {Mixed} value
+     */
+
+    /* jshint ignore:start */
+    // native commands(with execCommand), generate function for execCommand
+    var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript',
+                    'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull',
+                    'formatBlock', 'removeFormat',
+                    'backColor', 'foreColor', 'fontName'];
+
+    for (var idx = 0, len = commands.length; idx < len; idx ++) {
+      this[commands[idx]] = (function (sCmd) {
+        return function ($editable, value) {
+          beforeCommand($editable);
+
+          document.execCommand(sCmd, false, value);
+
+          afterCommand($editable, true);
+        };
+      })(commands[idx]);
+    }
+    /* jshint ignore:end */
+
+    /**
+     * @method tab
+     *
+     * handle tab key
+     *
+     * @param {jQuery} $editable
+     * @param {Object} options
+     */
+    this.tab = function ($editable, options) {
+      var rng = this.createRange($editable);
+      if (rng.isCollapsed() && rng.isOnCell()) {
+        table.tab(rng);
+      } else {
+        beforeCommand($editable);
+        typing.insertTab($editable, rng, options.tabsize);
+        afterCommand($editable);
+      }
+    };
+
+    /**
+     * @method untab
+     *
+     * handle shift+tab key
+     *
+     */
+    this.untab = function ($editable) {
+      var rng = this.createRange($editable);
+      if (rng.isCollapsed() && rng.isOnCell()) {
+        table.tab(rng, true);
+      }
+    };
+
+    /**
+     * @method insertParagraph
+     *
+     * insert paragraph
+     *
+     * @param {Node} $editable
+     */
+    this.insertParagraph = function ($editable) {
+      beforeCommand($editable);
+      typing.insertParagraph($editable);
+      afterCommand($editable);
+    };
+
+    /**
+     * @method insertOrderedList
+     *
+     * @param {jQuery} $editable
+     */
+    this.insertOrderedList = function ($editable) {
+      beforeCommand($editable);
+      bullet.insertOrderedList($editable);
+      afterCommand($editable);
+    };
+
+    /**
+     * @param {jQuery} $editable
+     */
+    this.insertUnorderedList = function ($editable) {
+      beforeCommand($editable);
+      bullet.insertUnorderedList($editable);
+      afterCommand($editable);
+    };
+
+    /**
+     * @param {jQuery} $editable
+     */
+    this.indent = function ($editable) {
+      beforeCommand($editable);
+      bullet.indent($editable);
+      afterCommand($editable);
+    };
+
+    /**
+     * @param {jQuery} $editable
+     */
+    this.outdent = function ($editable) {
+      beforeCommand($editable);
+      bullet.outdent($editable);
+      afterCommand($editable);
+    };
+
+    /**
+     * insert image
+     *
+     * @param {jQuery} $editable
+     * @param {String} sUrl
+     */
+    this.insertImage = function ($editable, sUrl, filename) {
+      async.createImage(sUrl, filename).then(function ($image) {
+        beforeCommand($editable);
+        $image.css({
+          display: '',
+          width: Math.min($editable.width(), $image.width())
+        });
+        range.create().insertNode($image[0]);
+        range.createFromNodeAfter($image[0]).select();
+        afterCommand($editable);
+      }).fail(function () {
+        var $holder = dom.makeLayoutInfo($editable).holder();
+        handler.bindCustomEvent(
+          $holder, $editable.data('callbacks'), 'image.upload.error'
+        )();
+      });
+    };
+
+    /**
+     * @method insertNode
+     * insert node
+     * @param {Node} $editable
+     * @param {Node} node
+     */
+    this.insertNode = function ($editable, node) {
+      beforeCommand($editable);
+      range.create().insertNode(node);
+      range.createFromNodeAfter(node).select();
+      afterCommand($editable);
+    };
+
+    /**
+     * insert text
+     * @param {Node} $editable
+     * @param {String} text
+     */
+    this.insertText = function ($editable, text) {
+      beforeCommand($editable);
+      var textNode = range.create().insertNode(dom.createText(text));
+      range.create(textNode, dom.nodeLength(textNode)).select();
+      afterCommand($editable);
+    };
+
+    /**
+     * paste HTML
+     * @param {Node} $editable
+     * @param {String} markup
+     */
+    this.pasteHTML = function ($editable, markup) {
+      beforeCommand($editable);
+      var contents = range.create().pasteHTML(markup);
+      range.createFromNodeAfter(list.last(contents)).select();
+      afterCommand($editable);
+    };
+
+    /**
+     * formatBlock
+     *
+     * @param {jQuery} $editable
+     * @param {String} tagName
+     */
+    this.formatBlock = function ($editable, tagName) {
+      beforeCommand($editable);
+      // [workaround] for MSIE, IE need `<`
+      tagName = agent.isMSIE ? '<' + tagName + '>' : tagName;
+      document.execCommand('FormatBlock', false, tagName);
+      afterCommand($editable);
+    };
+
+    this.formatPara = function ($editable) {
+      beforeCommand($editable);
+      this.formatBlock($editable, 'P');
+      afterCommand($editable);
+    };
+
+    /* jshint ignore:start */
+    for (var idx = 1; idx <= 6; idx ++) {
+      this['formatH' + idx] = function (idx) {
+        return function ($editable) {
+          this.formatBlock($editable, 'H' + idx);
+        };
+      }(idx);
+    };
+    /* jshint ignore:end */
+
+    /**
+     * fontSize
+     *
+     * @param {jQuery} $editable
+     * @param {String} value - px
+     */
+    this.fontSize = function ($editable, value) {
+      var rng = range.create();
+
+      if (rng.isCollapsed()) {
+        var spans = style.styleNodes(rng);
+        var firstSpan = list.head(spans);
+
+        $(spans).css({
+          'font-size': value + 'px'
+        });
+
+        // [workaround] added styled bogus span for style
+        //  - also bogus character needed for cursor position
+        if (firstSpan && !dom.nodeLength(firstSpan)) {
+          firstSpan.innerHTML = dom.ZERO_WIDTH_NBSP_CHAR;
+          range.createFromNodeAfter(firstSpan.firstChild).select();
+          $editable.data(KEY_BOGUS, firstSpan);
+        }
+      } else {
+        beforeCommand($editable);
+        $(style.styleNodes(rng)).css({
+          'font-size': value + 'px'
+        });
+        afterCommand($editable);
+      }
+    };
+
+    /**
+     * insert horizontal rule
+     * @param {jQuery} $editable
+     */
+    this.insertHorizontalRule = function ($editable) {
+      beforeCommand($editable);
+
+      var rng = range.create();
+      var hrNode = rng.insertNode($('<HR/>')[0]);
+      if (hrNode.nextSibling) {
+        range.create(hrNode.nextSibling, 0).normalize().select();
+      }
+
+      afterCommand($editable);
+    };
+
+    /**
+     * remove bogus node and character
+     */
+    this.removeBogus = function ($editable) {
+      var bogusNode = $editable.data(KEY_BOGUS);
+      if (!bogusNode) {
+        return;
+      }
+
+      var textNode = list.find(list.from(bogusNode.childNodes), dom.isText);
+
+      var bogusCharIdx = textNode.nodeValue.indexOf(dom.ZERO_WIDTH_NBSP_CHAR);
+      if (bogusCharIdx !== -1) {
+        textNode.deleteData(bogusCharIdx, 1);
+      }
+
+      if (dom.isEmpty(bogusNode)) {
+        dom.remove(bogusNode);
+      }
+
+      $editable.removeData(KEY_BOGUS);
+    };
+
+    /**
+     * lineHeight
+     * @param {jQuery} $editable
+     * @param {String} value
+     */
+    this.lineHeight = function ($editable, value) {
+      beforeCommand($editable);
+      style.stylePara(range.create(), {
+        lineHeight: value
+      });
+      afterCommand($editable);
+    };
+
+    /**
+     * unlink
+     *
+     * @type command
+     *
+     * @param {jQuery} $editable
+     */
+    this.unlink = function ($editable) {
+      var rng = this.createRange($editable);
+      if (rng.isOnAnchor()) {
+        var anchor = dom.ancestor(rng.sc, dom.isAnchor);
+        rng = range.createFromNode(anchor);
+        rng.select();
+
+        beforeCommand($editable);
+        document.execCommand('unlink');
+        afterCommand($editable);
+      }
+    };
+
+    /**
+     * create link (command)
+     *
+     * @param {jQuery} $editable
+     * @param {Object} linkInfo
+     * @param {Object} options
+     */
+    this.createLink = function ($editable, linkInfo, options) {
+      var linkUrl = linkInfo.url;
+      var linkText = linkInfo.text;
+      var isNewWindow = linkInfo.isNewWindow;
+      var rng = linkInfo.range || this.createRange($editable);
+      var isTextChanged = rng.toString() !== linkText;
+
+      options = options || dom.makeLayoutInfo($editable).editor().data('options');
+
+      beforeCommand($editable);
+
+      if (options.onCreateLink) {
+        linkUrl = options.onCreateLink(linkUrl);
+      }
+
+      var anchors = [];
+      if (isTextChanged) {
+        // Create a new link when text changed.
+        var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0]);
+        anchors.push(anchor);
+      } else {
+        anchors = style.styleNodes(rng, {
+          nodeName: 'A',
+          expandClosestSibling: true,
+          onlyPartialContains: true
+        });
+      }
+
+      $.each(anchors, function (idx, anchor) {
+        $(anchor).attr('href', linkUrl);
+        if (isNewWindow) {
+          $(anchor).attr('target', '_blank');
+        } else {
+          $(anchor).removeAttr('target');
+        }
+      });
+
+      var startRange = range.createFromNodeBefore(list.head(anchors));
+      var startPoint = startRange.getStartPoint();
+      var endRange = range.createFromNodeAfter(list.last(anchors));
+      var endPoint = endRange.getEndPoint();
+
+      range.create(
+        startPoint.node,
+        startPoint.offset,
+        endPoint.node,
+        endPoint.offset
+      ).select();
+
+      afterCommand($editable);
+    };
+
+    /**
+     * returns link info
+     *
+     * @return {Object}
+     * @return {WrappedRange} return.range
+     * @return {String} return.text
+     * @return {Boolean} [return.isNewWindow=true]
+     * @return {String} [return.url=""]
+     */
+    this.getLinkInfo = function ($editable) {
+      this.focus($editable);
+
+      var rng = range.create().expand(dom.isAnchor);
+
+      // Get the first anchor on range(for edit).
+      var $anchor = $(list.head(rng.nodes(dom.isAnchor)));
+
+      return {
+        range: rng,
+        text: rng.toString(),
+        isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : false,
+        url: $anchor.length ? $anchor.attr('href') : ''
+      };
+    };
+
+    /**
+     * setting color
+     *
+     * @param {Node} $editable
+     * @param {Object} sObjColor  color code
+     * @param {String} sObjColor.foreColor foreground color
+     * @param {String} sObjColor.backColor background color
+     */
+    this.color = function ($editable, sObjColor) {
+      var oColor = JSON.parse(sObjColor);
+      var foreColor = oColor.foreColor, backColor = oColor.backColor;
+
+      beforeCommand($editable);
+
+      if (foreColor) { document.execCommand('foreColor', false, foreColor); }
+      if (backColor) { document.execCommand('backColor', false, backColor); }
+
+      afterCommand($editable);
+    };
+
+    /**
+     * insert Table
+     *
+     * @param {Node} $editable
+     * @param {String} sDim dimension of table (ex : "5x5")
+     */
+    this.insertTable = function ($editable, sDim) {
+      var dimension = sDim.split('x');
+      beforeCommand($editable);
+
+      var rng = range.create().deleteContents();
+      rng.insertNode(table.createTable(dimension[0], dimension[1]));
+      afterCommand($editable);
+    };
+
+    /**
+     * float me
+     *
+     * @param {jQuery} $editable
+     * @param {String} value
+     * @param {jQuery} $target
+     */
+    this.floatMe = function ($editable, value, $target) {
+      beforeCommand($editable);
+      // bootstrap
+      $target.removeClass('pull-left pull-right');
+      if (value && value !== 'none') {
+        $target.addClass('pull-' + value);
+      }
+
+      // fallback for non-bootstrap
+      $target.css('float', value);
+      afterCommand($editable);
+    };
+
+    /**
+     * change image shape
+     *
+     * @param {jQuery} $editable
+     * @param {String} value css class
+     * @param {Node} $target
+     */
+    this.imageShape = function ($editable, value, $target) {
+      beforeCommand($editable);
+
+      $target.removeClass('img-rounded img-circle img-thumbnail');
+
+      if (value) {
+        $target.addClass(value);
+      }
+
+      afterCommand($editable);
+    };
+
+    /**
+     * resize overlay element
+     * @param {jQuery} $editable
+     * @param {String} value
+     * @param {jQuery} $target - target element
+     */
+    this.resize = function ($editable, value, $target) {
+      beforeCommand($editable);
+
+      $target.css({
+        width: value * 100 + '%',
+        height: ''
+      });
+
+      afterCommand($editable);
+    };
+
+    /**
+     * @param {Position} pos
+     * @param {jQuery} $target - target element
+     * @param {Boolean} [bKeepRatio] - keep ratio
+     */
+    this.resizeTo = function (pos, $target, bKeepRatio) {
+      var imageSize;
+      if (bKeepRatio) {
+        var newRatio = pos.y / pos.x;
+        var ratio = $target.data('ratio');
+        imageSize = {
+          width: ratio > newRatio ? pos.x : pos.y / ratio,
+          height: ratio > newRatio ? pos.x * ratio : pos.y
+        };
+      } else {
+        imageSize = {
+          width: pos.x,
+          height: pos.y
+        };
+      }
+
+      $target.css(imageSize);
+    };
+
+    /**
+     * remove media object
+     *
+     * @param {jQuery} $editable
+     * @param {String} value - dummy argument (for keep interface)
+     * @param {jQuery} $target - target element
+     */
+    this.removeMedia = function ($editable, value, $target) {
+      beforeCommand($editable);
+      $target.detach();
+
+      handler.bindCustomEvent(
+        $(), $editable.data('callbacks'), 'media.delete'
+      )($target, $editable);
+
+      afterCommand($editable);
+    };
+
+    /**
+     * set focus
+     *
+     * @param $editable
+     */
+    this.focus = function ($editable) {
+      $editable.focus();
+
+      // [workaround] for firefox bug http://goo.gl/lVfAaI
+      if (agent.isFF && !range.create().isOnEditable()) {
+        range.createFromNode($editable[0])
+             .normalize()
+             .collapse()
+             .select();
+      }
+    };
+
+    /**
+     * returns whether contents is empty or not.
+     *
+     * @param {jQuery} $editable
+     * @return {Boolean}
+     */
+    this.isEmpty = function ($editable) {
+      return dom.isEmpty($editable[0]) || dom.emptyPara === $editable.html();
+    };
+  };
+
+  /**
+   * @class module.Button
+   *
+   * Button
+   */
+  var Button = function () {
+    /**
+     * update button status
+     *
+     * @param {jQuery} $container
+     * @param {Object} styleInfo
+     */
+    this.update = function ($container, styleInfo) {
+      /**
+       * handle dropdown's check mark (for fontname, fontsize, lineHeight).
+       * @param {jQuery} $btn
+       * @param {Number} value
+       */
+      var checkDropdownMenu = function ($btn, value) {
+        $btn.find('.dropdown-menu li a').each(function () {
+          // always compare string to avoid creating another func.
+          var isChecked = ($(this).data('value') + '') === (value + '');
+          this.className = isChecked ? 'checked' : '';
+        });
+      };
+
+      /**
+       * update button state(active or not).
+       *
+       * @private
+       * @param {String} selector
+       * @param {Function} pred
+       */
+      var btnState = function (selector, pred) {
+        var $btn = $container.find(selector);
+        $btn.toggleClass('active', pred());
+      };
+
+      if (styleInfo.image) {
+        var $img = $(styleInfo.image);
+
+        btnState('button[data-event="imageShape"][data-value="img-rounded"]', function () {
+          return $img.hasClass('img-rounded');
+        });
+        btnState('button[data-event="imageShape"][data-value="img-circle"]', function () {
+          return $img.hasClass('img-circle');
+        });
+        btnState('button[data-event="imageShape"][data-value="img-thumbnail"]', function () {
+          return $img.hasClass('img-thumbnail');
+        });
+        btnState('button[data-event="imageShape"]:not([data-value])', function () {
+          return !$img.is('.img-rounded, .img-circle, .img-thumbnail');
+        });
+
+        var imgFloat = $img.css('float');
+        btnState('button[data-event="floatMe"][data-value="left"]', function () {
+          return imgFloat === 'left';
+        });
+        btnState('button[data-event="floatMe"][data-value="right"]', function () {
+          return imgFloat === 'right';
+        });
+        btnState('button[data-event="floatMe"][data-value="none"]', function () {
+          return imgFloat !== 'left' && imgFloat !== 'right';
+        });
+
+        var style = $img.attr('style');
+        btnState('button[data-event="resize"][data-value="1"]', function () {
+          return !!/(^|\s)(max-)?width\s*:\s*100%/.test(style);
+        });
+        btnState('button[data-event="resize"][data-value="0.5"]', function () {
+          return !!/(^|\s)(max-)?width\s*:\s*50%/.test(style);
+        });
+        btnState('button[data-event="resize"][data-value="0.25"]', function () {
+          return !!/(^|\s)(max-)?width\s*:\s*25%/.test(style);
+        });
+        return;
+      }
+
+      // fontname
+      var $fontname = $container.find('.note-fontname');
+      if ($fontname.length) {
+        var selectedFont = styleInfo['font-family'];
+        if (!!selectedFont) {
+
+          var list = selectedFont.split(',');
+          for (var i = 0, len = list.length; i < len; i++) {
+            selectedFont = list[i].replace(/[\'\"]/g, '').replace(/\s+$/, '').replace(/^\s+/, '');
+            if (agent.isFontInstalled(selectedFont)) {
+              break;
+            }
+          }
+          
+          $fontname.find('.note-current-fontname').text(selectedFont);
+          checkDropdownMenu($fontname, selectedFont);
+
+        }
+      }
+
+      // fontsize
+      var $fontsize = $container.find('.note-fontsize');
+      $fontsize.find('.note-current-fontsize').text(styleInfo['font-size']);
+      checkDropdownMenu($fontsize, parseFloat(styleInfo['font-size']));
+
+      // lineheight
+      var $lineHeight = $container.find('.note-height');
+      checkDropdownMenu($lineHeight, parseFloat(styleInfo['line-height']));
+
+      btnState('button[data-event="bold"]', function () {
+        return styleInfo['font-bold'] === 'bold';
+      });
+      btnState('button[data-event="italic"]', function () {
+        return styleInfo['font-italic'] === 'italic';
+      });
+      btnState('button[data-event="underline"]', function () {
+        return styleInfo['font-underline'] === 'underline';
+      });
+      btnState('button[data-event="strikethrough"]', function () {
+        return styleInfo['font-strikethrough'] === 'strikethrough';
+      });
+      btnState('button[data-event="superscript"]', function () {
+        return styleInfo['font-superscript'] === 'superscript';
+      });
+      btnState('button[data-event="subscript"]', function () {
+        return styleInfo['font-subscript'] === 'subscript';
+      });
+      btnState('button[data-event="justifyLeft"]', function () {
+        return styleInfo['text-align'] === 'left' || styleInfo['text-align'] === 'start';
+      });
+      btnState('button[data-event="justifyCenter"]', function () {
+        return styleInfo['text-align'] === 'center';
+      });
+      btnState('button[data-event="justifyRight"]', function () {
+        return styleInfo['text-align'] === 'right';
+      });
+      btnState('button[data-event="justifyFull"]', function () {
+        return styleInfo['text-align'] === 'justify';
+      });
+      btnState('button[data-event="insertUnorderedList"]', function () {
+        return styleInfo['list-style'] === 'unordered';
+      });
+      btnState('button[data-event="insertOrderedList"]', function () {
+        return styleInfo['list-style'] === 'ordered';
+      });
+    };
+
+    /**
+     * update recent color
+     *
+     * @param {Node} button
+     * @param {String} eventName
+     * @param {Mixed} value
+     */
+    this.updateRecentColor = function (button, eventName, value) {
+      var $color = $(button).closest('.note-color');
+      var $recentColor = $color.find('.note-recent-color');
+      var colorInfo = JSON.parse($recentColor.attr('data-value'));
+      colorInfo[eventName] = value;
+      $recentColor.attr('data-value', JSON.stringify(colorInfo));
+      var sKey = eventName === 'backColor' ? 'background-color' : 'color';
+      $recentColor.find('i').css(sKey, value);
+    };
+  };
+
+  /**
+   * @class module.Toolbar
+   *
+   * Toolbar
+   */
+  var Toolbar = function () {
+    var button = new Button();
+
+    this.update = function ($toolbar, styleInfo) {
+      button.update($toolbar, styleInfo);
+    };
+
+    /**
+     * @param {Node} button
+     * @param {String} eventName
+     * @param {String} value
+     */
+    this.updateRecentColor = function (buttonNode, eventName, value) {
+      button.updateRecentColor(buttonNode, eventName, value);
+    };
+
+    /**
+     * activate buttons exclude codeview
+     * @param {jQuery} $toolbar
+     */
+    this.activate = function ($toolbar) {
+      $toolbar.find('button')
+              .not('button[data-event="codeview"]')
+              .removeClass('disabled');
+    };
+
+    /**
+     * deactivate buttons exclude codeview
+     * @param {jQuery} $toolbar
+     */
+    this.deactivate = function ($toolbar) {
+      $toolbar.find('button')
+              .not('button[data-event="codeview"]')
+              .addClass('disabled');
+    };
+
+    /**
+     * @param {jQuery} $container
+     * @param {Boolean} [bFullscreen=false]
+     */
+    this.updateFullscreen = function ($container, bFullscreen) {
+      var $btn = $container.find('button[data-event="fullscreen"]');
+      $btn.toggleClass('active', bFullscreen);
+    };
+
+    /**
+     * @param {jQuery} $container
+     * @param {Boolean} [isCodeview=false]
+     */
+    this.updateCodeview = function ($container, isCodeview) {
+      var $btn = $container.find('button[data-event="codeview"]');
+      $btn.toggleClass('active', isCodeview);
+
+      if (isCodeview) {
+        this.deactivate($container);
+      } else {
+        this.activate($container);
+      }
+    };
+
+    /**
+     * get button in toolbar 
+     *
+     * @param {jQuery} $editable
+     * @param {String} name
+     * @return {jQuery}
+     */
+    this.get = function ($editable, name) {
+      var $toolbar = dom.makeLayoutInfo($editable).toolbar();
+
+      return $toolbar.find('[data-name=' + name + ']');
+    };
+
+    /**
+     * set button state
+     * @param {jQuery} $editable
+     * @param {String} name
+     * @param {Boolean} [isActive=true]
+     */
+    this.setButtonState = function ($editable, name, isActive) {
+      isActive = (isActive === false) ? false : true;
+
+      var $button = this.get($editable, name);
+      $button.toggleClass('active', isActive);
+    };
+  };
+
+  var EDITABLE_PADDING = 24;
+
+  var Statusbar = function () {
+    var $document = $(document);
+
+    this.attach = function (layoutInfo, options) {
+      if (!options.disableResizeEditor) {
+        layoutInfo.statusbar().on('mousedown', hStatusbarMousedown);
+      }
+    };
+
+    /**
+     * `mousedown` event handler on statusbar
+     *
+     * @param {MouseEvent} event
+     */
+    var hStatusbarMousedown = function (event) {
+      event.preventDefault();
+      event.stopPropagation();
+
+      var $editable = dom.makeLayoutInfo(event.target).editable();
+      var editableTop = $editable.offset().top - $document.scrollTop();
+
+      var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
+      var options = layoutInfo.editor().data('options');
+
+      $document.on('mousemove', function (event) {
+        var nHeight = event.clientY - (editableTop + EDITABLE_PADDING);
+
+        nHeight = (options.minHeight > 0) ? Math.max(nHeight, options.minHeight) : nHeight;
+        nHeight = (options.maxHeight > 0) ? Math.min(nHeight, options.maxHeight) : nHeight;
+
+        $editable.height(nHeight);
+      }).one('mouseup', function () {
+        $document.off('mousemove');
+      });
+    };
+  };
+
+  /**
+   * @class module.Popover
+   *
+   * Popover (http://getbootstrap.com/javascript/#popovers)
+   *
+   */
+  var Popover = function () {
+    var button = new Button();
+
+    /**
+     * returns position from placeholder
+     *
+     * @private
+     * @param {Node} placeholder
+     * @param {Object} options
+     * @param {Boolean} options.isAirMode
+     * @return {Position}
+     */
+    var posFromPlaceholder = function (placeholder, options) {
+      var isAirMode = options && options.isAirMode;
+      var isLeftTop = options && options.isLeftTop;
+
+      var $placeholder = $(placeholder);
+      var pos = isAirMode ? $placeholder.offset() : $placeholder.position();
+      var height = isLeftTop ? 0 : $placeholder.outerHeight(true); // include margin
+
+      // popover below placeholder.
+      return {
+        left: pos.left,
+        top: pos.top + height
+      };
+    };
+
+    /**
+     * show popover
+     *
+     * @private
+     * @param {jQuery} popover
+     * @param {Position} pos
+     */
+    var showPopover = function ($popover, pos) {
+      $popover.css({
+        display: 'block',
+        left: pos.left,
+        top: pos.top
+      });
+    };
+
+    var PX_POPOVER_ARROW_OFFSET_X = 20;
+
+    /**
+     * update current state
+     * @param {jQuery} $popover - popover container
+     * @param {Object} styleInfo - style object
+     * @param {Boolean} isAirMode
+     */
+    this.update = function ($popover, styleInfo, isAirMode) {
+      button.update($popover, styleInfo);
+
+      var $linkPopover = $popover.find('.note-link-popover');
+      if (styleInfo.anchor) {
+        var $anchor = $linkPopover.find('a');
+        var href = $(styleInfo.anchor).attr('href');
+        var target = $(styleInfo.anchor).attr('target');
+        $anchor.attr('href', href).html(href);
+        if (!target) {
+          $anchor.removeAttr('target');
+        } else {
+          $anchor.attr('target', '_blank');
+        }
+        showPopover($linkPopover, posFromPlaceholder(styleInfo.anchor, {
+          isAirMode: isAirMode
+        }));
+      } else {
+        $linkPopover.hide();
+      }
+
+      var $imagePopover = $popover.find('.note-image-popover');
+      if (styleInfo.image) {
+        showPopover($imagePopover, posFromPlaceholder(styleInfo.image, {
+          isAirMode: isAirMode,
+          isLeftTop: true
+        }));
+      } else {
+        $imagePopover.hide();
+      }
+
+      var $airPopover = $popover.find('.note-air-popover');
+      if (isAirMode && styleInfo.range && !styleInfo.range.isCollapsed()) {
+        var rect = list.last(styleInfo.range.getClientRects());
+        if (rect) {
+          var bnd = func.rect2bnd(rect);
+          showPopover($airPopover, {
+            left: Math.max(bnd.left + bnd.width / 2 - PX_POPOVER_ARROW_OFFSET_X, 0),
+            top: bnd.top + bnd.height
+          });
+        }
+      } else {
+        $airPopover.hide();
+      }
+    };
+
+    /**
+     * @param {Node} button
+     * @param {String} eventName
+     * @param {String} value
+     */
+    this.updateRecentColor = function (button, eventName, value) {
+      button.updateRecentColor(button, eventName, value);
+    };
+
+    /**
+     * hide all popovers
+     * @param {jQuery} $popover - popover container
+     */
+    this.hide = function ($popover) {
+      $popover.children().hide();
+    };
+  };
+
+  /**
+   * @class module.Handle
+   *
+   * Handle
+   */
+  var Handle = function (handler) {
+    var $document = $(document);
+
+    /**
+     * `mousedown` event handler on $handle
+     *  - controlSizing: resize image
+     *
+     * @param {MouseEvent} event
+     */
+    var hHandleMousedown = function (event) {
+      if (dom.isControlSizing(event.target)) {
+        event.preventDefault();
+        event.stopPropagation();
+
+        var layoutInfo = dom.makeLayoutInfo(event.target),
+            $handle = layoutInfo.handle(),
+            $popover = layoutInfo.popover(),
+            $editable = layoutInfo.editable(),
+            $editor = layoutInfo.editor();
+
+        var target = $handle.find('.note-control-selection').data('target'),
+            $target = $(target), posStart = $target.offset(),
+            scrollTop = $document.scrollTop();
+
+        var isAirMode = $editor.data('options').airMode;
+
+        $document.on('mousemove', function (event) {
+          handler.invoke('editor.resizeTo', {
+            x: event.clientX - posStart.left,
+            y: event.clientY - (posStart.top - scrollTop)
+          }, $target, !event.shiftKey);
+
+          handler.invoke('handle.update', $handle, {image: target}, isAirMode);
+          handler.invoke('popover.update', $popover, {image: target}, isAirMode);
+        }).one('mouseup', function () {
+          $document.off('mousemove');
+          handler.invoke('editor.afterCommand', $editable);
+        });
+
+        if (!$target.data('ratio')) { // original ratio.
+          $target.data('ratio', $target.height() / $target.width());
+        }
+      }
+    };
+
+    this.attach = function (layoutInfo) {
+      layoutInfo.handle().on('mousedown', hHandleMousedown);
+    };
+
+    /**
+     * update handle
+     * @param {jQuery} $handle
+     * @param {Object} styleInfo
+     * @param {Boolean} isAirMode
+     */
+    this.update = function ($handle, styleInfo, isAirMode) {
+      var $selection = $handle.find('.note-control-selection');
+      if (styleInfo.image) {
+        var $image = $(styleInfo.image);
+        var pos = isAirMode ? $image.offset() : $image.position();
+
+        // include margin
+        var imageSize = {
+          w: $image.outerWidth(true),
+          h: $image.outerHeight(true)
+        };
+
+        $selection.css({
+          display: 'block',
+          left: pos.left,
+          top: pos.top,
+          width: imageSize.w,
+          height: imageSize.h
+        }).data('target', styleInfo.image); // save current image element.
+        var sizingText = imageSize.w + 'x' + imageSize.h;
+        $selection.find('.note-control-selection-info').text(sizingText);
+      } else {
+        $selection.hide();
+      }
+    };
+
+    /**
+     * hide
+     *
+     * @param {jQuery} $handle
+     */
+    this.hide = function ($handle) {
+      $handle.children().hide();
+    };
+  };
+
+  var Fullscreen = function (handler) {
+    var $window = $(window);
+    var $scrollbar = $('html, body');
+
+    /**
+     * toggle fullscreen
+     *
+     * @param {Object} layoutInfo
+     */
+    this.toggle = function (layoutInfo) {
+
+      var $editor = layoutInfo.editor(),
+          $toolbar = layoutInfo.toolbar(),
+          $editable = layoutInfo.editable(),
+          $codable = layoutInfo.codable();
+
+      var resize = function (size) {
+        $editable.css('height', size.h);
+        $codable.css('height', size.h);
+        if ($codable.data('cmeditor')) {
+          $codable.data('cmeditor').setsize(null, size.h);
+        }
+      };
+
+      $editor.toggleClass('fullscreen');
+      var isFullscreen = $editor.hasClass('fullscreen');
+      if (isFullscreen) {
+        $editable.data('orgheight', $editable.css('height'));
+
+        $window.on('resize', function () {
+          resize({
+            h: $window.height() - $toolbar.outerHeight()
+          });
+        }).trigger('resize');
+
+        $scrollbar.css('overflow', 'hidden');
+      } else {
+        $window.off('resize');
+        resize({
+          h: $editable.data('orgheight')
+        });
+        $scrollbar.css('overflow', 'visible');
+      }
+
+      handler.invoke('toolbar.updateFullscreen', $toolbar, isFullscreen);
+    };
+  };
+
+
+  var CodeMirror;
+  if (agent.hasCodeMirror) {
+    if (agent.isSupportAmd) {
+      require(['CodeMirror'], function (cm) {
+        CodeMirror = cm;
+      });
+    } else {
+      CodeMirror = window.CodeMirror;
+    }
+  }
+
+  /**
+   * @class Codeview
+   */
+  var Codeview = function (handler) {
+
+    this.sync = function (layoutInfo) {
+      var isCodeview = handler.invoke('codeview.isActivated', layoutInfo);
+      if (isCodeview && agent.hasCodeMirror) {
+        layoutInfo.codable().data('cmEditor').save();
+      }
+    };
+
+    /**
+     * @param {Object} layoutInfo
+     * @return {Boolean}
+     */
+    this.isActivated = function (layoutInfo) {
+      var $editor = layoutInfo.editor();
+      return $editor.hasClass('codeview');
+    };
+
+    /**
+     * toggle codeview
+     *
+     * @param {Object} layoutInfo
+     */
+    this.toggle = function (layoutInfo) {
+      if (this.isActivated(layoutInfo)) {
+        this.deactivate(layoutInfo);
+      } else {
+        this.activate(layoutInfo);
+      }
+    };
+
+    /**
+     * activate code view
+     *
+     * @param {Object} layoutInfo
+     */
+    this.activate = function (layoutInfo) {
+      var $editor = layoutInfo.editor(),
+          $toolbar = layoutInfo.toolbar(),
+          $editable = layoutInfo.editable(),
+          $codable = layoutInfo.codable(),
+          $popover = layoutInfo.popover(),
+          $handle = layoutInfo.handle();
+
+      var options = $editor.data('options');
+
+      $codable.val(dom.html($editable, options.prettifyHtml));
+      $codable.height($editable.height());
+
+      handler.invoke('toolbar.updateCodeview', $toolbar, true);
+      handler.invoke('popover.hide', $popover);
+      handler.invoke('handle.hide', $handle);
+
+      $editor.addClass('codeview');
+
+      $codable.focus();
+
+      // activate CodeMirror as codable
+      if (agent.hasCodeMirror) {
+        var cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror);
+
+        // CodeMirror TernServer
+        if (options.codemirror.tern) {
+          var server = new CodeMirror.TernServer(options.codemirror.tern);
+          cmEditor.ternServer = server;
+          cmEditor.on('cursorActivity', function (cm) {
+            server.updateArgHints(cm);
+          });
+        }
+
+        // CodeMirror hasn't Padding.
+        cmEditor.setSize(null, $editable.outerHeight());
+        $codable.data('cmEditor', cmEditor);
+      }
+    };
+
+    /**
+     * deactivate code view
+     *
+     * @param {Object} layoutInfo
+     */
+    this.deactivate = function (layoutInfo) {
+      var $holder = layoutInfo.holder(),
+          $editor = layoutInfo.editor(),
+          $toolbar = layoutInfo.toolbar(),
+          $editable = layoutInfo.editable(),
+          $codable = layoutInfo.codable();
+
+      var options = $editor.data('options');
+
+      // deactivate CodeMirror as codable
+      if (agent.hasCodeMirror) {
+        var cmEditor = $codable.data('cmEditor');
+        $codable.val(cmEditor.getValue());
+        cmEditor.toTextArea();
+      }
+
+      var value = dom.value($codable, options.prettifyHtml) || dom.emptyPara;
+      var isChange = $editable.html() !== value;
+
+      $editable.html(value);
+      $editable.height(options.height ? $codable.height() : 'auto');
+      $editor.removeClass('codeview');
+
+      if (isChange) {
+        handler.bindCustomEvent(
+          $holder, $editable.data('callbacks'), 'change'
+        )($editable.html(), $editable);
+      }
+
+      $editable.focus();
+
+      handler.invoke('toolbar.updateCodeview', $toolbar, false);
+    };
+  };
+
+  var DragAndDrop = function (handler) {
+    var $document = $(document);
+
+    /**
+     * attach Drag and Drop Events
+     *
+     * @param {Object} layoutInfo - layout Informations
+     * @param {Object} options
+     */
+    this.attach = function (layoutInfo, options) {
+      if (options.airMode || options.disableDragAndDrop) {
+        // prevent default drop event
+        $document.on('drop', function (e) {
+          e.preventDefault();
+        });
+      } else {
+        this.attachDragAndDropEvent(layoutInfo, options);
+      }
+    };
+
+    /**
+     * attach Drag and Drop Events
+     *
+     * @param {Object} layoutInfo - layout Informations
+     * @param {Object} options
+     */
+    this.attachDragAndDropEvent = function (layoutInfo, options) {
+      var collection = $(),
+          $editor = layoutInfo.editor(),
+          $dropzone = layoutInfo.dropzone(),
+          $dropzoneMessage = $dropzone.find('.note-dropzone-message');
+
+      // show dropzone on dragenter when dragging a object to document
+      // -but only if the editor is visible, i.e. has a positive width and height
+      $document.on('dragenter', function (e) {
+        var isCodeview = handler.invoke('codeview.isActivated', layoutInfo);
+        var hasEditorSize = $editor.width() > 0 && $editor.height() > 0;
+        if (!isCodeview && !collection.length && hasEditorSize) {
+          $editor.addClass('dragover');
+          $dropzone.width($editor.width());
+          $dropzone.height($editor.height());
+          $dropzoneMessage.text(options.langInfo.image.dragImageHere);
+        }
+        collection = collection.add(e.target);
+      }).on('dragleave', function (e) {
+        collection = collection.not(e.target);
+        if (!collection.length) {
+          $editor.removeClass('dragover');
+        }
+      }).on('drop', function () {
+        collection = $();
+        $editor.removeClass('dragover');
+      });
+
+      // change dropzone's message on hover.
+      $dropzone.on('dragenter', function () {
+        $dropzone.addClass('hover');
+        $dropzoneMessage.text(options.langInfo.image.dropImage);
+      }).on('dragleave', function () {
+        $dropzone.removeClass('hover');
+        $dropzoneMessage.text(options.langInfo.image.dragImageHere);
+      });
+
+      // attach dropImage
+      $dropzone.on('drop', function (event) {
+
+        var dataTransfer = event.originalEvent.dataTransfer;
+        var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
+
+        if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
+          event.preventDefault();
+          layoutInfo.editable().focus();
+          handler.insertImages(layoutInfo, dataTransfer.files);
+        } else {
+          var insertNodefunc = function () {
+            layoutInfo.holder().summernote('insertNode', this);
+          };
+
+          for (var i = 0, len = dataTransfer.types.length; i < len; i++) {
+            var type = dataTransfer.types[i];
+            var content = dataTransfer.getData(type);
+
+            if (type.toLowerCase().indexOf('text') > -1) {
+              layoutInfo.holder().summernote('pasteHTML', content);
+            } else {
+              $(content).each(insertNodefunc);
+            }
+          }
+        }
+      }).on('dragover', false); // prevent default dragover event
+    };
+  };
+
+  var Clipboard = function (handler) {
+    var $paste;
+
+    this.attach = function (layoutInfo) {
+      // [workaround] getting image from clipboard
+      //  - IE11 and Firefox: CTRL+v hook
+      //  - Webkit: event.clipboardData
+      if ((agent.isMSIE && agent.browserVersion > 10) || agent.isFF) {
+        $paste = $('<div />').attr('contenteditable', true).css({
+          position : 'absolute',
+          left : -100000,
+          opacity : 0
+        });
+
+        layoutInfo.editable().on('keydown', function (e) {
+          if (e.ctrlKey && e.keyCode === key.code.V) {
+            handler.invoke('saveRange', layoutInfo.editable());
+            $paste.focus();
+
+            setTimeout(function () {
+              pasteByHook(layoutInfo);
+            }, 0);
+          }
+        });
+
+        layoutInfo.editable().before($paste);
+      } else {
+        layoutInfo.editable().on('paste', pasteByEvent);
+      }
+    };
+
+    var pasteByHook = function (layoutInfo) {
+      var $editable = layoutInfo.editable();
+      var node = $paste[0].firstChild;
+
+      if (dom.isImg(node)) {
+        var dataURI = node.src;
+        var decodedData = atob(dataURI.split(',')[1]);
+        var array = new Uint8Array(decodedData.length);
+        for (var i = 0; i < decodedData.length; i++) {
+          array[i] = decodedData.charCodeAt(i);
+        }
+
+        var blob = new Blob([array], { type : 'image/png' });
+        blob.name = 'clipboard.png';
+
+        handler.invoke('restoreRange', $editable);
+        handler.invoke('focus', $editable);
+        handler.insertImages(layoutInfo, [blob]);
+      } else {
+        var pasteContent = $('<div />').html($paste.html()).html();
+        handler.invoke('restoreRange', $editable);
+        handler.invoke('focus', $editable);
+
+        if (pasteContent) {
+          handler.invoke('pasteHTML', $editable, pasteContent);
+        }
+      }
+
+      $paste.empty();
+    };
+
+    /**
+     * paste by clipboard event
+     *
+     * @param {Event} event
+     */
+    var pasteByEvent = function (event) {
+      var clipboardData = event.originalEvent.clipboardData;
+      var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
+      var $editable = layoutInfo.editable();
+
+      if (clipboardData && clipboardData.items && clipboardData.items.length) {
+        var item = list.head(clipboardData.items);
+        if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
+          handler.insertImages(layoutInfo, [item.getAsFile()]);
+        }
+        handler.invoke('editor.afterCommand', $editable);
+      }
+    };
+  };
+
+  var LinkDialog = function (handler) {
+
+    /**
+     * toggle button status
+     *
+     * @private
+     * @param {jQuery} $btn
+     * @param {Boolean} isEnable
+     */
+    var toggleBtn = function ($btn, isEnable) {
+      $btn.toggleClass('disabled', !isEnable);
+      $btn.attr('disabled', !isEnable);
+    };
+
+    /**
+     * bind enter key
+     *
+     * @private
+     * @param {jQuery} $input
+     * @param {jQuery} $btn
+     */
+    var bindEnterKey = function ($input, $btn) {
+      $input.on('keypress', function (event) {
+        if (event.keyCode === key.code.ENTER) {
+          $btn.trigger('click');
+        }
+      });
+    };
+
+    /**
+     * Show link dialog and set event handlers on dialog controls.
+     *
+     * @param {jQuery} $editable
+     * @param {jQuery} $dialog
+     * @param {Object} linkInfo
+     * @return {Promise}
+     */
+    this.showLinkDialog = function ($editable, $dialog, linkInfo) {
+      return $.Deferred(function (deferred) {
+        var $linkDialog = $dialog.find('.note-link-dialog');
+
+        var $linkText = $linkDialog.find('.note-link-text'),
+        $linkUrl = $linkDialog.find('.note-link-url'),
+        $linkBtn = $linkDialog.find('.note-link-btn'),
+        $openInNewWindow = $linkDialog.find('input[type=checkbox]');
+
+        $linkDialog.one('shown.bs.modal', function () {
+          $linkText.val(linkInfo.text);
+
+          $linkText.on('input', function () {
+            toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
+            // if linktext was modified by keyup,
+            // stop cloning text from linkUrl
+            linkInfo.text = $linkText.val();
+          });
+
+          // if no url was given, copy text to url
+          if (!linkInfo.url) {
+            linkInfo.url = linkInfo.text || 'http://';
+            toggleBtn($linkBtn, linkInfo.text);
+          }
+
+          $linkUrl.on('input', function () {
+            toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
+            // display same link on `Text to display` input
+            // when create a new link
+            if (!linkInfo.text) {
+              $linkText.val($linkUrl.val());
+            }
+          }).val(linkInfo.url).trigger('focus').trigger('select');
+
+          bindEnterKey($linkUrl, $linkBtn);
+          bindEnterKey($linkText, $linkBtn);
+
+          $openInNewWindow.prop('checked', linkInfo.isNewWindow);
+
+          $linkBtn.one('click', function (event) {
+            event.preventDefault();
+
+            deferred.resolve({
+              range: linkInfo.range,
+              url: $linkUrl.val(),
+              text: $linkText.val(),
+              isNewWindow: $openInNewWindow.is(':checked')
+            });
+            $linkDialog.modal('hide');
+          });
+        }).one('hidden.bs.modal', function () {
+          // detach events
+          $linkText.off('input keypress');
+          $linkUrl.off('input keypress');
+          $linkBtn.off('click');
+
+          if (deferred.state() === 'pending') {
+            deferred.reject();
+          }
+        }).modal('show');
+      }).promise();
+    };
+
+    /**
+     * @param {Object} layoutInfo
+     */
+    this.show = function (layoutInfo) {
+      var $editor = layoutInfo.editor(),
+          $dialog = layoutInfo.dialog(),
+          $editable = layoutInfo.editable(),
+          $popover = layoutInfo.popover(),
+          linkInfo = handler.invoke('editor.getLinkInfo', $editable);
+
+      var options = $editor.data('options');
+
+      handler.invoke('editor.saveRange', $editable);
+      this.showLinkDialog($editable, $dialog, linkInfo).then(function (linkInfo) {
+        handler.invoke('editor.restoreRange', $editable);
+        handler.invoke('editor.createLink', $editable, linkInfo, options);
+        // hide popover after creating link
+        handler.invoke('popover.hide', $popover);
+      }).fail(function () {
+        handler.invoke('editor.restoreRange', $editable);
+      });
+    };
+  };
+
+  var ImageDialog = function (handler) {
+    /**
+     * toggle button status
+     *
+     * @private
+     * @param {jQuery} $btn
+     * @param {Boolean} isEnable
+     */
+    var toggleBtn = function ($btn, isEnable) {
+      $btn.toggleClass('disabled', !isEnable);
+      $btn.attr('disabled', !isEnable);
+    };
+
+    /**
+     * bind enter key
+     *
+     * @private
+     * @param {jQuery} $input
+     * @param {jQuery} $btn
+     */
+    var bindEnterKey = function ($input, $btn) {
+      $input.on('keypress', function (event) {
+        if (event.keyCode === key.code.ENTER) {
+          $btn.trigger('click');
+        }
+      });
+    };
+
+    this.show = function (layoutInfo) {
+      var $dialog = layoutInfo.dialog(),
+          $editable = layoutInfo.editable();
+
+      handler.invoke('editor.saveRange', $editable);
+      this.showImageDialog($editable, $dialog).then(function (data) {
+        handler.invoke('editor.restoreRange', $editable);
+
+        if (typeof data === 'string') {
+          // image url
+          handler.invoke('editor.insertImage', $editable, data);
+        } else {
+          // array of files
+          handler.insertImages(layoutInfo, data);
+        }
+      }).fail(function () {
+        handler.invoke('editor.restoreRange', $editable);
+      });
+    };
+
+    /**
+     * show image dialog
+     *
+     * @param {jQuery} $editable
+     * @param {jQuery} $dialog
+     * @return {Promise}
+     */
+    this.showImageDialog = function ($editable, $dialog) {
+      return $.Deferred(function (deferred) {
+        var $imageDialog = $dialog.find('.note-image-dialog');
+
+        var $imageInput = $dialog.find('.note-image-input'),
+            $imageUrl = $dialog.find('.note-image-url'),
+            $imageBtn = $dialog.find('.note-image-btn');
+
+        $imageDialog.one('shown.bs.modal', function () {
+          // Cloning imageInput to clear element.
+          $imageInput.replaceWith($imageInput.clone()
+            .on('change', function () {
+              deferred.resolve(this.files || this.value);
+              $imageDialog.modal('hide');
+            })
+            .val('')
+          );
+
+          $imageBtn.click(function (event) {
+            event.preventDefault();
+
+            deferred.resolve($imageUrl.val());
+            $imageDialog.modal('hide');
+          });
+
+          $imageUrl.on('keyup paste', function (event) {
+            var url;
+            
+            if (event.type === 'paste') {
+              url = event.originalEvent.clipboardData.getData('text');
+            } else {
+              url = $imageUrl.val();
+            }
+            
+            toggleBtn($imageBtn, url);
+          }).val('').trigger('focus');
+          bindEnterKey($imageUrl, $imageBtn);
+        }).one('hidden.bs.modal', function () {
+          $imageInput.off('change');
+          $imageUrl.off('keyup paste keypress');
+          $imageBtn.off('click');
+
+          if (deferred.state() === 'pending') {
+            deferred.reject();
+          }
+        }).modal('show');
+      });
+    };
+  };
+
+  var HelpDialog = function (handler) {
+    /**
+     * show help dialog
+     *
+     * @param {jQuery} $editable
+     * @param {jQuery} $dialog
+     * @return {Promise}
+     */
+    this.showHelpDialog = function ($editable, $dialog) {
+      return $.Deferred(function (deferred) {
+        var $helpDialog = $dialog.find('.note-help-dialog');
+
+        $helpDialog.one('hidden.bs.modal', function () {
+          deferred.resolve();
+        }).modal('show');
+      }).promise();
+    };
+
+    /**
+     * @param {Object} layoutInfo
+     */
+    this.show = function (layoutInfo) {
+      var $dialog = layoutInfo.dialog(),
+          $editable = layoutInfo.editable();
+
+      handler.invoke('editor.saveRange', $editable, true);
+      this.showHelpDialog($editable, $dialog).then(function () {
+        handler.invoke('editor.restoreRange', $editable);
+      });
+    };
+  };
+
+
+  /**
+   * @class EventHandler
+   *
+   * EventHandler
+   *  - TODO: new instance per a editor
+   */
+  var EventHandler = function () {
+    var self = this;
+
+    /**
+     * Modules
+     */
+    var modules = this.modules = {
+      editor: new Editor(this),
+      toolbar: new Toolbar(this),
+      statusbar: new Statusbar(this),
+      popover: new Popover(this),
+      handle: new Handle(this),
+      fullscreen: new Fullscreen(this),
+      codeview: new Codeview(this),
+      dragAndDrop: new DragAndDrop(this),
+      clipboard: new Clipboard(this),
+      linkDialog: new LinkDialog(this),
+      imageDialog: new ImageDialog(this),
+      helpDialog: new HelpDialog(this)
+    };
+
+    /**
+     * invoke module's method
+     *
+     * @param {String} moduleAndMethod - ex) 'editor.redo'
+     * @param {...*} arguments - arguments of method
+     * @return {*}
+     */
+    this.invoke = function () {
+      var moduleAndMethod = list.head(list.from(arguments));
+      var args = list.tail(list.from(arguments));
+
+      var splits = moduleAndMethod.split('.');
+      var hasSeparator = splits.length > 1;
+      var moduleName = hasSeparator && list.head(splits);
+      var methodName = hasSeparator ? list.last(splits) : list.head(splits);
+
+      var module = this.getModule(moduleName);
+      var method = module[methodName];
+
+      return method && method.apply(module, args);
+    };
+
+    /**
+     * returns module
+     *
+     * @param {String} moduleName - name of module
+     * @return {Module} - defaults is editor
+     */
+    this.getModule = function (moduleName) {
+      return this.modules[moduleName] || this.modules.editor;
+    };
+
+    /**
+     * @param {jQuery} $holder
+     * @param {Object} callbacks
+     * @param {String} eventNamespace
+     * @returns {Function}
+     */
+    var bindCustomEvent = this.bindCustomEvent = function ($holder, callbacks, eventNamespace) {
+      return function () {
+        var callback = callbacks[func.namespaceToCamel(eventNamespace, 'on')];
+        if (callback) {
+          callback.apply($holder[0], arguments);
+        }
+        return $holder.trigger('summernote.' + eventNamespace, arguments);
+      };
+    };
+
+    /**
+     * insert Images from file array.
+     *
+     * @private
+     * @param {Object} layoutInfo
+     * @param {File[]} files
+     */
+    this.insertImages = function (layoutInfo, files) {
+      var $editor = layoutInfo.editor(),
+          $editable = layoutInfo.editable(),
+          $holder = layoutInfo.holder();
+
+      var callbacks = $editable.data('callbacks');
+      var options = $editor.data('options');
+
+      // If onImageUpload options setted
+      if (callbacks.onImageUpload) {
+        bindCustomEvent($holder, callbacks, 'image.upload')(files);
+      // else insert Image as dataURL
+      } else {
+        $.each(files, function (idx, file) {
+          var filename = file.name;
+          if (options.maximumImageFileSize && options.maximumImageFileSize < file.size) {
+            bindCustomEvent($holder, callbacks, 'image.upload.error')(options.langInfo.image.maximumFileSizeError);
+          } else {
+            async.readFileAsDataURL(file).then(function (sDataURL) {
+              modules.editor.insertImage($editable, sDataURL, filename);
+            }).fail(function () {
+              bindCustomEvent($holder, callbacks, 'image.upload.error')(options.langInfo.image.maximumFileSizeError);
+            });
+          }
+        });
+      }
+    };
+
+    var commands = {
+      /**
+       * @param {Object} layoutInfo
+       */
+      showLinkDialog: function (layoutInfo) {
+        modules.linkDialog.show(layoutInfo);
+      },
+
+      /**
+       * @param {Object} layoutInfo
+       */
+      showImageDialog: function (layoutInfo) {
+        modules.imageDialog.show(layoutInfo);
+      },
+
+      /**
+       * @param {Object} layoutInfo
+       */
+      showHelpDialog: function (layoutInfo) {
+        modules.helpDialog.show(layoutInfo);
+      },
+
+      /**
+       * @param {Object} layoutInfo
+       */
+      fullscreen: function (layoutInfo) {
+        modules.fullscreen.toggle(layoutInfo);
+      },
+
+      /**
+       * @param {Object} layoutInfo
+       */
+      codeview: function (layoutInfo) {
+        modules.codeview.toggle(layoutInfo);
+      }
+    };
+
+    var hMousedown = function (event) {
+      //preventDefault Selection for FF, IE8+
+      if (dom.isImg(event.target)) {
+        event.preventDefault();
+      }
+    };
+
+    var hKeyupAndMouseup = function (event) {
+      var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
+      modules.editor.removeBogus(layoutInfo.editable());
+      hToolbarAndPopoverUpdate(event);
+    };
+
+    /**
+     * update sytle info
+     * @param {Object} styleInfo
+     * @param {Object} layoutInfo
+     */
+    this.updateStyleInfo = function (styleInfo, layoutInfo) {
+      if (!styleInfo) {
+        return;
+      }
+      var isAirMode = layoutInfo.editor().data('options').airMode;
+      if (!isAirMode) {
+        modules.toolbar.update(layoutInfo.toolbar(), styleInfo);
+      }
+
+      modules.popover.update(layoutInfo.popover(), styleInfo, isAirMode);
+      modules.handle.update(layoutInfo.handle(), styleInfo, isAirMode);
+    };
+
+    var hToolbarAndPopoverUpdate = function (event) {
+      var target = event.target;
+      // delay for range after mouseup
+      setTimeout(function () {
+        var layoutInfo = dom.makeLayoutInfo(target);
+        var styleInfo = modules.editor.currentStyle(target);
+        self.updateStyleInfo(styleInfo, layoutInfo);
+      }, 0);
+    };
+
+    var hScroll = function (event) {
+      var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
+      //hide popover and handle when scrolled
+      modules.popover.hide(layoutInfo.popover());
+      modules.handle.hide(layoutInfo.handle());
+    };
+
+    var hToolbarAndPopoverMousedown = function (event) {
+      // prevent default event when insertTable (FF, Webkit)
+      var $btn = $(event.target).closest('[data-event]');
+      if ($btn.length) {
+        event.preventDefault();
+      }
+    };
+
+    var hToolbarAndPopoverClick = function (event) {
+      var $btn = $(event.target).closest('[data-event]');
+
+      if (!$btn.length) {
+        return;
+      }
+
+      var eventName = $btn.attr('data-event'),
+          value = $btn.attr('data-value'),
+          hide = $btn.attr('data-hide');
+
+      var layoutInfo = dom.makeLayoutInfo(event.target);
+
+      // before command: detect control selection element($target)
+      var $target;
+      if ($.inArray(eventName, ['resize', 'floatMe', 'removeMedia', 'imageShape']) !== -1) {
+        var $selection = layoutInfo.handle().find('.note-control-selection');
+        $target = $($selection.data('target'));
+      }
+
+      // If requested, hide the popover when the button is clicked.
+      // Useful for things like showHelpDialog.
+      if (hide) {
+        $btn.parents('.popover').hide();
+      }
+
+      if ($.isFunction($.summernote.pluginEvents[eventName])) {
+        $.summernote.pluginEvents[eventName](event, modules.editor, layoutInfo, value);
+      } else if (modules.editor[eventName]) { // on command
+        var $editable = layoutInfo.editable();
+        $editable.focus();
+        modules.editor[eventName]($editable, value, $target);
+        event.preventDefault();
+      } else if (commands[eventName]) {
+        commands[eventName].call(this, layoutInfo);
+        event.preventDefault();
+      }
+
+      // after command
+      if ($.inArray(eventName, ['backColor', 'foreColor']) !== -1) {
+        var options = layoutInfo.editor().data('options', options);
+        var module = options.airMode ? modules.popover : modules.toolbar;
+        module.updateRecentColor(list.head($btn), eventName, value);
+      }
+
+      hToolbarAndPopoverUpdate(event);
+    };
+
+    var PX_PER_EM = 18;
+    var hDimensionPickerMove = function (event, options) {
+      var $picker = $(event.target.parentNode); // target is mousecatcher
+      var $dimensionDisplay = $picker.next();
+      var $catcher = $picker.find('.note-dimension-picker-mousecatcher');
+      var $highlighted = $picker.find('.note-dimension-picker-highlighted');
+      var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted');
+
+      var posOffset;
+      // HTML5 with jQuery - e.offsetX is undefined in Firefox
+      if (event.offsetX === undefined) {
+        var posCatcher = $(event.target).offset();
+        posOffset = {
+          x: event.pageX - posCatcher.left,
+          y: event.pageY - posCatcher.top
+        };
+      } else {
+        posOffset = {
+          x: event.offsetX,
+          y: event.offsetY
+        };
+      }
+
+      var dim = {
+        c: Math.ceil(posOffset.x / PX_PER_EM) || 1,
+        r: Math.ceil(posOffset.y / PX_PER_EM) || 1
+      };
+
+      $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' });
+      $catcher.attr('data-value', dim.c + 'x' + dim.r);
+
+      if (3 < dim.c && dim.c < options.insertTableMaxSize.col) {
+        $unhighlighted.css({ width: dim.c + 1 + 'em'});
+      }
+
+      if (3 < dim.r && dim.r < options.insertTableMaxSize.row) {
+        $unhighlighted.css({ height: dim.r + 1 + 'em'});
+      }
+
+      $dimensionDisplay.html(dim.c + ' x ' + dim.r);
+    };
+    
+    /**
+     * bind KeyMap on keydown
+     *
+     * @param {Object} layoutInfo
+     * @param {Object} keyMap
+     */
+    this.bindKeyMap = function (layoutInfo, keyMap) {
+      var $editor = layoutInfo.editor();
+      var $editable = layoutInfo.editable();
+
+      $editable.on('keydown', function (event) {
+        var keys = [];
+
+        // modifier
+        if (event.metaKey) { keys.push('CMD'); }
+        if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); }
+        if (event.shiftKey) { keys.push('SHIFT'); }
+
+        // keycode
+        var keyName = key.nameFromCode[event.keyCode];
+        if (keyName) {
+          keys.push(keyName);
+        }
+
+        var pluginEvent;
+        var keyString = keys.join('+');
+        var eventName = keyMap[keyString];
+        if (eventName) {
+          // FIXME Summernote doesn't support event pipeline yet.
+          //  - Plugin -> Base Code
+          pluginEvent = $.summernote.pluginEvents[keyString];
+          if ($.isFunction(pluginEvent)) {
+            if (pluginEvent(event, modules.editor, layoutInfo)) {
+              return false;
+            }
+          }
+
+          pluginEvent = $.summernote.pluginEvents[eventName];
+
+          if ($.isFunction(pluginEvent)) {
+            pluginEvent(event, modules.editor, layoutInfo);
+          } else if (modules.editor[eventName]) {
+            modules.editor[eventName]($editable, $editor.data('options'));
+            event.preventDefault();
+          } else if (commands[eventName]) {
+            commands[eventName].call(this, layoutInfo);
+            event.preventDefault();
+          }
+        } else if (key.isEdit(event.keyCode)) {
+          modules.editor.afterCommand($editable);
+        }
+      });
+    };
+
+    /**
+     * attach eventhandler
+     *
+     * @param {Object} layoutInfo - layout Informations
+     * @param {Object} options - user options include custom event handlers
+     */
+    this.attach = function (layoutInfo, options) {
+      // handlers for editable
+      if (options.shortcuts) {
+        this.bindKeyMap(layoutInfo, options.keyMap[agent.isMac ? 'mac' : 'pc']);
+      }
+      layoutInfo.editable().on('mousedown', hMousedown);
+      layoutInfo.editable().on('keyup mouseup', hKeyupAndMouseup);
+      layoutInfo.editable().on('scroll', hScroll);
+
+      // handler for clipboard
+      modules.clipboard.attach(layoutInfo, options);
+
+      // handler for handle and popover
+      modules.handle.attach(layoutInfo, options);
+      layoutInfo.popover().on('click', hToolbarAndPopoverClick);
+      layoutInfo.popover().on('mousedown', hToolbarAndPopoverMousedown);
+
+      // handler for drag and drop
+      modules.dragAndDrop.attach(layoutInfo, options);
+
+      // handlers for frame mode (toolbar, statusbar)
+      if (!options.airMode) {
+        // handler for toolbar
+        layoutInfo.toolbar().on('click', hToolbarAndPopoverClick);
+        layoutInfo.toolbar().on('mousedown', hToolbarAndPopoverMousedown);
+
+        // handler for statusbar
+        modules.statusbar.attach(layoutInfo, options);
+      }
+
+      // handler for table dimension
+      var $catcherContainer = options.airMode ? layoutInfo.popover() :
+                                                layoutInfo.toolbar();
+      var $catcher = $catcherContainer.find('.note-dimension-picker-mousecatcher');
+      $catcher.css({
+        width: options.insertTableMaxSize.col + 'em',
+        height: options.insertTableMaxSize.row + 'em'
+      }).on('mousemove', function (event) {
+        hDimensionPickerMove(event, options);
+      });
+
+      // save options on editor
+      layoutInfo.editor().data('options', options);
+
+      // ret styleWithCSS for backColor / foreColor clearing with 'inherit'.
+      if (!agent.isMSIE) {
+        // [workaround] for Firefox
+        //  - protect FF Error: NS_ERROR_FAILURE: Failure
+        setTimeout(function () {
+          document.execCommand('styleWithCSS', 0, options.styleWithSpan);
+        }, 0);
+      }
+
+      // History
+      var history = new History(layoutInfo.editable());
+      layoutInfo.editable().data('NoteHistory', history);
+
+      // All editor status will be saved on editable with jquery's data
+      // for support multiple editor with singleton object.
+      layoutInfo.editable().data('callbacks', {
+        onInit: options.onInit,
+        onFocus: options.onFocus,
+        onBlur: options.onBlur,
+        onKeydown: options.onKeydown,
+        onKeyup: options.onKeyup,
+        onMousedown: options.onMousedown,
+        onEnter: options.onEnter,
+        onPaste: options.onPaste,
+        onBeforeCommand: options.onBeforeCommand,
+        onChange: options.onChange,
+        onImageUpload: options.onImageUpload,
+        onImageUploadError: options.onImageUploadError,
+        onMediaDelete: options.onMediaDelete,
+        onToolbarClick: options.onToolbarClick
+      });
+
+      var styleInfo = modules.editor.styleFromNode(layoutInfo.editable());
+      this.updateStyleInfo(styleInfo, layoutInfo);
+    };
+
+    /**
+     * attach jquery custom event
+     *
+     * @param {Object} layoutInfo - layout Informations
+     */
+    this.attachCustomEvent = function (layoutInfo, options) {
+      var $holder = layoutInfo.holder();
+      var $editable = layoutInfo.editable();
+      var callbacks = $editable.data('callbacks');
+
+      $editable.focus(bindCustomEvent($holder, callbacks, 'focus'));
+      $editable.blur(bindCustomEvent($holder, callbacks, 'blur'));
+
+      $editable.keydown(function (event) {
+        if (event.keyCode === key.code.ENTER) {
+          bindCustomEvent($holder, callbacks, 'enter').call(this, event);
+        }
+        bindCustomEvent($holder, callbacks, 'keydown').call(this, event);
+      });
+      $editable.keyup(bindCustomEvent($holder, callbacks, 'keyup'));
+
+      $editable.on('mousedown', bindCustomEvent($holder, callbacks, 'mousedown'));
+      $editable.on('mouseup', bindCustomEvent($holder, callbacks, 'mouseup'));
+      $editable.on('scroll', bindCustomEvent($holder, callbacks, 'scroll'));
+
+      $editable.on('paste', bindCustomEvent($holder, callbacks, 'paste'));
+      
+      // [workaround] IE doesn't have input events for contentEditable
+      //  - see: https://goo.gl/4bfIvA
+      var changeEventName = agent.isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input';
+      $editable.on(changeEventName, function () {
+        bindCustomEvent($holder, callbacks, 'change')($editable.html(), $editable);
+      });
+
+      if (!options.airMode) {
+        layoutInfo.toolbar().click(bindCustomEvent($holder, callbacks, 'toolbar.click'));
+        layoutInfo.popover().click(bindCustomEvent($holder, callbacks, 'popover.click'));
+      }
+
+      // Textarea: auto filling the code before form submit.
+      if (dom.isTextarea(list.head($holder))) {
+        $holder.closest('form').submit(function (e) {
+          layoutInfo.holder().val(layoutInfo.holder().code());
+          bindCustomEvent($holder, callbacks, 'submit').call(this, e, $holder.code());
+        });
+      }
+
+      // textarea auto sync
+      if (dom.isTextarea(list.head($holder)) && options.textareaAutoSync) {
+        $holder.on('summernote.change', function () {
+          layoutInfo.holder().val(layoutInfo.holder().code());
+        });
+      }
+
+      // fire init event
+      bindCustomEvent($holder, callbacks, 'init')(layoutInfo);
+
+      // fire plugin init event
+      for (var i = 0, len = $.summernote.plugins.length; i < len; i++) {
+        if ($.isFunction($.summernote.plugins[i].init)) {
+          $.summernote.plugins[i].init(layoutInfo);
+        }
+      }
+    };
+      
+    this.detach = function (layoutInfo, options) {
+      layoutInfo.holder().off();
+      layoutInfo.editable().off();
+
+      layoutInfo.popover().off();
+      layoutInfo.handle().off();
+      layoutInfo.dialog().off();
+
+      if (!options.airMode) {
+        layoutInfo.dropzone().off();
+        layoutInfo.toolbar().off();
+        layoutInfo.statusbar().off();
+      }
+    };
+  };
+
+  /**
+   * @class Renderer
+   *
+   * renderer
+   *
+   * rendering toolbar and editable
+   */
+  var Renderer = function () {
+
+    /**
+     * bootstrap button template
+     * @private
+     * @param {String} label button name
+     * @param {Object} [options] button options
+     * @param {String} [options.event] data-event
+     * @param {String} [options.className] button's class name
+     * @param {String} [options.value] data-value
+     * @param {String} [options.title] button's title for popup
+     * @param {String} [options.dropdown] dropdown html
+     * @param {String} [options.hide] data-hide
+     */
+    var tplButton = function (label, options) {
+      var event = options.event;
+      var value = options.value;
+      var title = options.title;
+      var className = options.className;
+      var dropdown = options.dropdown;
+      var hide = options.hide;
+
+      return (dropdown ? '<div class="btn-group' +
+               (className ? ' ' + className : '') + '">' : '') +
+               '<button type="button"' +
+                 ' class="btn btn-default btn-sm' +
+                   ((!dropdown && className) ? ' ' + className : '') +
+                   (dropdown ? ' dropdown-toggle' : '') +
+                 '"' +
+                 (dropdown ? ' data-toggle="dropdown"' : '') +
+                 (title ? ' title="' + title + '"' : '') +
+                 (event ? ' data-event="' + event + '"' : '') +
+                 (value ? ' data-value=\'' + value + '\'' : '') +
+                 (hide ? ' data-hide=\'' + hide + '\'' : '') +
+                 ' tabindex="-1">' +
+                 label +
+                 (dropdown ? ' <span class="caret"></span>' : '') +
+               '</button>' +
+               (dropdown || '') +
+             (dropdown ? '</div>' : '');
+    };
+
+    /**
+     * bootstrap icon button template
+     * @private
+     * @param {String} iconClassName
+     * @param {Object} [options]
+     * @param {String} [options.event]
+     * @param {String} [options.value]
+     * @param {String} [options.title]
+     * @param {String} [options.dropdown]
+     */
+    var tplIconButton = function (iconClassName, options) {
+      var label = '<i class="' + iconClassName + '"></i>';
+      return tplButton(label, options);
+    };
+
+    /**
+     * bootstrap popover template
+     * @private
+     * @param {String} className
+     * @param {String} content
+     */
+    var tplPopover = function (className, content) {
+      var $popover = $('<div class="' + className + ' popover bottom in" style="display: none;">' +
+               '<div class="arrow"></div>' +
+               '<div class="popover-content">' +
+               '</div>' +
+             '</div>');
+
+      $popover.find('.popover-content').append(content);
+      return $popover;
+    };
+
+    /**
+     * bootstrap dialog template
+     *
+     * @param {String} className
+     * @param {String} [title='']
+     * @param {String} body
+     * @param {String} [footer='']
+     */
+    var tplDialog = function (className, title, body, footer) {
+      return '<div class="' + className + ' modal" aria-hidden="false">' +
+               '<div class="modal-dialog">' +
+                 '<div class="modal-content">' +
+                   (title ?
+                   '<div class="modal-header">' +
+                     '<button type="button" class="close" aria-hidden="true" tabindex="-1">&times;</button>' +
+                     '<h4 class="modal-title">' + title + '</h4>' +
+                   '</div>' : ''
+                   ) +
+                   '<div class="modal-body">' + body + '</div>' +
+                   (footer ?
+                   '<div class="modal-footer">' + footer + '</div>' : ''
+                   ) +
+                 '</div>' +
+               '</div>' +
+             '</div>';
+    };
+
+    /**
+     * bootstrap dropdown template
+     *
+     * @param {String|String[]} contents
+     * @param {String} [className='']
+     * @param {String} [nodeName='']
+     */
+    var tplDropdown = function (contents, className, nodeName) {
+      var classes = 'dropdown-menu' + (className ? ' ' + className : '');
+      nodeName = nodeName || 'ul';
+      if (contents instanceof Array) {
+        contents = contents.join('');
+      }
+
+      return '<' + nodeName + ' class="' + classes + '">' + contents + '</' + nodeName + '>';
+    };
+
+    var tplButtonInfo = {
+      picture: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.image.image, {
+          event: 'showImageDialog',
+          title: lang.image.image,
+          hide: true
+        });
+      },
+      link: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.link.link, {
+          event: 'showLinkDialog',
+          title: lang.link.link,
+          hide: true
+        });
+      },
+      table: function (lang, options) {
+        var dropdown = [
+          '<div class="note-dimension-picker">',
+          '<div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"></div>',
+          '<div class="note-dimension-picker-highlighted"></div>',
+          '<div class="note-dimension-picker-unhighlighted"></div>',
+          '</div>',
+          '<div class="note-dimension-display"> 1 x 1 </div>'
+        ];
+
+        return tplIconButton(options.iconPrefix + options.icons.table.table, {
+          title: lang.table.table,
+          dropdown: tplDropdown(dropdown, 'note-table')
+        });
+      },
+      style: function (lang, options) {
+        var items = options.styleTags.reduce(function (memo, v) {
+          var label = lang.style[v === 'p' ? 'normal' : v];
+          return memo + '<li><a data-event="formatBlock" href="#" data-value="' + v + '">' +
+                   (
+                     (v === 'p' || v === 'pre') ? label :
+                     '<' + v + '>' + label + '</' + v + '>'
+                   ) +
+                 '</a></li>';
+        }, '');
+
+        return tplIconButton(options.iconPrefix + options.icons.style.style, {
+          title: lang.style.style,
+          dropdown: tplDropdown(items)
+        });
+      },
+      fontname: function (lang, options) {
+        var realFontList = [];
+        var items = options.fontNames.reduce(function (memo, v) {
+          if (!agent.isFontInstalled(v) && !list.contains(options.fontNamesIgnoreCheck, v)) {
+            return memo;
+          }
+          realFontList.push(v);
+          return memo + '<li><a data-event="fontName" href="#" data-value="' + v + '" style="font-family:\'' + v + '\'">' +
+                          '<i class="' + options.iconPrefix + options.icons.misc.check + '"></i> ' + v +
+                        '</a></li>';
+        }, '');
+
+        var hasDefaultFont = agent.isFontInstalled(options.defaultFontName);
+        var defaultFontName = (hasDefaultFont) ? options.defaultFontName : realFontList[0];
+
+        var label = '<span class="note-current-fontname">' +
+                        defaultFontName +
+                     '</span>';
+        return tplButton(label, {
+          title: lang.font.name,
+          className: 'note-fontname',
+          dropdown: tplDropdown(items, 'note-check')
+        });
+      },
+      fontsize: function (lang, options) {
+        var items = options.fontSizes.reduce(function (memo, v) {
+          return memo + '<li><a data-event="fontSize" href="#" data-value="' + v + '">' +
+                          '<i class="' + options.iconPrefix + options.icons.misc.check + '"></i> ' + v +
+                        '</a></li>';
+        }, '');
+
+        var label = '<span class="note-current-fontsize">11</span>';
+        return tplButton(label, {
+          title: lang.font.size,
+          className: 'note-fontsize',
+          dropdown: tplDropdown(items, 'note-check')
+        });
+      },
+      color: function (lang, options) {
+        var colorButtonLabel = '<i class="' +
+                                  options.iconPrefix + options.icons.color.recent +
+                                '" style="color:black;background-color:yellow;"></i>';
+
+        var colorButton = tplButton(colorButtonLabel, {
+          className: 'note-recent-color',
+          title: lang.color.recent,
+          event: 'color',
+          value: '{"backColor":"yellow"}'
+        });
+
+        var items = [
+          '<li><div class="btn-group">',
+          '<div class="note-palette-title">' + lang.color.background + '</div>',
+          '<div class="note-color-reset" data-event="backColor"',
+          ' data-value="inherit" title="' + lang.color.transparent + '">' + lang.color.setTransparent + '</div>',
+          '<div class="note-color-palette" data-target-event="backColor"></div>',
+          '</div><div class="btn-group">',
+          '<div class="note-palette-title">' + lang.color.foreground + '</div>',
+          '<div class="note-color-reset" data-event="foreColor" data-value="inherit" title="' + lang.color.reset + '">',
+          lang.color.resetToDefault,
+          '</div>',
+          '<div class="note-color-palette" data-target-event="foreColor"></div>',
+          '</div></li>'
+        ];
+
+        var moreButton = tplButton('', {
+          title: lang.color.more,
+          dropdown: tplDropdown(items)
+        });
+
+        return colorButton + moreButton;
+      },
+      bold: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.font.bold, {
+          event: 'bold',
+          title: lang.font.bold
+        });
+      },
+      italic: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.font.italic, {
+          event: 'italic',
+          title: lang.font.italic
+        });
+      },
+      underline: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.font.underline, {
+          event: 'underline',
+          title: lang.font.underline
+        });
+      },
+      strikethrough: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.font.strikethrough, {
+          event: 'strikethrough',
+          title: lang.font.strikethrough
+        });
+      },
+      superscript: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.font.superscript, {
+          event: 'superscript',
+          title: lang.font.superscript
+        });
+      },
+      subscript: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.font.subscript, {
+          event: 'subscript',
+          title: lang.font.subscript
+        });
+      },
+      clear: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.font.clear, {
+          event: 'removeFormat',
+          title: lang.font.clear
+        });
+      },
+      ul: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.lists.unordered, {
+          event: 'insertUnorderedList',
+          title: lang.lists.unordered
+        });
+      },
+      ol: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.lists.ordered, {
+          event: 'insertOrderedList',
+          title: lang.lists.ordered
+        });
+      },
+      paragraph: function (lang, options) {
+        var leftButton = tplIconButton(options.iconPrefix + options.icons.paragraph.left, {
+          title: lang.paragraph.left,
+          event: 'justifyLeft'
+        });
+        var centerButton = tplIconButton(options.iconPrefix + options.icons.paragraph.center, {
+          title: lang.paragraph.center,
+          event: 'justifyCenter'
+        });
+        var rightButton = tplIconButton(options.iconPrefix + options.icons.paragraph.right, {
+          title: lang.paragraph.right,
+          event: 'justifyRight'
+        });
+        var justifyButton = tplIconButton(options.iconPrefix + options.icons.paragraph.justify, {
+          title: lang.paragraph.justify,
+          event: 'justifyFull'
+        });
+
+        var outdentButton = tplIconButton(options.iconPrefix + options.icons.paragraph.outdent, {
+          title: lang.paragraph.outdent,
+          event: 'outdent'
+        });
+        var indentButton = tplIconButton(options.iconPrefix + options.icons.paragraph.indent, {
+          title: lang.paragraph.indent,
+          event: 'indent'
+        });
+
+        var dropdown = [
+          '<div class="note-align btn-group">',
+          leftButton + centerButton + rightButton + justifyButton,
+          '</div><div class="note-list btn-group">',
+          indentButton + outdentButton,
+          '</div>'
+        ];
+
+        return tplIconButton(options.iconPrefix + options.icons.paragraph.paragraph, {
+          title: lang.paragraph.paragraph,
+          dropdown: tplDropdown(dropdown, '', 'div')
+        });
+      },
+      height: function (lang, options) {
+        var items = options.lineHeights.reduce(function (memo, v) {
+          return memo + '<li><a data-event="lineHeight" href="#" data-value="' + parseFloat(v) + '">' +
+                          '<i class="' + options.iconPrefix + options.icons.misc.check + '"></i> ' + v +
+                        '</a></li>';
+        }, '');
+
+        return tplIconButton(options.iconPrefix + options.icons.font.height, {
+          title: lang.font.height,
+          dropdown: tplDropdown(items, 'note-check')
+        });
+
+      },
+      help: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.options.help, {
+          event: 'showHelpDialog',
+          title: lang.options.help,
+          hide: true
+        });
+      },
+      fullscreen: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.options.fullscreen, {
+          event: 'fullscreen',
+          title: lang.options.fullscreen
+        });
+      },
+      codeview: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.options.codeview, {
+          event: 'codeview',
+          title: lang.options.codeview
+        });
+      },
+      undo: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.history.undo, {
+          event: 'undo',
+          title: lang.history.undo
+        });
+      },
+      redo: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.history.redo, {
+          event: 'redo',
+          title: lang.history.redo
+        });
+      },
+      hr: function (lang, options) {
+        return tplIconButton(options.iconPrefix + options.icons.hr.insert, {
+          event: 'insertHorizontalRule',
+          title: lang.hr.insert
+        });
+      }
+    };
+
+    var tplPopovers = function (lang, options) {
+      var tplLinkPopover = function () {
+        var linkButton = tplIconButton(options.iconPrefix + options.icons.link.edit, {
+          title: lang.link.edit,
+          event: 'showLinkDialog',
+          hide: true
+        });
+        var unlinkButton = tplIconButton(options.iconPrefix + options.icons.link.unlink, {
+          title: lang.link.unlink,
+          event: 'unlink'
+        });
+        var content = '<a href="http://www.google.com" target="_blank">www.google.com</a>&nbsp;&nbsp;' +
+                      '<div class="note-insert btn-group">' +
+                        linkButton + unlinkButton +
+                      '</div>';
+        return tplPopover('note-link-popover', content);
+      };
+
+      var tplImagePopover = function () {
+        var fullButton = tplButton('<span class="note-fontsize-10">100%</span>', {
+          title: lang.image.resizeFull,
+          event: 'resize',
+          value: '1'
+        });
+        var halfButton = tplButton('<span class="note-fontsize-10">50%</span>', {
+          title: lang.image.resizeHalf,
+          event: 'resize',
+          value: '0.5'
+        });
+        var quarterButton = tplButton('<span class="note-fontsize-10">25%</span>', {
+          title: lang.image.resizeQuarter,
+          event: 'resize',
+          value: '0.25'
+        });
+
+        var leftButton = tplIconButton(options.iconPrefix + options.icons.image.floatLeft, {
+          title: lang.image.floatLeft,
+          event: 'floatMe',
+          value: 'left'
+        });
+        var rightButton = tplIconButton(options.iconPrefix + options.icons.image.floatRight, {
+          title: lang.image.floatRight,
+          event: 'floatMe',
+          value: 'right'
+        });
+        var justifyButton = tplIconButton(options.iconPrefix + options.icons.image.floatNone, {
+          title: lang.image.floatNone,
+          event: 'floatMe',
+          value: 'none'
+        });
+
+        var roundedButton = tplIconButton(options.iconPrefix + options.icons.image.shapeRounded, {
+          title: lang.image.shapeRounded,
+          event: 'imageShape',
+          value: 'img-rounded'
+        });
+        var circleButton = tplIconButton(options.iconPrefix + options.icons.image.shapeCircle, {
+          title: lang.image.shapeCircle,
+          event: 'imageShape',
+          value: 'img-circle'
+        });
+        var thumbnailButton = tplIconButton(options.iconPrefix + options.icons.image.shapeThumbnail, {
+          title: lang.image.shapeThumbnail,
+          event: 'imageShape',
+          value: 'img-thumbnail'
+        });
+        var noneButton = tplIconButton(options.iconPrefix + options.icons.image.shapeNone, {
+          title: lang.image.shapeNone,
+          event: 'imageShape',
+          value: ''
+        });
+
+        var removeButton = tplIconButton(options.iconPrefix + options.icons.image.remove, {
+          title: lang.image.remove,
+          event: 'removeMedia',
+          value: 'none'
+        });
+
+        var content = (options.disableResizeImage ? '' : '<div class="btn-group">' + fullButton + halfButton + quarterButton + '</div>') +
+                      '<div class="btn-group">' + leftButton + rightButton + justifyButton + '</div><br>' +
+                      '<div class="btn-group">' + roundedButton + circleButton + thumbnailButton + noneButton + '</div>' +
+                      '<div class="btn-group">' + removeButton + '</div>';
+        return tplPopover('note-image-popover', content);
+      };
+
+      var tplAirPopover = function () {
+        var $content = $('<div />');
+        for (var idx = 0, len = options.airPopover.length; idx < len; idx ++) {
+          var group = options.airPopover[idx];
+
+          var $group = $('<div class="note-' + group[0] + ' btn-group">');
+          for (var i = 0, lenGroup = group[1].length; i < lenGroup; i++) {
+            var $button = $(tplButtonInfo[group[1][i]](lang, options));
+
+            $button.attr('data-name', group[1][i]);
+
+            $group.append($button);
+          }
+          $content.append($group);
+        }
+
+        return tplPopover('note-air-popover', $content.children());
+      };
+
+      var $notePopover = $('<div class="note-popover" />');
+
+      $notePopover.append(tplLinkPopover());
+      $notePopover.append(tplImagePopover());
+
+      if (options.airMode) {
+        $notePopover.append(tplAirPopover());
+      }
+
+      return $notePopover;
+    };
+
+    var tplHandles = function (options) {
+      return '<div class="note-handle">' +
+               '<div class="note-control-selection">' +
+                 '<div class="note-control-selection-bg"></div>' +
+                 '<div class="note-control-holder note-control-nw"></div>' +
+                 '<div class="note-control-holder note-control-ne"></div>' +
+                 '<div class="note-control-holder note-control-sw"></div>' +
+                 '<div class="' +
+                 (options.disableResizeImage ? 'note-control-holder' : 'note-control-sizing') +
+                 ' note-control-se"></div>' +
+                 (options.disableResizeImage ? '' : '<div class="note-control-selection-info"></div>') +
+               '</div>' +
+             '</div>';
+    };
+
+    /**
+     * shortcut table template
+     * @param {String} title
+     * @param {String} body
+     */
+    var tplShortcut = function (title, keys) {
+      var keyClass = 'note-shortcut-col col-xs-6 note-shortcut-';
+      var body = [];
+
+      for (var i in keys) {
+        if (keys.hasOwnProperty(i)) {
+          body.push(
+            '<div class="' + keyClass + 'key">' + keys[i].kbd + '</div>' +
+            '<div class="' + keyClass + 'name">' + keys[i].text + '</div>'
+            );
+        }
+      }
+
+      return '<div class="note-shortcut-row row"><div class="' + keyClass + 'title col-xs-offset-6">' + title + '</div></div>' +
+             '<div class="note-shortcut-row row">' + body.join('</div><div class="note-shortcut-row row">') + '</div>';
+    };
+
+    var tplShortcutText = function (lang) {
+      var keys = [
+        { kbd: '⌘ + B', text: lang.font.bold },
+        { kbd: '⌘ + I', text: lang.font.italic },
+        { kbd: '⌘ + U', text: lang.font.underline },
+        { kbd: '⌘ + \\', text: lang.font.clear }
+      ];
+
+      return tplShortcut(lang.shortcut.textFormatting, keys);
+    };
+
+    var tplShortcutAction = function (lang) {
+      var keys = [
+        { kbd: '⌘ + Z', text: lang.history.undo },
+        { kbd: '⌘ + ⇧ + Z', text: lang.history.redo },
+        { kbd: '⌘ + ]', text: lang.paragraph.indent },
+        { kbd: '⌘ + [', text: lang.paragraph.outdent },
+        { kbd: '⌘ + ENTER', text: lang.hr.insert }
+      ];
+
+      return tplShortcut(lang.shortcut.action, keys);
+    };
+
+    var tplShortcutPara = function (lang) {
+      var keys = [
+        { kbd: '⌘ + ⇧ + L', text: lang.paragraph.left },
+        { kbd: '⌘ + ⇧ + E', text: lang.paragraph.center },
+        { kbd: '⌘ + ⇧ + R', text: lang.paragraph.right },
+        { kbd: '⌘ + ⇧ + J', text: lang.paragraph.justify },
+        { kbd: '⌘ + ⇧ + NUM7', text: lang.lists.ordered },
+        { kbd: '⌘ + ⇧ + NUM8', text: lang.lists.unordered }
+      ];
+
+      return tplShortcut(lang.shortcut.paragraphFormatting, keys);
+    };
+
+    var tplShortcutStyle = function (lang) {
+      var keys = [
+        { kbd: '⌘ + NUM0', text: lang.style.normal },
+        { kbd: '⌘ + NUM1', text: lang.style.h1 },
+        { kbd: '⌘ + NUM2', text: lang.style.h2 },
+        { kbd: '⌘ + NUM3', text: lang.style.h3 },
+        { kbd: '⌘ + NUM4', text: lang.style.h4 },
+        { kbd: '⌘ + NUM5', text: lang.style.h5 },
+        { kbd: '⌘ + NUM6', text: lang.style.h6 }
+      ];
+
+      return tplShortcut(lang.shortcut.documentStyle, keys);
+    };
+
+    var tplExtraShortcuts = function (lang, options) {
+      var extraKeys = options.extraKeys;
+      var keys = [];
+
+      for (var key in extraKeys) {
+        if (extraKeys.hasOwnProperty(key)) {
+          keys.push({ kbd: key, text: extraKeys[key] });
+        }
+      }
+
+      return tplShortcut(lang.shortcut.extraKeys, keys);
+    };
+
+    var tplShortcutTable = function (lang, options) {
+      var colClass = 'class="note-shortcut note-shortcut-col col-sm-6 col-xs-12"';
+      var template = [
+        '<div ' + colClass + '>' + tplShortcutAction(lang, options) + '</div>' +
+        '<div ' + colClass + '>' + tplShortcutText(lang, options) + '</div>',
+        '<div ' + colClass + '>' + tplShortcutStyle(lang, options) + '</div>' +
+        '<div ' + colClass + '>' + tplShortcutPara(lang, options) + '</div>'
+      ];
+
+      if (options.extraKeys) {
+        template.push('<div ' + colClass + '>' + tplExtraShortcuts(lang, options) + '</div>');
+      }
+
+      return '<div class="note-shortcut-row row">' +
+               template.join('</div><div class="note-shortcut-row row">') +
+             '</div>';
+    };
+
+    var replaceMacKeys = function (sHtml) {
+      return sHtml.replace(/⌘/g, 'Ctrl').replace(/⇧/g, 'Shift');
+    };
+
+    var tplDialogInfo = {
+      image: function (lang, options) {
+        var imageLimitation = '';
+        if (options.maximumImageFileSize) {
+          var unit = Math.floor(Math.log(options.maximumImageFileSize) / Math.log(1024));
+          var readableSize = (options.maximumImageFileSize / Math.pow(1024, unit)).toFixed(2) * 1 +
+                             ' ' + ' KMGTP'[unit] + 'B';
+          imageLimitation = '<small>' + lang.image.maximumFileSize + ' : ' + readableSize + '</small>';
+        }
+
+        var body = '<div class="form-group row note-group-select-from-files">' +
+                     '<label>' + lang.image.selectFromFiles + '</label>' +
+                     '<input class="note-image-input form-control" type="file" name="files" accept="image/*" multiple="multiple" />' +
+                     imageLimitation +
+                   '</div>' +
+                   '<div class="form-group row">' +
+                     '<label>' + lang.image.url + '</label>' +
+                     '<input class="note-image-url form-control col-md-12" type="text" />' +
+                   '</div>';
+        var footer = '<button href="#" class="btn btn-primary note-image-btn disabled" disabled>' + lang.image.insert + '</button>';
+        return tplDialog('note-image-dialog', lang.image.insert, body, footer);
+      },
+
+      link: function (lang, options) {
+        var body = '<div class="form-group row">' +
+                     '<label>' + lang.link.textToDisplay + '</label>' +
+                     '<input class="note-link-text form-control col-md-12" type="text" />' +
+                   '</div>' +
+                   '<div class="form-group row">' +
+                     '<label>' + lang.link.url + '</label>' +
+                     '<input class="note-link-url form-control col-md-12" type="text" value="http://" />' +
+                   '</div>' +
+                   (!options.disableLinkTarget ?
+                     '<div class="checkbox">' +
+                       '<label>' + '<input type="checkbox" checked> ' +
+                         lang.link.openInNewWindow +
+                       '</label>' +
+                     '</div>' : ''
+                   );
+        var footer = '<button href="#" class="btn btn-primary note-link-btn disabled" disabled>' + lang.link.insert + '</button>';
+        return tplDialog('note-link-dialog', lang.link.insert, body, footer);
+      },
+
+      help: function (lang, options) {
+        var body = '<a class="modal-close pull-right" aria-hidden="true" tabindex="-1">' + lang.shortcut.close + '</a>' +
+                   '<div class="title">' + lang.shortcut.shortcuts + '</div>' +
+                   (agent.isMac ? tplShortcutTable(lang, options) : replaceMacKeys(tplShortcutTable(lang, options))) +
+                   '<p class="text-center">' +
+                     '<a href="//summernote.org/" target="_blank">Summernote 0.6.16</a> · ' +
+                     '<a href="//github.com/summernote/summernote" target="_blank">Project</a> · ' +
+                     '<a href="//github.com/summernote/summernote/issues" target="_blank">Issues</a>' +
+                   '</p>';
+        return tplDialog('note-help-dialog', '', body, '');
+      }
+    };
+
+    var tplDialogs = function (lang, options) {
+      var dialogs = '';
+
+      $.each(tplDialogInfo, function (idx, tplDialog) {
+        dialogs += tplDialog(lang, options);
+      });
+
+      return '<div class="note-dialog">' + dialogs + '</div>';
+    };
+
+    var tplStatusbar = function () {
+      return '<div class="note-resizebar">' +
+               '<div class="note-icon-bar"></div>' +
+               '<div class="note-icon-bar"></div>' +
+               '<div class="note-icon-bar"></div>' +
+             '</div>';
+    };
+
+    var representShortcut = function (str) {
+      if (agent.isMac) {
+        str = str.replace('CMD', '⌘').replace('SHIFT', '⇧');
+      }
+
+      return str.replace('BACKSLASH', '\\')
+                .replace('SLASH', '/')
+                .replace('LEFTBRACKET', '[')
+                .replace('RIGHTBRACKET', ']');
+    };
+
+    /**
+     * createTooltip
+     *
+     * @param {jQuery} $container
+     * @param {Object} keyMap
+     * @param {String} [sPlacement]
+     */
+    var createTooltip = function ($container, keyMap, sPlacement) {
+      var invertedKeyMap = func.invertObject(keyMap);
+      var $buttons = $container.find('button');
+
+      $buttons.each(function (i, elBtn) {
+        var $btn = $(elBtn);
+        var sShortcut = invertedKeyMap[$btn.data('event')];
+        if (sShortcut) {
+          $btn.attr('title', function (i, v) {
+            return v + ' (' + representShortcut(sShortcut) + ')';
+          });
+        }
+      // bootstrap tooltip on btn-group bug
+      // https://github.com/twbs/bootstrap/issues/5687
+      }).tooltip({
+        container: 'body',
+        trigger: 'hover',
+        placement: sPlacement || 'top'
+      }).on('click', function () {
+        $(this).tooltip('hide');
+      });
+    };
+
+    // createPalette
+    var createPalette = function ($container, options) {
+      var colorInfo = options.colors;
+      $container.find('.note-color-palette').each(function () {
+        var $palette = $(this), eventName = $palette.attr('data-target-event');
+        var paletteContents = [];
+        for (var row = 0, lenRow = colorInfo.length; row < lenRow; row++) {
+          var colors = colorInfo[row];
+          var buttons = [];
+          for (var col = 0, lenCol = colors.length; col < lenCol; col++) {
+            var color = colors[col];
+            buttons.push(['<button type="button" class="note-color-btn" style="background-color:', color,
+                           ';" data-event="', eventName,
+                           '" data-value="', color,
+                           '" title="', color,
+                           '" data-toggle="button" tabindex="-1"></button>'].join(''));
+          }
+          paletteContents.push('<div class="note-color-row">' + buttons.join('') + '</div>');
+        }
+        $palette.html(paletteContents.join(''));
+      });
+    };
+
+    /**
+     * create summernote layout (air mode)
+     *
+     * @param {jQuery} $holder
+     * @param {Object} options
+     */
+    this.createLayoutByAirMode = function ($holder, options) {
+      var langInfo = options.langInfo;
+      var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
+      var id = func.uniqueId();
+
+      $holder.addClass('note-air-editor note-editable panel-body');
+      $holder.attr({
+        'id': 'note-editor-' + id,
+        'contentEditable': true
+      });
+
+      var body = document.body;
+
+      // create Popover
+      var $popover = $(tplPopovers(langInfo, options));
+      $popover.addClass('note-air-layout');
+      $popover.attr('id', 'note-popover-' + id);
+      $popover.appendTo(body);
+      createTooltip($popover, keyMap);
+      createPalette($popover, options);
+
+      // create Handle
+      var $handle = $(tplHandles(options));
+      $handle.addClass('note-air-layout');
+      $handle.attr('id', 'note-handle-' + id);
+      $handle.appendTo(body);
+
+      // create Dialog
+      var $dialog = $(tplDialogs(langInfo, options));
+      $dialog.addClass('note-air-layout');
+      $dialog.attr('id', 'note-dialog-' + id);
+      $dialog.find('button.close, a.modal-close').click(function () {
+        $(this).closest('.modal').modal('hide');
+      });
+      $dialog.appendTo(body);
+    };
+
+    /**
+     * create summernote layout (normal mode)
+     *
+     * @param {jQuery} $holder
+     * @param {Object} options
+     */
+    this.createLayoutByFrame = function ($holder, options) {
+      var langInfo = options.langInfo;
+
+      //01. create Editor
+      var $editor = $('<div class="note-editor panel panel-default" />');
+      if (options.width) {
+        $editor.width(options.width);
+      }
+
+      //02. statusbar (resizebar)
+      if (options.height > 0) {
+        $('<div class="note-statusbar">' + (options.disableResizeEditor ? '' : tplStatusbar()) + '</div>').prependTo($editor);
+      }
+
+      //03 editing area
+      var $editingArea = $('<div class="note-editing-area" />');
+      //03. create editable
+      var isContentEditable = !$holder.is(':disabled');
+      var $editable = $('<div class="note-editable panel-body" contentEditable="' + isContentEditable + '"></div>').prependTo($editingArea);
+      
+      if (options.height) {
+        $editable.height(options.height);
+      }
+      if (options.direction) {
+        $editable.attr('dir', options.direction);
+      }
+      var placeholder = $holder.attr('placeholder') || options.placeholder;
+      if (placeholder) {
+        $editable.attr('data-placeholder', placeholder);
+      }
+
+      $editable.html(dom.html($holder) || dom.emptyPara);
+
+      //031. create codable
+      $('<textarea class="note-codable"></textarea>').prependTo($editingArea);
+
+      //04. create Popover
+      var $popover = $(tplPopovers(langInfo, options)).prependTo($editingArea);
+      createPalette($popover, options);
+      createTooltip($popover, keyMap);
+
+      //05. handle(control selection, ...)
+      $(tplHandles(options)).prependTo($editingArea);
+
+      $editingArea.prependTo($editor);
+
+      //06. create Toolbar
+      var $toolbar = $('<div class="note-toolbar panel-heading" />');
+      for (var idx = 0, len = options.toolbar.length; idx < len; idx ++) {
+        var groupName = options.toolbar[idx][0];
+        var groupButtons = options.toolbar[idx][1];
+
+        var $group = $('<div class="note-' + groupName + ' btn-group" />');
+        for (var i = 0, btnLength = groupButtons.length; i < btnLength; i++) {
+          var buttonInfo = tplButtonInfo[groupButtons[i]];
+          // continue creating toolbar even if a button doesn't exist
+          if (!$.isFunction(buttonInfo)) { continue; }
+
+          var $button = $(buttonInfo(langInfo, options));
+          $button.attr('data-name', groupButtons[i]);  // set button's alias, becuase to get button element from $toolbar
+          $group.append($button);
+        }
+        $toolbar.append($group);
+      }
+
+      var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
+      createPalette($toolbar, options);
+      createTooltip($toolbar, keyMap, 'bottom');
+      $toolbar.prependTo($editor);
+
+      //07. create Dropzone
+      $('<div class="note-dropzone"><div class="note-dropzone-message"></div></div>').prependTo($editor);
+
+      //08. create Dialog
+      var $dialogContainer = options.dialogsInBody ? $(document.body) : $editor;
+      var $dialog = $(tplDialogs(langInfo, options)).prependTo($dialogContainer);
+      $dialog.find('button.close, a.modal-close').click(function () {
+        $(this).closest('.modal').modal('hide');
+      });
+
+      //09. Editor/Holder switch
+      $editor.insertAfter($holder);
+      $holder.hide();
+    };
+
+    this.hasNoteEditor = function ($holder) {
+      return this.noteEditorFromHolder($holder).length > 0;
+    };
+
+    this.noteEditorFromHolder = function ($holder) {
+      if ($holder.hasClass('note-air-editor')) {
+        return $holder;
+      } else if ($holder.next().hasClass('note-editor')) {
+        return $holder.next();
+      } else {
+        return $();
+      }
+    };
+
+    /**
+     * create summernote layout
+     *
+     * @param {jQuery} $holder
+     * @param {Object} options
+     */
+    this.createLayout = function ($holder, options) {
+      if (options.airMode) {
+        this.createLayoutByAirMode($holder, options);
+      } else {
+        this.createLayoutByFrame($holder, options);
+      }
+    };
+
+    /**
+     * returns layoutInfo from holder
+     *
+     * @param {jQuery} $holder - placeholder
+     * @return {Object}
+     */
+    this.layoutInfoFromHolder = function ($holder) {
+      var $editor = this.noteEditorFromHolder($holder);
+      if (!$editor.length) {
+        return;
+      }
+
+      // connect $holder to $editor
+      $editor.data('holder', $holder);
+
+      return dom.buildLayoutInfo($editor);
+    };
+
+    /**
+     * removeLayout
+     *
+     * @param {jQuery} $holder - placeholder
+     * @param {Object} layoutInfo
+     * @param {Object} options
+     *
+     */
+    this.removeLayout = function ($holder, layoutInfo, options) {
+      if (options.airMode) {
+        $holder.removeClass('note-air-editor note-editable')
+               .removeAttr('id contentEditable');
+
+        layoutInfo.popover().remove();
+        layoutInfo.handle().remove();
+        layoutInfo.dialog().remove();
+      } else {
+        $holder.html(layoutInfo.editable().html());
+
+        if (options.dialogsInBody) {
+          layoutInfo.dialog().remove();
+        }
+        layoutInfo.editor().remove();
+        $holder.show();
+      }
+    };
+
+    /**
+     *
+     * @return {Object}
+     * @return {function(label, options=):string} return.button {@link #tplButton function to make text button}
+     * @return {function(iconClass, options=):string} return.iconButton {@link #tplIconButton function to make icon button}
+     * @return {function(className, title=, body=, footer=):string} return.dialog {@link #tplDialog function to make dialog}
+     */
+    this.getTemplate = function () {
+      return {
+        button: tplButton,
+        iconButton: tplIconButton,
+        dialog: tplDialog
+      };
+    };
+
+    /**
+     * add button information
+     *
+     * @param {String} name button name
+     * @param {Function} buttonInfo function to make button, reference to {@link #tplButton},{@link #tplIconButton}
+     */
+    this.addButtonInfo = function (name, buttonInfo) {
+      tplButtonInfo[name] = buttonInfo;
+    };
+
+    /**
+     *
+     * @param {String} name
+     * @param {Function} dialogInfo function to make dialog, reference to {@link #tplDialog}
+     */
+    this.addDialogInfo = function (name, dialogInfo) {
+      tplDialogInfo[name] = dialogInfo;
+    };
+  };
+
+
+  // jQuery namespace for summernote
+  /**
+   * @class $.summernote 
+   * 
+   * summernote attribute  
+   * 
+   * @mixin defaults
+   * @singleton  
+   * 
+   */
+  $.summernote = $.summernote || {};
+
+  // extends default settings
+  //  - $.summernote.version
+  //  - $.summernote.options
+  //  - $.summernote.lang
+  $.extend($.summernote, defaults);
+
+  var renderer = new Renderer();
+  var eventHandler = new EventHandler();
+
+  $.extend($.summernote, {
+    /** @property {Renderer} */
+    renderer: renderer,
+    /** @property {EventHandler} */
+    eventHandler: eventHandler,
+    /** 
+     * @property {Object} core 
+     * @property {core.agent} core.agent 
+     * @property {core.dom} core.dom
+     * @property {core.range} core.range 
+     */
+    core: {
+      agent: agent,
+      list : list,
+      dom: dom,
+      range: range
+    },
+    /** 
+     * @property {Object} 
+     * pluginEvents event list for plugins
+     * event has name and callback function.
+     * 
+     * ``` 
+     * $.summernote.addPlugin({
+     *     events : {
+     *          'hello' : function(layoutInfo, value, $target) {
+     *              console.log('event name is hello, value is ' + value );
+     *          }
+     *     }     
+     * })
+     * ```
+     * 
+     * * event name is data-event property.
+     * * layoutInfo is a summernote layout information.
+     * * value is data-value property.
+     */
+    pluginEvents: {},
+
+    plugins : []
+  });
+
+  /**
+   * @method addPlugin
+   *
+   * add Plugin in Summernote 
+   * 
+   * Summernote can make a own plugin.
+   *
+   * ### Define plugin
+   * ```
+   * // get template function  
+   * var tmpl = $.summernote.renderer.getTemplate();
+   * 
+   * // add a button   
+   * $.summernote.addPlugin({
+   *     buttons : {
+   *        // "hello"  is button's namespace.      
+   *        "hello" : function(lang, options) {
+   *            // make icon button by template function          
+   *            return tmpl.iconButton(options.iconPrefix + 'header', {
+   *                // callback function name when button clicked 
+   *                event : 'hello',
+   *                // set data-value property                 
+   *                value : 'hello',                
+   *                hide : true
+   *            });           
+   *        }
+   *     
+   *     }, 
+   *     
+   *     events : {
+   *        "hello" : function(layoutInfo, value) {
+   *            // here is event code 
+   *        }
+   *     }     
+   * });
+   * ``` 
+   * ### Use a plugin in toolbar
+   * 
+   * ``` 
+   *    $("#editor").summernote({
+   *    ...
+   *    toolbar : [
+   *        // display hello plugin in toolbar     
+   *        ['group', [ 'hello' ]]
+   *    ]
+   *    ...    
+   *    });
+   * ```
+   *  
+   *  
+   * @param {Object} plugin
+   * @param {Object} [plugin.buttons] define plugin button. for detail, see to Renderer.addButtonInfo
+   * @param {Object} [plugin.dialogs] define plugin dialog. for detail, see to Renderer.addDialogInfo
+   * @param {Object} [plugin.events] add event in $.summernote.pluginEvents 
+   * @param {Object} [plugin.langs] update $.summernote.lang
+   * @param {Object} [plugin.options] update $.summernote.options
+   */
+  $.summernote.addPlugin = function (plugin) {
+
+    // save plugin list
+    $.summernote.plugins.push(plugin);
+
+    if (plugin.buttons) {
+      $.each(plugin.buttons, function (name, button) {
+        renderer.addButtonInfo(name, button);
+      });
+    }
+
+    if (plugin.dialogs) {
+      $.each(plugin.dialogs, function (name, dialog) {
+        renderer.addDialogInfo(name, dialog);
+      });
+    }
+
+    if (plugin.events) {
+      $.each(plugin.events, function (name, event) {
+        $.summernote.pluginEvents[name] = event;
+      });
+    }
+
+    if (plugin.langs) {
+      $.each(plugin.langs, function (locale, lang) {
+        if ($.summernote.lang[locale]) {
+          $.extend($.summernote.lang[locale], lang);
+        }
+      });
+    }
+
+    if (plugin.options) {
+      $.extend($.summernote.options, plugin.options);
+    }
+  };
+
+  /*
+   * extend $.fn
+   */
+  $.fn.extend({
+    /**
+     * @method
+     * Initialize summernote
+     *  - create editor layout and attach Mouse and keyboard events.
+     * 
+     * ```
+     * $("#summernote").summernote( { options ..} );
+     * ```
+     *   
+     * @member $.fn
+     * @param {Object|String} options reference to $.summernote.options
+     * @return {this}
+     */
+    summernote: function () {
+      // check first argument's type
+      //  - {String}: External API call {{module}}.{{method}}
+      //  - {Object}: init options
+      var type = $.type(list.head(arguments));
+      var isExternalAPICalled = type === 'string';
+      var hasInitOptions = type === 'object';
+
+      // extend default options with custom user options
+      var options = hasInitOptions ? list.head(arguments) : {};
+
+      options = $.extend({}, $.summernote.options, options);
+      options.icons = $.extend({}, $.summernote.options.icons, options.icons);
+
+      // Include langInfo in options for later use, e.g. for image drag-n-drop
+      // Setup language info with en-US as default
+      options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]);
+
+      // override plugin options
+      if (!isExternalAPICalled && hasInitOptions) {
+        for (var i = 0, len = $.summernote.plugins.length; i < len; i++) {
+          var plugin = $.summernote.plugins[i];
+
+          if (options.plugin[plugin.name]) {
+            $.summernote.plugins[i] = $.extend(true, plugin, options.plugin[plugin.name]);
+          }
+        }
+      }
+
+      this.each(function (idx, holder) {
+        var $holder = $(holder);
+
+        // if layout isn't created yet, createLayout and attach events
+        if (!renderer.hasNoteEditor($holder)) {
+          renderer.createLayout($holder, options);
+
+          var layoutInfo = renderer.layoutInfoFromHolder($holder);
+          $holder.data('layoutInfo', layoutInfo);
+
+          eventHandler.attach(layoutInfo, options);
+          eventHandler.attachCustomEvent(layoutInfo, options);
+        }
+      });
+
+      var $first = this.first();
+      if ($first.length) {
+        var layoutInfo = renderer.layoutInfoFromHolder($first);
+
+        // external API
+        if (isExternalAPICalled) {
+          var moduleAndMethod = list.head(list.from(arguments));
+          var args = list.tail(list.from(arguments));
+
+          // TODO now external API only works for editor
+          var params = [moduleAndMethod, layoutInfo.editable()].concat(args);
+          return eventHandler.invoke.apply(eventHandler, params);
+        } else if (options.focus) {
+          // focus on first editable element for initialize editor
+          layoutInfo.editable().focus();
+        }
+      }
+
+      return this;
+    },
+
+    /**
+     * @method 
+     * 
+     * get the HTML contents of note or set the HTML contents of note.
+     *
+     * * get contents 
+     * ```
+     * var content = $("#summernote").code();
+     * ```
+     * * set contents 
+     *
+     * ```
+     * $("#summernote").code(html);
+     * ```
+     *
+     * @member $.fn 
+     * @param {String} [html] - HTML contents(optional, set)
+     * @return {this|String} - context(set) or HTML contents of note(get).
+     */
+    code: function (html) {
+      // get the HTML contents of note
+      if (html === undefined) {
+        var $holder = this.first();
+        if (!$holder.length) {
+          return;
+        }
+
+        var layoutInfo = renderer.layoutInfoFromHolder($holder);
+        var $editable = layoutInfo && layoutInfo.editable();
+
+        if ($editable && $editable.length) {
+          var isCodeview = eventHandler.invoke('codeview.isActivated', layoutInfo);
+          eventHandler.invoke('codeview.sync', layoutInfo);
+          return isCodeview ? layoutInfo.codable().val() :
+                              layoutInfo.editable().html();
+        }
+        return dom.value($holder);
+      }
+
+      // set the HTML contents of note
+      this.each(function (i, holder) {
+        var layoutInfo = renderer.layoutInfoFromHolder($(holder));
+        var $editable = layoutInfo && layoutInfo.editable();
+        if ($editable) {
+          $editable.html(html);
+        }
+      });
+
+      return this;
+    },
+
+    /**
+     * @method
+     * 
+     * destroy Editor Layout and detach Key and Mouse Event
+     *
+     * @member $.fn
+     * @return {this}
+     */
+    destroy: function () {
+      this.each(function (idx, holder) {
+        var $holder = $(holder);
+
+        if (!renderer.hasNoteEditor($holder)) {
+          return;
+        }
+
+        var info = renderer.layoutInfoFromHolder($holder);
+        var options = info.editor().data('options');
+
+        eventHandler.detach(info, options);
+        renderer.removeLayout($holder, info, options);
+      });
+
+      return this;
+    }
+  });
+}));

Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
bower_components/summernote/dist/summernote.min.js


+ 144 - 26
chat.php

@@ -113,46 +113,164 @@ endif;
         <script src="bower_components/respondJs/dest/respond.min.js"></script>
         <![endif]-->
         <style>
-            <?php if(CUSTOMCSS == "true") : 
-$template_file = "custom.css";
-$file_handle = fopen($template_file, "rb");
-echo fread($file_handle, filesize($template_file));
-fclose($file_handle);
-echo "\n";
-endif; ?>        
+            .offline{
+                -webkit-filter: grayscale; /*sepia, hue-rotate, invert....*/
+                -webkit-filter: brightness(25%);
+            }
+            <?php customCSS(); ?>      
         </style>
     </head>
 
-    <body class="scroller-body" style="padding: 0px;">
+    <body id="chat" class="scroller-body" style="padding: 0px;">
+        
+        <!-- D A T A B A S E -->
+        
+        <?php
+        
+            $dbcreated = false;
+        
+            if (!extension_loaded("SQLITE3")){ 
+
+                echo '<div class="row" style="margin: 0"><div class="panel panel-danger" style="margin: 10px";>';
+                echo '<div class="panel-heading">';
+                echo '<h3 class="panel-title">SQLITE3</h3>';
+                echo '</div>';
+                echo '<div style="color: gray" class="panel-body">';
+				echo getError(getOS(),'sqlite3');
+
+                echo '</div></div></div>';
+                die();
+
+            }  
+        
+            if( $db = new SQLite3("chatpack.db") )
+            {
+                if( $db->busyTimeout(5000) )
+                {
+                    if( $db->exec("PRAGMA journal_mode = wal;") )
+                    {
+                        $logtable = "CREATE TABLE IF NOT EXISTS chatpack_log
+                                     (id INTEGER PRIMARY KEY,
+                                     timestamp INTEGER NOT NULL,
+                                     user TEXT NOT NULL,
+                                     avatar TEXT NOT NULL,
+                                     message TEXT NOT NULL,
+                                     liked INTEGER DEFAULT 0)";
+
+                        if( $db->exec($logtable) )
+                        {
+                            $usertable = "CREATE TABLE IF NOT EXISTS chatpack_typing
+                                          (id INTEGER PRIMARY KEY,
+                                          timestamp INTEGER NOT NULL,
+                                          user TEXT NOT NULL)";
+                            
+                            $onlinetable = "CREATE TABLE IF NOT EXISTS chatpack_last_message
+                                          (
+                                          user TEXT PRIMARY KEY NOT NULL,
+                                          timestamp INTEGER NOT NULL,
+                                          avatar TEXT NOT NULL)";
+
+                            if( $db->exec($usertable) && $db->exec($onlinetable) )
+                            {
+                                $dbcreated = true;
+                            }
+                            else
+                            {
+                                errormessage("creating database table for typing");
+                            }
+                        }
+                        else
+                        {
+                            errormessage("creating database table for messages");
+                        }
+
+                        if( !$db->close() )
+                        {
+                            errormessage("closing database connection");
+                        }
+                    }
+                    else
+                    {
+                        errormessage("setting journal mode");
+                    }
+                }
+                else
+                {
+                    errormessage("setting busy timeout");
+                }
+            }
+            else
+            {
+                errormessage("using SQLite");
+            }
+        
+            if( $dbcreated )
+            {
+
+        ?>
+        
         <div class="main-wrapper" style="position: initial;">
             <div id="content" class="container-fluid">
                 <br>
                 <div class="row">
-                    <div class="col-lg-12">
-                        <div class="content-box big-box chat">
-                            <div class="content-title i-block">
-                                <h4 class="zero-m">Welcome To The Chat <?php echo $USER->username;?></h4>
-                            </div>
-                            <div class="box" style="overflow: hidden; width: auto; height: 500px;">
+                    <div class="col-lg-10">
+                        <div class="content-box big-box chat gray-bg">
+                            <div class="box" style="overflow: hidden; width: auto; height: 550px;">
+                                <div id="intro">
+                                    <center><img class="logo" alt="logo" src="images/organizr-logo-h.png">
+                                    <br><br>start chatting...</center>
+                                </div>
                                 <ul id="messages" class="chat-double chat-container"></ul>
+                                <ul class="chat-double chat-container" style="padding: 0px;"><li id="istyping"></li></ul>
+                            </div>
+                            <br/>
+                            <input id="message" autofocus type="text" class="form-control gray-bg" placeholder="Enter your text" autocomplete="off"/>
+                            <audio id="tabalert" preload="auto">
+                                <source src="chat/audio/newmessage.mp3" type="audio/mpeg">
+                            </audio>
+
+                        </div>
+                    </div>
+                    <div class="col-lg-2">
+                        <div class="content-box">
+                            <div class="content-title big-box i-block gray-bg">
+                                <h4 class="zero-m">Online</h4>
                             </div>
-                            <form id="message_form">             
-                                <input id="writehere" type="text" class="form-control" placeholder="Enter your text">
-                            </form>
+                            <div class="clearfix"></div>
+                            <div id="onlineusers" class="big-box"></div>
                         </div>
                     </div>
                 </div>
+
             </div>    
         </div>
+        
+        <?php
+            
+            }
+        
+            function errormessage($msg)
+            {
+                echo "<div style=\"margin-top: 50px;\">";
+                echo "<span style=\"color:#d89334;\">error </span>";
+                echo $msg;
+                echo "</div>";
+            }
+                
+        ?>
     </body>
-    
-      <script>
 
-          $(".box").niceScroll({
-                railpadding: {top:0,right:0,left:0,bottom:0},
-                scrollspeed: 30,
-                mousescrollstep: 60
-            });
-  </script>
+    <script>
+        $(".box").niceScroll({
+            railpadding: {top:0,right:0,left:0,bottom:0},
+            scrollspeed: 30,
+            mousescrollstep: 60
+        });
+        $("#onlineusers").niceScroll({
+            railpadding: {top:0,right:0,left:0,bottom:0},
+            scrollspeed: 30,
+            mousescrollstep: 60
+        });
+    </script>
 
-</html>
+</html>

BIN
chat/audio/newmessage.mp3


+ 66 - 0
chat/checkuser.php

@@ -0,0 +1,66 @@
+<?php
+
+$user = $_POST["user"];
+
+if( $db = new SQLite3("../chatpack.db") )
+{
+    if( $db->busyTimeout(5000) )
+    {
+        if( $db->exec("PRAGMA journal_mode = wal;") )
+        {
+            if( $result = @$db->query("SELECT timestamp, user FROM chatpack_log
+                                       WHERE LOWER(user)= LOWER('$user') ORDER BY id DESC") )
+            {
+                if( $row = $result->fetchArray() )
+                {
+                    $timestamp = $row["timestamp"];
+                    $timenow = time();
+
+                    if( $timestamp < $timenow - 2700 )  // user's last message too old means user offline
+                    {
+                        echo "success";
+                    }
+                    else  // user's last message young enough means user still online
+                    {
+                        echo "usernametaken";
+                    }
+                }
+                else  // username available
+                {
+                    echo "success";
+                }
+            }
+            else
+            {
+                errormessage("querying database while checking user");
+            }
+
+            if( !@$db->close() )
+            {
+                errormessage("closing database connection after checking user");
+            }
+        }
+        else
+        {
+            errormessage("setting journal mode");
+        }
+    }
+    else
+    {
+        errormessage("setting busy timeout");
+    }
+}
+else
+{
+    errormessage("using SQLite");
+}
+
+function errormessage($msg)
+{
+    echo "<div style=\"margin-top: 50px;\">";
+    echo "<span style=\"color:#d89334;\">error </span>";
+    echo $msg;
+    echo "</div>";
+}
+
+?>

+ 7 - 0
chat/connect.php

@@ -0,0 +1,7 @@
+<?php
+
+$db = new SQLite3("../chatpack.db");
+$db->busyTimeout(5000);
+$db->exec("PRAGMA journal_mode = wal;");
+
+?>

+ 36 - 0
chat/getlikes.php

@@ -0,0 +1,36 @@
+<?php
+
+include("connect.php");
+   
+$likedmessages = array();
+$unlikedmessages = array();
+
+if( $result = $db->query("SELECT id, liked FROM chatpack_log") )
+{
+    while( $row = $result->fetchArray() )
+    {
+        $messageid = $row["id"];
+        $liked = $row["liked"];
+
+        if( $liked == 1 )
+        {
+            array_push($likedmessages, $messageid);
+        }
+        else if( $liked == 0 )
+        {
+            array_push($unlikedmessages, $messageid);
+        }
+    }
+}
+
+$db->close();
+
+// pass likes and unlikes back to chat.js
+
+$likes = json_encode($likedmessages);
+$unlikes = json_encode($unlikedmessages);
+$likesandunlikes = $likes . "#" . $unlikes;
+
+echo $likesandunlikes;
+
+?>

+ 26 - 0
chat/getonline.php

@@ -0,0 +1,26 @@
+<?php
+
+include("connect.php");
+
+$onlineusers = array();
+
+if( $result = $db->query("SELECT user, timestamp, avatar FROM chatpack_last_message ORDER BY user ASC") )
+{
+    while( $row = $result->fetchArray() )
+    {            
+        $user = $row["user"];
+        $timestamp = $row["timestamp"];
+        $avatar = $row["avatar"];
+        $push = array($user, $timestamp, $avatar);
+
+        array_push($onlineusers, $push);
+    }
+}
+
+$db->close();
+
+// pass online users as JSON back to chat.js
+
+echo json_encode($onlineusers);
+
+?>

+ 23 - 0
chat/gettyping.php

@@ -0,0 +1,23 @@
+<?php
+
+include("connect.php");
+
+$typingusers = array();
+
+if( $result = $db->query("SELECT user FROM chatpack_typing") )
+{
+    while( $row = $result->fetchArray() )
+    {            
+        $user = $row["user"];
+
+        array_push($typingusers, $user);
+    }
+}
+
+$db->close();
+
+// pass typing users as JSON back to chat.js
+
+echo json_encode($typingusers);
+
+?>

BIN
chat/img/favicon.ico


BIN
chat/img/like.png


+ 81 - 0
chat/logmessage.php

@@ -0,0 +1,81 @@
+<?php
+// Load USER
+//require_once("../user.php");
+//$USER = new User("registration_callback");
+
+// Some PHP config stuff
+ini_set("display_errors", 1);
+ini_set("error_reporting", E_ALL | E_STRICT);
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $data = $_POST["messagedata"];
+    $dataarray = explode("###", $data);
+    $message = $dataarray[0];
+    $user = $dataarray[1];
+    $avatar = $dataarray[2];
+}elseif ($_SERVER['REQUEST_METHOD'] === 'GET') {
+    if(count($_GET) > 0){
+        if(isset($_GET["type"])){
+            $image = $_GET["image"];
+            $title = $_GET["title"];
+            $summary = $_GET["summary"];
+            if($_GET["type"] === 'movie' || $_GET["type"] === 'episode' ){
+                $message = '<div class="thumbnail"><div class="member-info zero-m"><img src="'.$image.'" alt="admin" class="img pull-left" style="
+    width: 100px;"><span class="text-muted zero-m"><strong>'.$title.'</strong></span><p class="text-muted zero-m">'.$summary.'</p></div></div>';
+            }elseif($_GET["type"] === 'track'){
+                $message = "";
+            }else{
+                $message = "";
+            }
+        }else{
+           $message = $_GET["message"]; 
+        }
+        $user = $_GET["user"];
+        $avatar = $_GET["avatar"];
+    }else{
+        die("no access");
+    }
+}else{
+    die("no access");
+}
+
+include("connect.php");
+
+if( strlen($message) > 0 )
+{
+    $timestamp = time();
+    $message = utf8_decode($message);
+    
+    if( !stristr($message, "specialcharemoticon") )  // only encrypt text message
+    {
+        $message = encryptmessage($message);
+    }
+
+    // log message
+
+    $db->exec("INSERT INTO chatpack_log (timestamp, user, avatar, message)
+               VALUES ('$timestamp', '$user', '$avatar', '$message')");
+    $db->exec("REPLACE INTO chatpack_last_message (timestamp, user, avatar)
+               VALUES ('$timestamp', '$user', '$avatar')");
+}
+
+function encryptmessage($msg)
+{
+    $key = "OEFKSjczdG5JWkFITHZNUmFLT1I4aWRWaVVWY3l1SXdJZ285V2R3Ri90QjF4NUU1VG9mNnM
+            wcDRYWTQ1dEtRRXRYNlFWZE01QW1WS0hTNXZzaEtRbEdkcXY4cWpEOVRBYjBzSGJlRXVPWW
+            9aWUtzNGZtK1BnRzRPeXk4ZWY0VUphUjc5VzRGQ2s0UXRrNENOWERJWmM3SWNFSEtpM0hpcT
+            l2UVRET2UrMkxQR29ONVpOVDRnSHArTGVwQU15NXg4YzdNSWZQTlBOd2FlWmY2aWRQOUdSZVh
+            3VXQ4a1JlNDkwMWZIVE42cmpIMkRrUkg1VnF1NC9zMmhTZFROVnNleVlSTnVvcWtDYlB3TEJU
+            eDlRT3ZPZVQ2N0psT0NFNW5nekFCdG9xLzZ6K0Qva1V5UzNoVlAxWGt1ZittZnE5ek10Q2x4Q1
+            QrdHVRdEVoYUIxc2V1UjgrZDZyK1Zzem9LOEtpSG9halczNEpmem5nRWllSDBaRzNERHBTbUxB
+            MGlodTZsclFEVzZLcjVBNEtYRUpxQXVNaEcycGN4U2VzT01NRlljM3pHL3Q1az0";
+    $initvector = "aC92eG1PdGhuMXN6";
+    $encryptedmessage = openssl_encrypt($msg, "AES-256-CBC", $key, 0, $initvector);
+    $encryptedmessage = utf8_decode($encryptedmessage);
+    
+    return $encryptedmessage;
+}
+
+$db->close();
+
+?>

+ 2 - 0
chat/php.ini

@@ -0,0 +1,2 @@
+post_max_size = 15M
+upload_max_filesize = 10M

+ 154 - 0
chat/refreshmessages.php

@@ -0,0 +1,154 @@
+<?php
+// Load USER
+//require_once("../user.php");
+//$USER = new User("registration_callback");
+
+// Some PHP config stuff
+ini_set("display_errors", 1);
+ini_set("error_reporting", E_ALL | E_STRICT);
+
+$currentuser = $_POST["user"];
+
+// get last 25 messages, which covers 25 users messaging during one 500 millisecond message refresh
+
+include("connect.php");
+
+if( $result = $db->query("SELECT * FROM
+                         (SELECT id, timestamp, user, avatar, message
+                          FROM chatpack_log ORDER BY id DESC LIMIT 125)
+                          ORDER BY id ASC") )
+{   
+    $newmessages = array();
+
+    while( $row = $result->fetchArray() )
+    {
+        $id = $row["id"];
+        $timestamp = $row["timestamp"];
+        $user = $row["user"];
+        $avatar = $row["avatar"];
+        $message = $row["message"];
+        
+        $timenow = time();
+        $messagetime = date("h:iA", intval($timestamp));
+        $messagedate = date("m-d", intval($timestamp));
+        $message = utf8_encode($message);
+        
+        $msgstr = "";  // message components
+
+        if( strlen($user) > 0 && strlen($message) > 0 )
+        {  
+            // catch emoticon
+
+            $emoticon = false;
+
+            if( stristr($message, "specialcharemoticon") )
+            {
+                $emoticonid = substr($message, 11);
+                $message = "<img class=\"emoticonimgchat\" id=\"$emoticonid\" src=\"img/$emoticonid.png\">";
+                $emoticon = true;
+            }
+
+            // catch image
+
+            $image = false;
+
+            if( stristr($message, "specialcharimg") )
+            {
+                $imagename = substr($message, 14);
+                $message = "<img class=\"thumbnailimgchat\" id=\"$imagename\" src=\"uploads/$imagename\">";
+                $image = true;
+
+                $endingpos = strpos($imagename, ".");
+                $originalname = substr($imagename, 0, $endingpos-1);
+                $ending = substr($imagename, $endingpos+1);
+                $originalimg = $originalname . "." . $ending;
+            }
+            
+            if( !$emoticon && !$image )
+            {
+                $message = decryptmessage($message);
+            }
+            
+            // catch URLs
+
+            /*$message = str_replace("https://", "http://", $message);
+
+            if( !stristr($message, "http://www.") )
+            {
+                $message = str_replace("www.", "http://www.", $message);
+            }
+
+            $message = preg_replace("!((http|ftp)(s)?:\/\/)(www\.)?[a-zA-Z0-9.?&_/=\-\%\:,\#\+]+!",
+                                    "<a href=\"$0\" target=\"_blank\">$0</a>", $message);
+            $message = str_replace("target=\"_blank\">http://", "target=\"_blank\">", $message); */
+
+            // catch highlightings
+
+            $message = preg_replace("/\*{3}(.*?)\*{3}/", "<mark>$1</mark>", $message);
+            $message = preg_replace("/\*{2}(.*?)\*{2}/",
+                                    "<span style=\"font-size: 20px; color: #b77fdb;\"><em>$1</em></span>", $message);
+            $message = preg_replace("/\*(.*?)\*/",
+                                    "<span style=\"color: #d89334;\"><strong>$1</strong></span>", $message);
+
+            // user online avatar
+            
+            //$avatar = "<img class=\"avatarimg\" id=\"$timestamp\" src=\"" . $avatar . "\">";
+
+            // unique message key
+
+            $keystring = $timestamp . $user . $messagetime . $message . $id;
+            $messagekey = md5($keystring);
+
+            // show user avatar and message
+            if($user == $currentuser){
+                $msgstr = $msgstr . "<p class=\"avatarandtext\" id=\"$messagekey\"><li><img src=\"$avatar\" id=\"$timestamp\" class=\"img-circle user-avatar $user\" alt=\"$user\"><div class=\"chat-panel blue-bg messagelike\" id=\"$id\"><div class=\"chat-heading clearfix\"><h4 class=\"pull-left zero-m\">$user</h4><p class=\"pull-right\"><i class=\"fa fa-clock-o\"></i>$messagedate $messagetime </p></div><div class=\"chat-body\">$message</div><span class=\"readed\"><i class=\"fa fa-heart red\" id=\"like$id\"></i></span></div></li></p>";//class="chat-inverted"
+            }else{
+                $msgstr = $msgstr . "<p class=\"avatarandtext\" id=\"$messagekey\"><li class=\"chat-inverted\"><img src=\"$avatar\" id=\"$timestamp\" class=\"img-circle user-avatar $user\" alt=\"$user\"><div class=\"chat-panel red-bg messagelike\" id=\"$id\"><div class=\"chat-heading clearfix\"><h4 class=\"pull-left zero-m\">$user</h4><p class=\"pull-right\"><i class=\"fa fa-clock-o\"></i>$messagedate $messagetime </p></div><div class=\"chat-body\">$message</div><span class=\"readed\"><i class=\"fa fa-heart red liked\" id=\"like$id\"></i></span></div></li></p>";//class="chat-inverted"
+            }
+
+            array_push($newmessages, $msgstr);
+        }
+    }
+}
+
+$db->close();
+
+function decryptmessage($msg)
+{
+    $key = "OEFKSjczdG5JWkFITHZNUmFLT1I4aWRWaVVWY3l1SXdJZ285V2R3Ri90QjF4NUU1VG9mNnM
+            wcDRYWTQ1dEtRRXRYNlFWZE01QW1WS0hTNXZzaEtRbEdkcXY4cWpEOVRBYjBzSGJlRXVPWW
+            9aWUtzNGZtK1BnRzRPeXk4ZWY0VUphUjc5VzRGQ2s0UXRrNENOWERJWmM3SWNFSEtpM0hpcT
+            l2UVRET2UrMkxQR29ONVpOVDRnSHArTGVwQU15NXg4YzdNSWZQTlBOd2FlWmY2aWRQOUdSZVh
+            3VXQ4a1JlNDkwMWZIVE42cmpIMkRrUkg1VnF1NC9zMmhTZFROVnNleVlSTnVvcWtDYlB3TEJU
+            eDlRT3ZPZVQ2N0psT0NFNW5nekFCdG9xLzZ6K0Qva1V5UzNoVlAxWGt1ZittZnE5ek10Q2x4Q1
+            QrdHVRdEVoYUIxc2V1UjgrZDZyK1Zzem9LOEtpSG9halczNEpmem5nRWllSDBaRzNERHBTbUxB
+            MGlodTZsclFEVzZLcjVBNEtYRUpxQXVNaEcycGN4U2VzT01NRlljM3pHL3Q1az0";
+    $initvector = "aC92eG1PdGhuMXN6";
+    $decryptedmessage = openssl_decrypt($msg, "AES-256-CBC", $key, 0, $initvector);
+    $decryptedmessage = utf8_encode($decryptedmessage);
+    
+    return $decryptedmessage;
+}
+
+// pass new messages back to chat.js
+
+if( count($newmessages) == 1 )
+{
+    echo $newmessages[0];
+}
+else
+{
+    for( $i=0; $i<count($newmessages); $i++ )
+    {
+        if( $i == count($newmessages) - 1 )
+        {
+            echo $newmessages[$i];
+        }
+        else
+        {
+            echo $newmessages[$i] . "###endofmessage###";
+        }
+    }
+}
+
+?>

+ 41 - 0
chat/setlike.php

@@ -0,0 +1,41 @@
+<?php
+
+$messageid = $_POST["messageid"];
+
+include("connect.php");
+
+$likemessage = false;
+
+if( $result = $db->query("SELECT liked FROM chatpack_log WHERE id='$messageid'") )
+{
+    if( $row = $result->fetchArray() )
+    {
+        $liked = $row["liked"];
+
+        if( $liked == 0 )
+        {
+            $likemessage = true;
+        }
+        else if( $liked == 1 )
+        {
+            $likemessage = false;
+        }
+    }
+}
+
+$db->close();
+
+include("connect.php");
+
+if( $likemessage )  // like message
+{
+    $db->exec("UPDATE chatpack_log SET liked='1' WHERE id='$messageid'");
+}
+else  // unlike message
+{
+    $db->exec("UPDATE chatpack_log SET liked='0' WHERE id='$messageid'");
+}
+
+$db->close();
+
+?>

+ 58 - 0
chat/settyping.php

@@ -0,0 +1,58 @@
+<?php
+
+$data = $_POST["datavars"];
+$dataarray = explode("###", $data);
+$user = $dataarray[0];
+$settyping = $dataarray[1];
+
+include("connect.php");
+
+$useristyping = false;
+$offlineusers = array();
+
+if( $result = $db->query("SELECT timestamp, user FROM chatpack_typing") )
+{
+    while( $row = $result->fetchArray() )
+    {
+        $typinguser = $row["user"];
+        $timestamp = $row["timestamp"];
+
+        // check whether user is currently typing
+        
+        if( strcmp($typinguser, $user) == 0 )
+        {
+            $useristyping = true;
+        }
+        
+        // catch users who are offline but still set as typing
+        
+        $timenow = time();
+        
+        if( $timestamp < $timenow - 2700 )
+        {
+            array_push($offlineusers, $typinguser);
+        }
+    }
+}
+
+if( !$useristyping && $settyping == 1 )  // set user as typing
+{
+    $timestamp = time();
+    
+    $db->exec("INSERT INTO chatpack_typing (timestamp, user) VALUES ('$timestamp', '$user')");
+}
+else if( $settyping == 0 )  // set user as not typing
+{
+    $db->exec("DELETE FROM chatpack_typing WHERE user='$user'");
+}
+
+// set offline users as not typing
+
+for( $i=0; $i<count($offlineusers); $i++ )
+{
+    $db->exec("DELETE FROM chatpack_typing WHERE user='$offlineusers[$i]'");
+}
+
+$db->close();
+
+?>

+ 177 - 0
chat/uploadimage.php

@@ -0,0 +1,177 @@
+<?php
+
+$data = $_POST["datavars"];
+$dataarray = explode("###", $data);
+$user = $dataarray[0];
+$avatar = $dataarray[1];
+$imagename = $_FILES["image"]["name"];
+$size = $_FILES["image"]["size"];
+$tempname = $_FILES["image"]["tmp_name"];
+$type = $_FILES["image"]["type"];
+$endtemp = explode(".", $_FILES["image"]["name"]);
+$ending = end($endtemp);
+
+// unique image filename
+                
+$unique = md5($imagename . $tempname . time());
+$filename = $unique . "." . $ending;
+                
+// thumbnail
+
+$thumbname = $unique . "t" . "." . $ending;
+            
+// path
+
+$uploaddir = "../uploads";
+$uploaddirexists = false;
+
+if( !is_dir($uploaddir) )  // check if upload directory exists
+{
+    if( mkdir($uploaddir, 0705, true) )  // create upload directory
+    {
+        $uploaddirexists = true;
+    }
+}
+else
+{
+    $uploaddirexists = true;
+}
+
+$path = "../uploads/" . $filename;
+$thumbpath = "../uploads/" . $thumbname;
+
+// upload
+
+if( strlen($user) > 0 && strlen($imagename) > 0 && $size > 0 && $uploaddirexists )
+{
+    if( ($type == "image/gif") || ($type == "image/jpeg") || ($type == "image/jpg") || ($type == "image/png") )
+    {   
+        if( $size < 5000000 )
+        {   
+            if( $_FILES["image"]["error"] == 0 )
+            {   
+                if( !file_exists($path) )
+                {   
+                    if( copy($tempname, $path) )  // upload image
+                    {   
+                        // thumbnail	
+
+                        $sizedata = getimagesize($tempname);
+
+                        if( $type == "image/gif" )
+                        {
+                            $imagetoupload = @imagecreatefromgif($tempname);
+                        }
+                        elseif( $type == "image/jpeg" || $type == "image/jpg" )
+                        {   
+                            $imagetoupload = @imagecreatefromjpeg($tempname);
+                        }
+                        elseif( $type == "image/png" )
+                        {
+                            $imagetoupload = @imagecreatefrompng($tempname);
+                        }
+
+                        if( $imagetoupload )  // imagecreatefromX
+                        {
+                            $width = imagesx($imagetoupload);
+                            $height = imagesy($imagetoupload);
+                            $div = $width / $height;
+                            $newwidth = 150;
+                            $newheight = 150 / $div;
+
+                            $newimage = @imageCreateTrueColor($newwidth, $newheight);
+
+                            if( $newimage )  // imagecreatetruecolor
+                            {   
+                                // upload thumbnail
+
+                                $imagecopy = @imagecopyresized($newimage, $imagetoupload, 0, 0, 0, 0,
+                                                               $newwidth, $newheight, $sizedata[0], $sizedata[1]);
+
+                                if( $imagecopy )  // imagecopyresized
+                                {   
+                                    if( $type == "image/gif" )
+                                    {
+                                        $img = @imagegif($newimage, $thumbpath);
+                                    }
+                                    elseif( $type == "image/jpeg" || $type1 == "image/jpg" )
+                                    {
+                                        $img = @imagejpeg($newimage, $thumbpath);
+                                    }
+                                    elseif( $type == "image/png" )
+                                    {
+                                        $img = @imagepng($newimage, $thumbpath);
+                                    }
+
+                                    if( $img )  // imageX
+                                    {   
+                                        @imagedestroy($newimage);
+
+                                        // db entry
+
+                                        include("connect.php");
+                                        
+                                        $timestamp = time();
+                                        $message = "specialcharimg" . $thumbname;
+
+                                        if( !$db->exec("INSERT INTO chatpack_log (timestamp, user, avatar, message)
+                                                        VALUES ('$timestamp', '$user', '$avatar', '$message')") )
+                                        {
+                                            cleanup($path, $thumbpath, $filename);  // clean up on error
+                                        }
+
+                                        $db->close();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    else  // error upload
+                    {
+                        cleanup($path, $thumbpath, $filename);
+                    }
+                }
+                else  // error exists
+                {
+                    cleanup($path, $thumbpath, $filename);
+                }
+            }
+        }
+        else  // error size
+        {
+            cleanup($path, $thumbpath, $filename);
+        }
+    }
+    else  // error type
+    {
+        cleanup($path, $thumbpath, $filename);
+    }
+}
+
+function cleanup($path, $thumbpath, $filename)
+{	
+    // delete image
+
+    if( file_exists($path) )
+    {
+        unlink($path);
+    }
+
+    // delete thumbnail
+
+    if( file_exists($thumbpath) )
+    {
+        unlink($thumbpath);
+    }
+    
+    // delete db entry
+    
+    include("connect.php");
+    
+    $message = "specialcharimg" . $thumbname;
+    $db->exec("DELETE FROM chatpack_log WHERE message='$message'");
+
+    $db->close();
+}
+
+?>

+ 510 - 89
chatjs.php

@@ -4,6 +4,10 @@ $USER = new User("registration_callback");
 $userpic = md5( strtolower( trim( $USER->email ) ) );
 header("Content-type: application/javascript");
 ?>
+var isMobile = false; //initiate as false
+// device detection
+if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 
+    || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) isMobile = true;
 /*
  * JavaScript MD5
  * https://github.com/blueimp/JavaScript-MD5
@@ -286,101 +290,518 @@ header("Content-type: application/javascript");
   }
 }(this));
 
-var last_time=0;		
-function sendText(){
-    $('#writehere')[0].onkeydown=function(e){
-        if (e.keyCode==13){
-            e.preventDefault();
-            if (this.value){
-                var datatosend={"text":this.value,"user":"<?=$USER->username;?>","email":"<?=$USER->email;?>"};
-                $.ajax({
-                    type:"POST",
-                    url:"chatAJAX.php",
-                    data:datatosend,
-                    datatype:"json",
-                    success:function(data){}
-                })
-                this.value='';
-                $("#writehere").attr('placeholder', "Sending");
-                $("#writehere").prop('disabled', true);
-                
-            }
+$(document).ready(function()
+{
+    // init
+    
+    $(window).focus();
+    var tabinfocus = true;
+    $("#chat").hide();
+    $("#username").val("");
+    $("#username").focus();
+    
+    // allowed characters in username
+    
+    $("#username").keyup(function()
+    {
+        var text = $(this).val();
+        $(this).val(text.replace(/[^a-zA-Z0-9 ]/g, ""));
+    });
+    
+    // enter username
+    
+    var user = "";
+    
+    user = "<?php echo $USER->username; ?>";
+    
+    // choose avatar, check username, start chat
+    
+    var avatar = "";
+    avatar = "https://www.gravatar.com/avatar/" + md5('<?php echo $USER->email; ?>') + "?d=mm";
+
+    startchat();
+    
+    // start chat
+    
+    function startchat()
+    {
+        // zoom chat to fit viewport
+        
+        if( $(window).innerWidth() > 630 )  // if not mobile
+        {
+            var ratio = $(window).innerHeight() / 660;  // browser viewport by chat height
+        
+            //$("#chat").css("zoom", ratio);
+            //$("#chat").css("-moz-transform-origin", "0 0");
+            //$("#chat").css("-moz-transform", "scale(" + ratio + ")");      
         }
+        
+        // update favicon to user avatar
+        
+        //$("#favicon").remove();
+        //var userfavicon = "<link id=\"favicon\" type=\"image/x-icon\"" + 
+                          " rel=\"shortcut icon\" href=\"img/" + avatar + ".ico\">";
+        //$(userfavicon).appendTo("head");
+        
+        // start chat
+        
+        $("#createuser").hide();
+        $("#chat").show();
+        $("#message").focus();
+        
+        refresh();
     }
-}
-		
-function checkText(){
-    $.ajax({
-        type:"POST",
-        url:"chatAJAX.php",
-        data:{time:last_time},
-        datatype:"json",
-        success:function(data){
-            if (data){
-                data=JSON.parse(data);
-                for (var i=0; i<data.length; i++){
-                    if(data[i].USER === "<?=$USER->username;?>"){                        
-                        $('#messages').append(
-                            '<li><img src="https://www.gravatar.com/avatar/'+ md5(data[i].EMAIL) +'?s=100&d=mm" class="img-circle user-avatar" alt="user"><div class="chat-panel blue-bg"><div class="chat-heading clearfix"><h4 class="pull-left zero-m">' + data[i].USER + '</h4><p class="pull-right"><i class="fa fa-clock-o"></i>'+ (setDateTime(data[i].TIME)) + '</p></div><div class="chat-body">' + data[i].MESSAGE + '</div></div></li>'
-                        );
-                        $(".box").animate({ scrollTop: $('.box').prop("scrollHeight")}, 0);
-                    }else{
-                        $('#messages').append(
-                            '<li class="chat-inverted"><img src="https://www.gravatar.com/avatar/'+ md5(data[i].EMAIL) +'?s=100&d=mm" class="img-circle user-avatar" alt="user"><div class="chat-panel red-bg"><div class="chat-heading clearfix"><h4 class="pull-left zero-m">' + data[i].USER + '</h4><p class="pull-right"><i class="fa fa-clock-o"></i>'+ (setDateTime(data[i].TIME)) + '</p></div><div class="chat-body">' + data[i].MESSAGE + '</div></div></li>'
-                        );
-                        $(".box").animate({ scrollTop: $('.box').prop("scrollHeight")}, 0);                     
+    
+    // allowed characters in message
+    
+
+    
+    // log message
+    
+    $("#message").focus().keypress(function(event)
+    {
+        if( event.keyCode === 13 )
+        {
+            event.preventDefault();
+            
+            if( $("#content").is(":visible") )
+            {
+                var message = $("#message").val();
+                message = encodeURIComponent(message);
+                var data = "messagedata=" + message + "###" + user + "###" + avatar;
+                $(this).val("");
+
+                $.ajax
+                ({
+                    type: "POST",
+                    url: "chat/logmessage.php",
+                    data: data,
+                    cache: false
+                });    
+            }
+        }
+    });
+    
+    // log emoticon
+    
+    $(".emoticonimg").click(function()
+    {
+        var emoticonselected = $(this);
+        emoticonselected.hide();
+        var emoticonid = $(this).attr("id");
+        var data = "messagedata=" + "specialchar" + emoticonid + "###" + user + "###" + avatar;
+        
+        $.ajax
+        ({
+            type: "POST",
+            url: "chat/logmessage.php",
+            data: data,
+            cache: false,
+            success: function(result)
+            {
+                emoticonselected.show();
+            }
+        });
+    });
+    
+    // show upload form
+    
+    $("#showuploadform").click(function()
+    {
+        $("#showuploadform").hide();
+        $("#showemoticons").show();
+        $("#uploadform").show();
+        $("#emoticons").hide();
+        $("#loadingwrapper").hide();
+        
+        var r = Math.floor((Math.random() * 14) + 1);
+        var randomemoticon = "chat/img/emoticon" + r + ".png";
+        $("#showemoticonsimg").attr("src", randomemoticon);
+        
+        refresh();  // also use as manual content refresh
+    });
+    
+    // upload image
+    
+    $("#form").submit(function(event)
+    {
+        event.preventDefault();
+        
+        $("#uploadform").hide();
+        $("#showemoticons").hide();
+        $("#loadingwrapper").show();
+        
+        var filedata = $("#uploadimage").prop("files")[0];
+        var formdata = new FormData();             
+        formdata.append("image", filedata);
+        var datavars = user + "###" + avatar;
+        formdata.append("datavars", datavars);
+        
+        $.ajax
+        ({
+            type: "POST",
+            url: "chat/uploadimage.php",
+            cache: false,
+            contentType: false,
+            processData: false,
+            data: formdata,
+            success: function(result)
+            {
+                $("#uploadimage").val("");
+                $("#showuploadform").show();
+                $("#showemoticons").hide();
+                $("#uploadform").hide();
+                $("#loadingwrapper").hide();
+                $("#content").show();
+                $("#emoticons").show();
+            }
+        }); 
+    });
+    
+    // show emoticons
+    
+    $("#showemoticons").click(function()
+    {
+        $("#showuploadform").show();
+        $("#showemoticons").hide();
+        $("#uploadform").hide();
+        $("#loadingwrapper").hide();
+        $("#content").show();
+        $("#emoticons").show();
+    });
+    
+    // refresh content
+    
+    var content = $("#messages");
+    var newcontent = content.html();
+    
+    function refresh()
+    {   
+        setTimeout(function()
+        {
+            var data = "user=" + user;
+            
+            $.ajax
+            ({
+                type: "POST",
+                url: "chat/refreshmessages.php",
+                data: data,
+                cache: false,
+                success: function(result)
+                {   
+                    // check who is still online
+                                    
+                    var datetoday = new Date();
+                    var timenow = datetoday.getTime() / 1000;  
+
+                    $.ajax
+                ({
+                    url: "chat/getonline.php",
+                    cache: false,
+                    success: function(result)
+                    {
+                        var onlineusers = JSON.parse(result);
+                        var oldonlineusers = $("#onlineusers").html();
+                        var newonlineusers = '';
+                        
+                        if( onlineusers.length <= 0 )  // no user typing
+                        {
+                            newonlineusers += "No Users Online";
+                        }
+                        else
+                        {   
+                            if( onlineusers.length >= 1 )  // one user typing
+                            {
+                                jQuery.each( onlineusers, function( i, val ) {
+                                    var timecheck = val[1];
+                                    var status = "";
+                                    var color = "";
+                                    if( timecheck < timenow - 1800 )
+                                    {
+                                        status = '<span style="min-height: 14px;margin-top: 10px;" class="pull-right badge badge-danger animated pulse"> </span>';
+                                        color = "red";
+                                    }else{
+                                        status = '<span style="min-height: 14px;margin-top: 10px;" class="pull-right badge badge-success animated flash"> </span>';                       
+                                        color = "blue";                       
+                                    }
+                                    if( timecheck < timenow - 3600 )
+                                    {
+                                        newonlineusers += '';      
+                                    }else{
+                                        newonlineusers += '<div class="member-info"><img style="height:40px" src="'+val[2]+'" alt="admin" class="img-circle"><span class="member-name">'+val[0]+'</span>'+status+'</div>'; 
+                                        i++;
+                                    }
+                                    
+                                    $("img[alt^='"+val[0]+"']").each(function()
+                                    {
+                                        var timestamp = val[1];
+
+                                        // set user offline avatar
+
+                                        if( timestamp < timenow - 1800 )
+                                        {
+                                            $(this).addClass("offline");
+                                            $(this).removeClass("online");
+
+                                        }
+                                        else  // set user online avatar
+                                        {
+
+                                            $(this).addClass("online");
+                                            $(this).removeClass("offline");
+
+                                        }
+                                    });
+                                    
+                                });
+                            }
+                        }
+                        if(newonlineusers === ''){ newonlineusers = "No Users Online";}
+                        if( newonlineusers != oldonlineusers )
+                        {
+                            $("#onlineusers").html(newonlineusers);
+                        }
+                    }
+                });
+
+                    
+                    
+                    // new messages
+                    
+                    var newmessages = result.split("###endofmessage###");
+
+                    for( var i=0; i<newmessages.length; i++ )
+                    {
+                        var message = newmessages[i];
+                        
+                        if( message != "" )
+                        {
+                            var messagekeypos = message.indexOf("avatarandtext");
+                            var messagekey = message.substr(messagekeypos + 19, 32);
+                            var contentdom = document.getElementById("messages");
+                            var messagedom = document.getElementById(messagekey);
+                            
+                            if( $.contains(contentdom, messagedom) == false )
+                            {
+                                // append new message
+
+                                content.append(message).promise().done(function()
+                                {
+                                    // scroll to bottom
+
+                                    if( newcontent != content.html() )
+                                    {
+                                        var toscroll = document.getElementById("messages");
+                                        toscroll.scrollTop = toscroll.scrollHeight;
+                                        $(".box").animate({ scrollTop: $('.box').prop("scrollHeight")}, 0);
+                                        newcontent = content.html();
+                                    }
+                                    
+                                    // new message tab alert
+
+                                    var userwriting = user + "writing";
+
+                                    if( message.lastIndexOf(userwriting) == -1 )
+                                    {
+                                        newmessagealert(message);
+                                    }
+                                    
+                                    // refresh eventlisteners of messages to set likes
+                        
+                                    $(".messagelike").unbind().click(function()
+                                    {
+                                        if( this.innerHTML.indexOf("target=") == -1 &&
+                                            this.innerHTML.indexOf("_blank") == -1 )
+                                        {
+                                            likemessage(this.id);
+                                        }
+                                    });
+                                });
+                            }
+                        }
                     }
+                    
+                    // get and show likes
+                    
+                    $.ajax
+                    ({
+                        url: "chat/getlikes.php",
+                        cache: false,
+                        success: function(result)
+                        {
+                            var likesandunlikes = result.split("#");
+                            var likedmessages = likesandunlikes[0];
+                            var unlikedmessages = likesandunlikes[1];
+                            var likes = JSON.parse(likedmessages);
+                            var unlikes = JSON.parse(unlikedmessages);
+                            
+                            for( var i=0; i<likes.length; i++ )
+                            {
+                                var likeid = "#like" + likes[i];
+                                $(likeid).show();
+                            }
+                            
+                            for( var i=0; i<unlikes.length; i++ )
+                            {
+                                var unlikeid = "#like" + unlikes[i];
+                                $(unlikeid).hide();
+                            }
+                        }
+                    });
+                    
+                    // hide intro
+                    
+                    if( result != "" )
+                    {
+                        $("#intro").hide();        
+                    }
+                    
+                    // loop
+                    
+                    refresh();
                 }
-                last_time=Date.now()/1000;
-                $('#messages').scrollTop($('#messages')[0].scrollHeight);
+            });
+            
+            // update if current user is typing
+            
+            if( $("#message").val() != "" )
+            {
+                istyping(user, 1);      
+            }
+            else if( $("#message").val() == "" )
+            {
+                istyping(user, 0);     
             }
-            checkText();
-            $("#writehere").attr('placeholder', "Enter your text");
-            $("#writehere").prop('disabled', false);
-            $("#writehere").focus();
+            
+        }, 500);
+    }
+    
+    // tab focus
+    
+    window.onfocus = function()
+    {
+        tabinfocus = true;
+        parent.document.title = "Chat";
+        window.parent.$("span[id^='chat.phps']").html("");
+    };
+    
+    window.onblur = function()
+    {
+        tabinfocus = false;
+    };
+    
+    // new message tab alert
+    
+    function newmessagealert(message)
+    {   
+        if( !tabinfocus )
+        {
+            // title marker
+            i = parseInt(parent.document.title);
+            if(isNaN(i)){
+                i = 1;
+             }else{
+                i++
+             }
+
+            parent.document.title = i + " Chat";
+            //window.parent.$("#chat.phpx").addClass("gottem");
+            window.parent.$("span[id^='chat.phps']").html(i);
+            var $jQueryObject = $($.parseHTML(message));
+            var alertMessage = $jQueryObject.find(".chat-body").html();
+            var alertUsername = $jQueryObject.find("h4[class^='pull-left zero-m']").html();
+            var alertIcon = $jQueryObject.find("img").attr("src");;
+            if(isMobile === false){
+                parent.Push.create(alertUsername, {
+                    body: alertMessage,
+                    icon: alertIcon,
+                    timeout: 4000,
+                    onClick: function () {
+                        window.parent.focus();
+                        this.close();
+                    }
+                });
+            }
+                        
+            // sound
+                        
+            var audio = $("#tabalert")[0];
+            audio.play();
         }
-    });					
-}	
-                                                     
-function datecompare(date1, sign, date2) {
-    var day1 = date1.getDate();
-    var mon1 = date1.getMonth();
-    var year1 = date1.getFullYear();
-    var day2 = date2.getDate();
-    var mon2 = date2.getMonth();
-    var year2 = date2.getFullYear();
-    if (sign === '===') {
-        if (day1 === day2 && mon1 === mon2 && year1 === year2) return true;
-        else return false;
     }
-    else if (sign === '>') {
-        if (year1 > year2) return true;
-        else if (year1 === year2 && mon1 > mon2) return true;
-        else if (year1 === year2 && mon1 === mon2 && day1 > day2) return true;
-        else return false;
-    }    
-}                                             
-		
-function setDateTime(timedate){
-    var tempdate=new Date(timedate*1000),
-    date=TwoDigits(tempdate.getDate()),
-    month=TwoDigits(tempdate.getMonth()+1),
-    year=tempdate.getFullYear(),
-    hours=TwoDigits(tempdate.getHours()),
-    mins=TwoDigits(tempdate.getMinutes()),
-    sec=TwoDigits(tempdate.getSeconds());
-    var Today = new Date();
-    if(datecompare(Today, ">", tempdate)){
-        return (month+'-'+date+' '+hours+':'+mins );                
+    
+    // like message
+    
+    function likemessage(messageid)
+    {
+        var data = "messageid=" + messageid;
+        
+        $.ajax
+        ({
+            type: "POST",
+            url: "chat/setlike.php",
+            data: data,
+            cache: false
+        });
     }
-    if(datecompare(Today, "===", tempdate)){
-        return (hours+':'+mins );
+    
+    // update which users are typing
+    
+    function istyping(u, t)
+    {
+        // set typing user
+        
+        var data = "datavars=" + u + "###" + t;
+                
+        $.ajax
+        ({
+            type: "POST",
+            url: "chat/settyping.php",
+            data: data,
+            cache: false,
+            success: function(result)
+            {
+                // get typing users
+                
+                $.ajax
+                ({
+                    url: "chat/gettyping.php",
+                    cache: false,
+                    success: function(result)
+                    {
+                        var typingusers = JSON.parse(result);
+                        
+                        if( typingusers.length == 0 )  // no user typing
+                        {
+                            $("#istyping").html("");
+                        }
+                        else
+                        {   //$("#istyping").html("<li class=\"chat-inverted\"><img src=\"images/ellipsis.png\" class=\"dont img-circle user-avatar online\" alt=\"user\"><div class=\"chat-panel red-bg\"><div class=\"chat-body\">" + typingusers[0] + "...</div></div></li>");
+                            if( typingusers.length == 1 )  // one user typing
+                            {
+                                $("#istyping").html(typingusers[0] + " is typing...");
+                                //$(".box").animate({ scrollTop: $('.box').prop("scrollHeight")}, 0);
+                            }
+                            else if( typingusers.length == 2 )  // two users typing
+                            {
+                                var whoistyping = typingusers[0] +
+                                                  " and " + typingusers[1] +
+                                                  " are typing...";
+                                $("#istyping").html(whoistyping);
+                                //$(".box").animate({ scrollTop: $('.box').prop("scrollHeight")}, 0);
+                            }
+                            else if( typingusers.length > 2 )  // more than two users typing
+                            {
+                                var whoistyping = typingusers[0] +
+                                                  " and " + typingusers[1] +
+                                                  " and others are typing...";
+                                $("#istyping").html(whoistyping);
+                                //$(".box").animate({ scrollTop: $('.box').prop("scrollHeight")}, 0);
+                            }
+                        }
+                    }
+                });
+            }
+        });
     }
-}
-		
-function TwoDigits(number){
-    return (number<10 ? '0' : '') + number;
-}
-                      
-sendText();
-checkText();
+});

+ 1 - 0
check.php

@@ -162,6 +162,7 @@ $folder = USER_HOME;
                 
                 check("PDO_SQLITE");
                 check("PDO");
+                check("SQLITE3");
                 check("Zip");
                 check("openssl");
                 check("session");

+ 15 - 1
config/configDefaults.php

@@ -9,14 +9,21 @@ return array(
 	"plexRecentTV" => "false",
 	"plexRecentMusic" => "false",
 	"plexPlayingNow" => "false",
+ 	"plexShowNames" => false,
 	"plexHomeAuth" => false,
+	"plexSearch" => false,
+	"plexRecentItems" => "20",
+	"plexTabName" => "",
 	"embyURL" => "",
 	"embyToken" => "",
 	"embyRecentMovie" => "false",
 	"embyRecentTV" => "false",
 	"embyRecentMusic" => "false",
 	"embyPlayingNow" => "false",
+ 	"embyShowNames" => false,
 	"embyHomeAuth" => false,
+	"embySearch" => false,
+	"embyRecentItems" => "20",
 	"sonarrURL" => "",
 	"sonarrKey" => "",
 	"sonarrHomeAuth" => false,
@@ -71,5 +78,12 @@ return array(
 	"homepageCustomHTML1Auth" => false,
 	"git_branch" => "master",
 	"git_check" => true,
-    "speedTest" => false,
+	"speedTest" => false,
+	"smtpHostType" => "tls",
+	"homepageNoticeTitle" => "",
+	"homepageNoticeMessage" => "",
+	"homepageNoticeType" => "success",
+	"homepageNoticeAuth" => "false",
+	"homepageNoticeLayout" => "elegant",
+	"ipInfoToken" => "ddd0c072ad5021",
 );

+ 36 - 7
css/style.css

@@ -6,13 +6,24 @@
  *
 */
 /*@import url(https://fonts.googleapis.com/css?family=Roboto:100,400,500,300,700);*/
+.loop-animation {
+    animation-iteration-count: infinite;
+    -webkit-animation-iteration-count: infinite;
+    -moz-animation-iteration-count: infinite;
+    -o-animation-iteration-count: infinite;
+}
+.elip{
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    display: block;
+}
 .preloader {
   z-index: 99999;
   position: fixed !important;
   height: 100%;
   width: 100%;
 }
-
 /*!
  * Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
  * Copyright 2015 Daniel Cardoso <@DanielCardoso>
@@ -907,6 +918,9 @@ button::-moz-focus-inner {
 html, body {
   height: 100%;
 }
+::-webkit-scrollbar { 
+   display: none;
+}
 
 body {
   font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
@@ -917,6 +931,10 @@ body {
   position: relative;
   background: #F1F4F5;
   overflow-x: hidden;
+  overflow-x:hidden; 
+  /*MS Edge */
+  -ms-overflow-style: -ms-autohiding-scrollbar;
+  -ms-overflow-style: none; 
 }
 
 .main-wrapper {
@@ -1267,6 +1285,7 @@ body {
 .member-info {
   font-weight: 400;
   margin-top: 10px;
+  overflow: hidden;
 }
 
 .member-info img {
@@ -2155,6 +2174,13 @@ table.dataTable.no-footer, table.dataTable thead th, table.dataTable thead td {
     -moz-background-clip: padding;
     background-clip: padding-box;
 }
+@media (min-width: 768px) {
+    .fc-title {
+     text-overflow: ellipsis;
+     white-space: nowrap;
+     overflow: hidden;   
+    }
+}
 
 .fc-calendar .fc-toolbar {
   background: #63A8EB;
@@ -2515,7 +2541,7 @@ a.thumbnail.active, a.thumbnail:focus, a.thumbnail:hover {
 }
 
 .input-group-addon {
-  border: solid 1px #C0C0C0 !important;
+  border: solid 0px #C0C0C0 !important;
 }
 
 .form-inline .form-group {
@@ -3265,7 +3291,7 @@ ul.inbox-pagination li {
   /*padding: 22px;*/
   padding: 19px 22px 19px 22px;
   line-height: 1.4;
-  z-index: 4000;
+  z-index: 10000;
   pointer-events: none;
   color: #fff;
   font-size: 80%;
@@ -3737,8 +3763,8 @@ ul.inbox-pagination li {
 .ns-effect-exploader.ns-show {
   -webkit-animation-name: animLoad;
           animation-name: animLoad;
-  -webkit-animation-duration: 2.5s;
-          animation-duration: 2.5s;
+  -webkit-animation-duration: 1.0s;
+          animation-duration: 1.0s;
 }
 
 @-webkit-keyframes animLoad {
@@ -3810,8 +3836,8 @@ ul.inbox-pagination li {
 
 .ns-effect-exploader.ns-show .ns-box-inner,
 .ns-effect-exploader.ns-show .ns-close {
-  -webkit-animation-delay: 2.4s;
-          animation-delay: 2.4s;
+  -webkit-animation-delay: .9s;
+          animation-delay: .9s;
   -webkit-animation-duration: 0.3s;
           animation-duration: 0.3s;
   -webkit-animation-fill-mode: both;
@@ -6146,6 +6172,9 @@ label {
   }.mdi-fullscreen {
     display: none !important;
   }
+  .mdi-window-restore {
+    display: none !important;
+  }    
   /*Remove margins from content block*/
   .content {
     margin: 0;

+ 2 - 1
error.php

@@ -67,6 +67,7 @@ $errorImage = $codes[$status][2];
         <link rel="stylesheet" href="<?php echo checkRootPath(dirname($_SERVER['SCRIPT_NAME'])); ?>bower_components/bootstrap/dist/css/bootstrap.min.css">
         <link rel="stylesheet" href="<?php echo checkRootPath(dirname($_SERVER['SCRIPT_NAME'])); ?>bower_components/Waves/dist/waves.min.css"> 
         <link rel="stylesheet" href="<?php echo checkRootPath(dirname($_SERVER['SCRIPT_NAME'])); ?>css/style.css">
+		<style><?php customCSS(); ?></style>
     </head>
     <body class="gray-bg" style="padding: 0;">
         <div class="main-wrapper" style="position: initial;">
@@ -82,7 +83,7 @@ $errorImage = $codes[$status][2];
                                     <div class="big-box text-left">
                                         <center><img src="<?php echo checkRootPath(dirname($_SERVER['SCRIPT_NAME'])); ?>images/<?=$errorImage;?>.png" style="height: 200px;"></center>
                                         <h4 style="color: <?=$topbar;?>;" class="text-center"><?php echo $message;?></h4>
-                                        <button style="background:<?=$topbar;?>;" onclick="window.history.back();" type="button" class="btn log-in btn-block btn-primary text-uppercase waves waves-effect waves-float"><text style="color:<?=$topbartext;?>;"><?php echo $language->translate("GO_BACK");?></text></button>
+                                        <button style="background:<?=$topbar;?>;" onclick="parent.location='../'" type="button" class="btn log-in btn-block btn-primary text-uppercase waves waves-effect waves-float"><text style="color:<?=$topbartext;?>;"><?php echo $language->translate("GO_BACK");?></text></button>
                                     </div>
                                 </div>
                             </div>

Файловите разлики са ограничени, защото са твърде много
+ 594 - 218
functions.php


+ 325 - 139
homepage.php

@@ -17,63 +17,10 @@ $USER = new User("registration_callback");
 // Check if connection to homepage is allowed
 qualifyUser(HOMEPAGEAUTHNEEDED, true);
 
-$dbfile = DATABASE_LOCATION.'users.db';
-
-$file_db = new PDO("sqlite:" . $dbfile);
-$file_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
-
-$dbOptions = $file_db->query('SELECT name FROM sqlite_master WHERE type="table" AND name="options"');
-
-$hasOptions = "No";
-
-foreach($dbOptions as $row) :
-
-    if (in_array("options", $row)) :
-        $hasOptions = "Yes";
-    endif;
-
-endforeach;
-
-if($hasOptions == "No") :
-
-    $title = "Organizr";
-    $topbar = "#333333"; 
-    $topbartext = "#66D9EF";
-    $bottombar = "#333333";
-    $sidebar = "#393939";
-    $hoverbg = "#AD80FD";
-    $activetabBG = "#F92671";
-    $activetabicon = "#FFFFFF";
-    $activetabtext = "#FFFFFF";
-    $inactiveicon = "#66D9EF";
-    $inactivetext = "#66D9EF";
-    $loading = "#66D9EF";
-    $hovertext = "#000000";
-
-endif;
-
-if($hasOptions == "Yes") :
-
-    $resulto = $file_db->query('SELECT * FROM options'); 
-    foreach($resulto as $row) : 
-
-        $title = isset($row['title']) ? $row['title'] : "Organizr";
-        $topbartext = isset($row['topbartext']) ? $row['topbartext'] : "#66D9EF";
-        $topbar = isset($row['topbar']) ? $row['topbar'] : "#333333";
-        $bottombar = isset($row['bottombar']) ? $row['bottombar'] : "#333333";
-        $sidebar = isset($row['sidebar']) ? $row['sidebar'] : "#393939";
-        $hoverbg = isset($row['hoverbg']) ? $row['hoverbg'] : "#AD80FD";
-        $activetabBG = isset($row['activetabBG']) ? $row['activetabBG'] : "#F92671";
-        $activetabicon = isset($row['activetabicon']) ? $row['activetabicon'] : "#FFFFFF";
-        $activetabtext = isset($row['activetabtext']) ? $row['activetabtext'] : "#FFFFFF";
-        $inactiveicon = isset($row['inactiveicon']) ? $row['inactiveicon'] : "#66D9EF";
-        $inactivetext = isset($row['inactivetext']) ? $row['inactivetext'] : "#66D9EF";
-        $loading = isset($row['loading']) ? $row['loading'] : "#66D9EF";
-        $hovertext = isset($row['hovertext']) ? $row['hovertext'] : "#000000";
-
-    endforeach;
-
-endif;
+// Load Colours/Appearance
+foreach(loadAppearance() as $key => $value) {
+	$$key = $value;
+}
 
 $startDate = date('Y-m-d',strtotime("-".CALENDARSTARTDAY." days"));
 $endDate = date('Y-m-d',strtotime("+".CALENDARENDDAY." days")); 
@@ -101,8 +48,12 @@ $endDate = date('Y-m-d',strtotime("+".CALENDARENDDAY." days"));
 
         <link rel="stylesheet" href="bower_components/fullcalendar/dist/fullcalendar.css">
 
-        <link rel="stylesheet" href="css/style.css">
+        <link rel="stylesheet" href="css/style.css?v=<?php echo INSTALLEDVERSION; ?>">
         <link rel="stylesheet" href="bower_components/mdi/css/materialdesignicons.min.css?v=<?php echo INSTALLEDVERSION; ?>">
+        <link rel="stylesheet" href="bower_components/google-material-color/dist/palette.css?v=<?php echo INSTALLEDVERSION; ?>">
+        <link rel="stylesheet" type="text/css" href="bower_components/slick/slick.css?v=<?php echo INSTALLEDVERSION; ?>">
+        <!-- Add the slick-theme.css if you want default styling -->
+       
 
         <!--Scripts-->
         <script src="bower_components/jquery/dist/jquery.min.js"></script>
@@ -113,7 +64,8 @@ $endDate = date('Y-m-d',strtotime("+".CALENDARENDDAY." days"));
         <script src="bower_components/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.js"></script>
         <script src="bower_components/jquery.nicescroll/jquery.nicescroll.min.js"></script>
         <script src="bower_components/cta/dist/cta.min.js"></script>
-        <script src="bower_components/fullcalendar/dist/fullcalendar.js"></script>
+        <script src="bower_components/fullcalendar/dist/fullcalendar.js?v=<?php echo INSTALLEDVERSION; ?>"></script>
+        <script src="bower_components/slick/slick.js?v=<?php echo INSTALLEDVERSION; ?>"></script>
 
         <script src="js/jqueri_ui_custom/jquery-ui.min.js"></script>
 	    <script src="js/jquery.mousewheel.min.js" type="text/javascript"></script>
@@ -126,6 +78,29 @@ $endDate = date('Y-m-d',strtotime("+".CALENDARENDDAY." days"));
         <script src="bower_components/respondJs/dest/respond.min.js"></script>
         <![endif]-->
         <style>
+            .recentItems {
+                padding-top: 10px;
+                margin: 5px 0;
+            }
+            .slick-image-tall{
+                width: 125px;
+                height: 180px;
+            }
+            .slick-image-short{
+                width: 125px;
+                height: 130px;
+                margin-top: 50px;
+            }
+            .overlay{
+                position: absolute;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                display: none;
+                z-index: 0;
+                opacity: .98;
+            }
             sort {
                 display: none;
             }
@@ -213,27 +188,19 @@ $endDate = date('Y-m-d',strtotime("+".CALENDARENDDAY." days"));
 				white-space: normal !important;
 				width: 0% !important;
 				font-size: 12px; !important;
-			}<?php if(CUSTOMCSS == "true") : 
-$template_file = "custom.css";
-$file_handle = fopen($template_file, "rb");
-echo fread($file_handle, filesize($template_file));
-fclose($file_handle);
-echo "\n";
-endif; ?>        
+			}<?php customCSS(); ?>       
         </style>
     </head>
 
     <body class="scroller-body" style="padding: 0px;">
         <div class="main-wrapper" style="position: initial;">
             <div id="content" class="container-fluid">
-<!-- <button id="numBnt">Numerical</button> -->
                 <br/>
+ 
+                <?php if (qualifyUser(HOMEPAGENOTICEAUTH) && HOMEPAGENOTICETITLE && HOMEPAGENOTICETYPE && HOMEPAGENOTICEMESSAGE && HOMEPAGENOTICELAYOUT) { echo buildHomepageNotice(HOMEPAGENOTICELAYOUT, HOMEPAGENOTICETYPE, HOMEPAGENOTICETITLE, HOMEPAGENOTICEMESSAGE); } ?>
+                
+                <?php if (qualifyUser(HOMEPAGECUSTOMHTML1AUTH) && HOMEPAGECUSTOMHTML1) { echo "<div>" . HOMEPAGECUSTOMHTML1 . "</div>"; } ?>
 
-				 <?php if (qualifyUser(HOMEPAGECUSTOMHTML1AUTH) && HOMEPAGECUSTOMHTML1) { ?>
-				<div>
-					<?php echo HOMEPAGECUSTOMHTML1; ?>
-				</div>
-                <?php } ?>
                 <?php if(SPEEDTEST == "true"){ ?>
                 <style type="text/css">
 
@@ -262,9 +229,9 @@ endif; ?>
                             var ul = document.getElementById('upload')
                             var ping = document.getElementById('ping')
                             var jitter = document.getElementById('jitter')
-                            dl.className = status === 1 ? 'flash' : ''
-                            ping.className = status === 2 ? 'flash' : ''
-                            jitter.className = ul.className = status === 3 ? 'flash' : ''
+                            dl.className = status === 1 ? 'w-name flash' : 'w-name'
+                            ping.className = status === 2 ? 'w-name flash' : 'w-name'
+                            jitter.className = ul.className = status === 3 ? 'w-name flash' : 'w-name'
                             if (status >= 4) {
                                 clearInterval(interval)
                                 document.getElementById('abortBtn').style.display = 'none'
@@ -300,7 +267,7 @@ endif; ?>
                                 <div class="w-descr left pull-left text-center">
                                     <span class="testName text-uppercase w-name">Download</span>
                                     <br>
-                                    <span class="w-amount counter" id="download" ></span>
+                                    <span class="w-name counter" id="download" ></span>
                                 </div>
                             </div>
                         </div>
@@ -314,7 +281,7 @@ endif; ?>
                                 <div class="w-descr left pull-left text-center">
                                     <span class="testName text-uppercase w-name">Upload</span>
                                     <br>
-                                    <span class="w-amount counter" id="upload" ></span>
+                                    <span class="w-name counter" id="upload" ></span>
                                 </div>
                             </div>
                         </div>
@@ -328,7 +295,7 @@ endif; ?>
                                 <div class="w-descr left pull-left text-center">
                                     <span class="testName text-uppercase w-name">Latency</span>
                                     <br>
-                                    <span class="w-amount counter" id="ping" ></span>
+                                    <span class="w-name counter" id="ping" ></span>
                                 </div>
                             </div>
                         </div>
@@ -342,7 +309,7 @@ endif; ?>
                                 <div class="w-descr left pull-left text-center">
                                     <span class="testName text-uppercase w-name">Jitter</span>
                                     <br>
-                                    <span class="w-amount counter" id="jitter" ></span>
+                                    <span class="w-name counter" id="jitter" ></span>
                                 </div>
                             </div>
                         </div>
@@ -370,9 +337,28 @@ endif; ?>
                     </div>
                 </div>
                 <?php } ?>
+                <?php if((PLEXSEARCH == "true" && qualifyUser(PLEXHOMEAUTH))) { ?>
+                <div id="searchPlexRow" class="row">
+                    <div class="col-lg-12">
+                        <div class="content-box box-shadow big-box todo-list">                        
+                            <form id="plexSearchForm" onsubmit="return false;" autocomplete="off">
+                                <div class="">
+                                    <div class="input-group">
+                                        <div style="border-radius: 25px 0 0 25px; border:0" class="input-group-addon gray-bg"><i class="fa fa-search white"></i></div>
+                                        <input id="searchInput" type="text" style="border-radius: 0;" autocomplete="off" name="search-title" class="form-control input-group-addon gray-bg" placeholder="Media Search">
+										<div id="clearSearch" style="border-radius: 0 25px 25px 0;border:0; cursor: pointer;" class="input-group-addon gray-bg"><i class="fa fa-close white"></i></div>
+                                        <button style="display:none" id="plexSearchForm_submit" class="btn btn-primary waves"></button>
+                                    </div>
+                                </div>
+                            </form>
+                            <div id="resultshere" class="table-responsive"></div>
+                        </div>
+                    </div>
+                </div>
+                <?php } ?>
+                
                 <?php if((NZBGETURL != "" && qualifyUser(NZBGETHOMEAUTH)) || (SABNZBDURL != "" && qualifyUser(SABNZBDHOMEAUTH))) { ?>
                 <div id="downloadClientRow" class="row">
-                    <sort>2</sort>
                     <div class="col-xs-12 col-md-12">
                         <div class="content-box">
                             <div class="tabbable panel with-nav-tabs panel-default">
@@ -442,36 +428,39 @@ endif; ?>
                 </div>
                 <?php } ?>
 				<?php if (qualifyUser(PLEXHOMEAUTH) && PLEXTOKEN) { ?>
+                <div id="plexRowNowPlaying" class="row">
+                    <?php if(PLEXPLAYINGNOW == "true"){ echo getPlexStreams(12, PLEXSHOWNAMES, $USER->role); } ?>
+                </div>
                 <div id="plexRow" class="row">
-                    <sort>3</sort>
-
+                    <div class="col-lg-12">
                     <?php
-                    $plexSize = (PLEXRECENTMOVIE == "true") + (PLEXRECENTTV == "true") + (PLEXRECENTMUSIC == "true") + (PLEXPLAYINGNOW == "true");
-                    if(PLEXRECENTMOVIE == "true"){ echo getPlexRecent("movie", 12/$plexSize); }
-                    if(PLEXRECENTTV == "true"){ echo getPlexRecent("season", 12/$plexSize); }
-                    if(PLEXRECENTMUSIC == "true"){ echo getPlexRecent("album", 12/$plexSize); }
-                    if(PLEXPLAYINGNOW == "true"){ echo getPlexStreams(12/$plexSize); }
+                    if(PLEXRECENTMOVIE == "true" || PLEXRECENTTV == "true" || PLEXRECENTMUSIC == "true"){  
+                        $plexArray = array("movie" => PLEXRECENTMOVIE, "season" => PLEXRECENTTV, "album" => PLEXRECENTMUSIC);
+                        echo getPlexRecent($plexArray);
+                    } 
                     ?>
-
+                    </div>
                 </div>
 				<?php } ?>
 				<?php if (qualifyUser(EMBYHOMEAUTH) && EMBYTOKEN) { ?>
+                <div id="embyRowNowPlaying" class="row">
+                    <?php if(EMBYPLAYINGNOW == "true"){ echo getEmbyStreams(12, EMBYSHOWNAMES, $USER->role); } ?>
+                </div>
                 <div id="embyRow" class="row">
-                    <sort>3</sort>
-
+                    <div class="col-lg-12">
                     <?php
-                    $embySize = (EMBYRECENTMOVIE == "true") + (EMBYRECENTTV == "true") + (EMBYRECENTMUSIC == "true") + (EMBYPLAYINGNOW == "true");
-                    if(EMBYRECENTMOVIE == "true"){ echo getEmbyRecent("movie", 12/$embySize); }
-                    if(EMBYRECENTTV == "true"){ echo getEmbyRecent("season", 12/$embySize); }
-                    if(EMBYRECENTMUSIC == "true"){ echo getEmbyRecent("album", 12/$embySize); }
-                    if(EMBYPLAYINGNOW == "true"){ echo getEmbyStreams(12/$embySize); }
+                    if(EMBYRECENTMOVIE == "true" || EMBYRECENTTV == "true" || EMBYRECENTMUSIC == "true"){  
+                        $embyArray = array("Movie" => EMBYRECENTMOVIE, "Episode" => EMBYRECENTTV, "MusicAlbum" => EMBYRECENTMUSIC, "Series" => EMBYRECENTTV);
+                        echo getEmbyRecent($embyArray);
+                    } 
+    
                     ?>
+                    </div>
 
                 </div>
 				<?php } ?>
                 <?php if ((SONARRURL != "" && qualifyUser(SONARRHOMEAUTH)) || (RADARRURL != "" && qualifyUser(RADARRHOMEAUTH)) || (HEADPHONESURL != "" && qualifyUser(HEADPHONESHOMEAUTH)) || (SICKRAGEURL != "" && qualifyUser(SICKRAGEHOMEAUTH))) { ?>
                 <div id="calendarLegendRow" class="row" style="padding: 0 0 10px 0;">
-                    <sort>1</sort>
                     <div class="col-lg-12 content-form form-inline">
                         <div class="form-group">
                             <select class="form-control" id="imagetype_selector" style="width: auto !important; display: inline-block">
@@ -489,7 +478,6 @@ endif; ?>
                     </div>
                 </div>
                 <div id="calendarRow" class="row">
-                    <sort>1</sort>
                     <div class="col-lg-12">
                         <div id="calendar" class="fc-calendar box-shadow fc fc-ltr fc-unthemed"></div>
                     </div>
@@ -498,23 +486,194 @@ endif; ?>
             </div>    
         </div>
         <script>
-		function localStorageSupport() {
-			return (('localStorage' in window) && window['localStorage'] !== null)
-		}
+        $('.close-btn').click(function(e){
+            var closedBox = $(this).closest('div.content-box').remove();
+            e.preventDefault();
+        });
+		$('#clearSearch').click(function(e){
+            $('#searchInput').val("");
+            $('#resultshere').html("");
+            $('#searchInput').focus();
+            e.preventDefault();
+        });
+        
+		$(document).on("click", ".openTab", function(e) {
+			if($(this).attr("openTab") === "true") {
+				var isActive = parent.$("div[data-content-name^='<?php echo strtolower(PLEXTABNAME);?>']");
+				var activeFrame = isActive.children('iframe');
+				if(isActive.length === 1){
+					activeFrame.attr("src", $(this).attr("href"));
+					parent.$("li[name='<?php echo strtolower(PLEXTABNAME);?>']").trigger("click");
+				}else{
+					parent.$("li[name='<?php echo strtolower(PLEXTABNAME);?>']").trigger("click");
+					parent.$("div[data-content-name^='<?php echo strtolower(PLEXTABNAME);?>']").children('iframe').attr("src", $(this).attr("href"));
+				}
+				e.preventDefault();
+			}else{
+				console.log("nope");
+			}
+
+        });
+        
+            
+        function localStorageSupport() {
+            return (('localStorage' in window) && window['localStorage'] !== null)
+        }
 		
         $( document ).ready(function() {
-             $('.repeat-btn').click(function(){
+            $('#plexSearchForm').on('submit', function () {
+                ajax_request('POST', 'search-plex', {
+                    searchtitle: $('#plexSearchForm [name=search-title]').val(),
+                }).done(function(data){ $('#resultshere').html(data);});
+
+            });
+            $('.repeat-btn').click(function(){
                 var refreshBox = $(this).closest('div.content-box');
                 $("<div class='refresh-preloader'><div class='la-timer la-dark'><div></div></div></div>").appendTo(refreshBox).fadeIn(300);
-
                 setTimeout(function(){
-                  var refreshPreloader = refreshBox.find('.refresh-preloader'),
-                      deletedRefreshBox = refreshPreloader.fadeOut(300, function(){
-                      refreshPreloader.remove();
-                  });
+                    var refreshPreloader = refreshBox.find('.refresh-preloader'),
+                    deletedRefreshBox = refreshPreloader.fadeOut(300, function(){
+                        refreshPreloader.remove();
+                    });
                 },1500);
-
-              });
+            });
+            $(document).on('click', '.w-refresh', function(){
+                var id = $(this).attr("link");
+                $("div[np^='"+id+"']").toggle();
+            });
+     
+            $('.recentItems').slick({
+              
+                slidesToShow: 13,
+                slidesToScroll: 13,
+                infinite: true,
+                lazyLoad: 'ondemand',
+                prevArrow: '<a class="zero-m pull-left prev-mail btn btn-default waves waves-button btn-sm waves-effect waves-float"><i class="fa fa-angle-left"></i></a>',
+                nextArrow: '<a class="pull-left next-mail btn btn-default waves waves-button btn-sm waves-effect waves-float"><i class="fa fa-angle-right"></i></a>',
+                appendArrows: '.recentHeader',
+                responsive: [
+                {
+                  breakpoint: 1750,
+                  settings: {
+                    slidesToShow: 12,
+                    slidesToScroll: 12,
+                  }
+                },
+                {
+                  breakpoint: 1600,
+                  settings: {
+                    slidesToShow: 11,
+                    slidesToScroll: 11,
+                  }
+                },
+                {
+                  breakpoint: 1450,
+                  settings: {
+                    slidesToShow: 10,
+                    slidesToScroll: 10,
+                  }
+                },
+                {
+                  breakpoint: 1300,
+                  settings: {
+                    slidesToShow: 9,
+                    slidesToScroll: 9,
+                  }
+                },
+                {
+                  breakpoint: 1150,
+                  settings: {
+                    slidesToShow: 8,
+                    slidesToScroll: 8,
+                  }
+                },
+                {
+                  breakpoint: 1000,
+                  settings: {
+                    slidesToShow: 7,
+                    slidesToScroll: 7,
+                  }
+                },
+                {
+                  breakpoint: 850,
+                  settings: {
+                    slidesToShow: 6,
+                    slidesToScroll: 6,
+                  }
+                },
+                {
+                  breakpoint: 700,
+                  settings: {
+                    slidesToShow: 5,
+                    slidesToScroll: 5,
+                  }
+                },
+                {
+                  breakpoint: 675,
+                  settings: {
+                    slidesToShow: 4,
+                    slidesToScroll: 4
+                  }
+                },
+                {
+                  breakpoint: 480,
+                  settings: {
+                    slidesToShow: 3,
+                    slidesToScroll: 3
+                  }
+                }
+                // You can unslick at a given breakpoint now by adding:
+                // settings: "unslick"
+                // instead of a settings object
+              ]
+            });
+            
+            var movieFiltered = false;
+            var seasonFiltered = false;
+            var albumFiltered = false;
+
+            $('.js-filter-movie').on('click', function(){
+              if (movieFiltered === false) {
+                $('.recentItems').slick('slickFilter','.item-season, .item-album, .item-Series, .item-Episode, .item-MusicAlbum');
+                $(this).text('Show Movies');
+                movieFiltered = true;
+              } else {
+                $('.recentItems').slick('slickUnfilter');
+                $(this).text('Hide Movies');
+                movieFiltered = false;
+              }
+            });
+            
+            $('.js-filter-season').on('click', function(){
+              if (seasonFiltered === false) {
+                $('.recentItems').slick('slickFilter','.item-movie, .item-album, .item-Movie, .item-MusicAlbum');
+                $(this).text('Show TV');
+                seasonFiltered = true;
+              } else {
+                $('.recentItems').slick('slickUnfilter');
+                $(this).text('Hide TV');
+                seasonFiltered = false;
+              }
+            });
+            
+            $('.js-filter-album').on('click', function(){
+              if (albumFiltered === false) {
+                $('.recentItems').slick('slickFilter','.item-season, .item-movie, .item-Series, .item-Episode, .item-Movie');
+                $(this).text('Show Music');
+                albumFiltered = true;
+              } else {
+                $('.recentItems').slick('slickUnfilter');
+                $(this).text('Hide Music');
+                albumFiltered = false;
+              }
+            });
+            
+            /*$('.w-refresh').click(function(e){
+                var moreInfo = $(this).closest('div.overlay').addClass("show");
+                console.log(moreInfo);
+                moreInfo.show();
+                e.preventDefault();
+            });*/
 
             $("body").niceScroll({
                 railpadding: {top:0,right:0,left:0,bottom:0},
@@ -534,32 +693,32 @@ endif; ?>
             // check if browser support HTML5 local storage
 			
             <?php if((NZBGETURL != "" && qualifyUser(NZBGETHOMEAUTH)) || (SABNZBDURL != "" && qualifyUser(SABNZBDHOMEAUTH))){ ?>
-			var queueRefresh = 30000;
-			var historyRefresh = 120000; // This really doesn't need to happen that often
-			
-			var queueLoad = function() {
-				<?php if(SABNZBDURL != "") { echo '$("tbody.dl-queue.sabnzbd").load("ajax.php?a=sabnzbd-update&list=queue");'; } ?>
-				<?php if(NZBGETURL != "") { echo '$("tbody.dl-queue.nzbget").load("ajax.php?a=nzbget-update&list=listgroups");'; } ?>
-			};
-			
-			var historyLoad = function() {
-				<?php if(SABNZBDURL != "") { echo '$("tbody.dl-history.sabnzbd").load("ajax.php?a=sabnzbd-update&list=history");'; } ?>
-				<?php if(NZBGETURL != "") { echo '$("tbody.dl-history.nzbget").load("ajax.php?a=nzbget-update&list=history");'; } ?>
-			};
-			
-			// Initial Loads
-			queueLoad();
-			historyLoad();
-			
-			// Interval Loads
-			var queueInterval = setInterval(queueLoad, queueRefresh);
-			var historyInterval = setInterval(historyLoad, historyRefresh);
-			
-			// Manual Load
-			$("#getDownloader").click(function() {
-				queueLoad();
-				historyLoad();
-			});
+            var queueRefresh = 30000;
+            var historyRefresh = 120000; // This really doesn't need to happen that often
+
+            var queueLoad = function() {
+            <?php if(SABNZBDURL != "") { echo '$("tbody.dl-queue.sabnzbd").load("ajax.php?a=sabnzbd-update&list=queue");'; } ?>
+            <?php if(NZBGETURL != "") { echo '$("tbody.dl-queue.nzbget").load("ajax.php?a=nzbget-update&list=listgroups");'; } ?>
+            };
+
+            var historyLoad = function() {
+            <?php if(SABNZBDURL != "") { echo '$("tbody.dl-history.sabnzbd").load("ajax.php?a=sabnzbd-update&list=history");'; } ?>
+            <?php if(NZBGETURL != "") { echo '$("tbody.dl-history.nzbget").load("ajax.php?a=nzbget-update&list=history");'; } ?>
+            };
+
+            // Initial Loads
+            queueLoad();
+			         historyLoad();
+
+            // Interval Loads
+            var queueInterval = setInterval(queueLoad, queueRefresh);
+            var historyInterval = setInterval(historyLoad, historyRefresh);
+
+            // Manual Load
+            $("#getDownloader").click(function() {
+                queueLoad();
+                historyLoad();
+            });
             <?php } ?>
         });
         </script>
@@ -589,10 +748,37 @@ endif; ?>
                         today: { buttonText: '<?php echo $language->translate("TODAY");?>' },
                     },
                     events: [
-<?php if (SICKRAGEURL != "" && qualifyUser(SICKRAGEHOMEAUTH)){ echo getSickrageCalendarWanted($sickrage->future()); echo getSickrageCalendarHistory($sickrage->history("100","downloaded")); } ?>
-<?php if (SONARRURL != "" && qualifyUser(SONARRHOMEAUTH)){ echo getSonarrCalendar($sonarr->getCalendar($startDate, $endDate)); } ?>
-<?php if (RADARRURL != "" && qualifyUser(RADARRHOMEAUTH)){ echo getRadarrCalendar($radarr->getCalendar($startDate, $endDate)); } ?>                 
-<?php if (HEADPHONESURL != "" && qualifyUser(HEADPHONESHOMEAUTH)){ echo getHeadphonesCalendar(HEADPHONESURL, HEADPHONESKEY, "getHistory"); echo getHeadphonesCalendar(HEADPHONESURL, HEADPHONESKEY, "getWanted"); } ?>                                
+<?php 
+if (SICKRAGEURL != "" && qualifyUser(SICKRAGEHOMEAUTH)){
+	try { 
+		echo getSickrageCalendarWanted($sickrage->future());
+	} catch (Exception $e) { 
+		writeLog("error", "SICKRAGE/BEARD ERROR: ".strip($e->getMessage())); 
+	} try { 
+		echo getSickrageCalendarHistory($sickrage->history("100","downloaded"));
+	} catch (Exception $e) { 
+		writeLog("error", "SICKRAGE/BEARD ERROR: ".strip($e->getMessage())); 
+	}
+}
+if (SONARRURL != "" && qualifyUser(SONARRHOMEAUTH)){
+	try {
+		echo getSonarrCalendar($sonarr->getCalendar($startDate, $endDate)); 
+	} catch (Exception $e) { 
+		writeLog("error", "SONARR ERROR: ".strip($e->getMessage())); 
+	}
+}
+if (RADARRURL != "" && qualifyUser(RADARRHOMEAUTH)){ 
+	try { 
+		echo getRadarrCalendar($radarr->getCalendar($startDate, $endDate)); 
+	} catch (Exception $e) { 
+		writeLog("error", "RADARR ERROR: ".strip($e->getMessage())); 
+	}
+}
+if (HEADPHONESURL != "" && qualifyUser(HEADPHONESHOMEAUTH)){
+	echo getHeadphonesCalendar(HEADPHONESURL, HEADPHONESKEY, "getHistory"); 
+	echo getHeadphonesCalendar(HEADPHONESURL, HEADPHONESKEY, "getWanted"); 
+
+}?>                                
                     ],
                     eventRender: function eventRender( event, element, view ) {
                         return ['all', event.imagetype].indexOf($('#imagetype_selector').val()) >= 0
@@ -600,7 +786,7 @@ endif; ?>
 
                     editable: false,
                     droppable: false,
-					timeFormat: '<?php echo CALTIMEFORMAT; ?>',
+					               timeFormat: '<?php echo CALTIMEFORMAT; ?>',
                 });
             });
             $('#imagetype_selector').on('change',function(){

BIN
images/clipboard.png


BIN
images/css.png


BIN
images/html.png


BIN
images/livetv.png


BIN
images/no-list.png


BIN
images/no-np.png


BIN
images/no-search.png


BIN
images/pf-blue.png


BIN
images/pin.png


BIN
images/platforms/android.png


BIN
images/platforms/atv.png


BIN
images/platforms/chrome.png


BIN
images/platforms/chromecast.png


BIN
images/platforms/cloudsync.png


BIN
images/platforms/default.png


BIN
images/platforms/dlna.png


BIN
images/platforms/emby.png


BIN
images/platforms/firefox.png


BIN
images/platforms/gtv.png


BIN
images/platforms/ie.png


BIN
images/platforms/ios.png


BIN
images/platforms/kodi.png


BIN
images/platforms/linux.png


BIN
images/platforms/msedge.png


BIN
images/platforms/opera.png


BIN
images/platforms/osx.png


BIN
images/platforms/pht.png


BIN
images/platforms/playstation.png


BIN
images/platforms/pmh.png


BIN
images/platforms/pmp.png


BIN
images/platforms/pms.png


BIN
images/platforms/roku.png


BIN
images/platforms/safari.png


BIN
images/platforms/samsung.png


BIN
images/platforms/wiiu.png


BIN
images/platforms/win8.png


BIN
images/platforms/wp.png


BIN
images/platforms/xbmc.png


BIN
images/platforms/xbox.png


BIN
images/security.png


+ 201 - 34
index.php

@@ -8,6 +8,11 @@ upgradeCheck();
 // Lazyload settings
 $databaseConfig = configLazy('config/config.php');
 
+// Load Colours/Appearance
+foreach(loadAppearance() as $key => $value) {
+	$$key = $value;
+}
+
 //Set some variables
 ini_set("display_errors", 1);
 ini_set("error_reporting", E_ALL | E_STRICT);
@@ -19,7 +24,7 @@ $hasOptions = "No";
 $settingsicon = "No";
 $settingsActive = "";
 $action = "";
-$title = "Organizr";
+/*$title = "Organizr";
 $topbar = "#333333"; 
 $topbartext = "#66D9EF";
 $bottombar = "#333333";
@@ -31,7 +36,7 @@ $activetabtext = "#FFFFFF";
 $inactiveicon = "#66D9EF";
 $inactivetext = "#66D9EF";
 $loading = "#66D9EF";
-$hovertext = "#000000";
+$hovertext = "#000000";*/
 $loadingIcon = "images/organizr-load-w-thick.gif";
 $baseURL = "";
 
@@ -40,6 +45,8 @@ if(isset($_POST['action'])) {
     $action = $_POST['action'];
 	unset($_POST['action']);
 }
+//Get Invite Code
+$inviteCode = isset($_GET['inviteCode']) ? $_GET['inviteCode'] : null;
 
 // Check for config file
 if(!file_exists('config/config.php')) {
@@ -141,7 +148,7 @@ if (file_exists('config/config.php')) {
 
     endif;
 
-    if($hasOptions == "Yes") :
+    /*if($hasOptions == "Yes") :
 
         $resulto = $file_db->query('SELECT * FROM options');
 
@@ -163,7 +170,7 @@ if (file_exists('config/config.php')) {
 
         endforeach;
 
-    endif;
+    endif;*/
 
     $userpic = md5( strtolower( trim( $USER->email ) ) );
     if(LOADINGICON !== "") : $loadingIcon = LOADINGICON; endif;
@@ -265,6 +272,7 @@ if(file_exists("images/settings2.png")) : $iconRotate = "false"; $settingsIcon =
         <meta name="theme-color" content="#2d89ef">
         <link rel="stylesheet" type="text/css" href="css/addtohomescreen.css">
         <script src="js/addtohomescreen.js"></script>
+        <script src="js/push.js"></script>
 		<!--Other-->
 		<script src="js/ajax.js?v=<?php echo INSTALLEDVERSION; ?>"></script>
         <!--[if lt IE 9]>
@@ -498,13 +506,7 @@ if(file_exists("images/settings2.png")) : $iconRotate = "false"; $settingsIcon =
             padding: 5px 22px;
         }
         <?php endif; ?>
-        <?php if(CUSTOMCSS == "true") : 
-$template_file = "custom.css";
-$file_handle = fopen($template_file, "rb");
-echo fread($file_handle, filesize($template_file));
-fclose($file_handle);
-echo "\n";
-endif; ?>
+        <?php customCSS(); ?>
 
     </style>
 
@@ -552,10 +554,11 @@ endif; ?>
                                     <a class="tab-link">
                                         <?php if($row['iconurl']) : ?>
                                             <i style="font-size: 19px; padding: 0 10px; font-size: 19px;">
+                                                <span id="<?=$row['url'];?>s" class="badge badge-success" style="position: absolute;z-index: 100;right: 0px;"></span>
                                                 <img src="<?=$row['iconurl'];?>" style="height: 30px; width: 30px; margin-top: -2px;">
                                             </i>
                                         <?php else : ?>
-                                            <i class="fa <?=$row['icon'];?> fa-lg"></i>
+                                            <i class="fa <?=$row['icon'];?> fa-lg"><span id="<?=$row['url'];?>s" class="badge badge-success" style="position: absolute;z-index: 100;right: 0px;"></span></i>
                                         <?php endif; ?>
                                         <?=$row['name'];?>
                                     </a>
@@ -633,7 +636,12 @@ endif; ?>
                                 <i class="mdi mdi-refresh"></i>
                             </a>
                         </li>
-                        <li style="display: none" id="splitView" class="dropdown some-btn">
+                        <li class="dropdown some-btn">
+                            <a id="popout" class="popout">
+                                <i class="mdi mdi-window-restore"></i>
+                            </a>
+                        </li>
+                        <li style="display: block" id="splitView" class="dropdown some-btn">
                             <a class="spltView">
                                 <i class="mdi mdi-window-close"></i>
                             </a>
@@ -645,7 +653,7 @@ endif; ?>
             <!--Content-->
             <div id="content" class="content" style="">
                 <script>addToHomescreen();</script>
-
+				
                 <!--Load Framed Content-->
                 <?php if($needSetup == "Yes" && $configReady == "Yes") : ?>
                 <div class="table-wrapper" style="background:<?=$sidebar;?>;">
@@ -735,13 +743,13 @@ endif; ?>
                                     <div class="big-box text-left">
 
                                         <h3 class="text-center"><?php echo $language->translate("SPECIFY_LOCATION");?></h3>
-                                        <h5 class="text-left"><strong><?php echo $language->translate("CURRENT_DIRECTORY");?>: <?php echo __DIR__; ?> <br><?php echo $language->translate("PARENT_DIRECTORY");?>: <?php echo dirname(__DIR__); ?></strong></h5>
+                                        <h5 class="text-left"><strong><?php echo $language->translate("CURRENT_DIRECTORY");?>: <?php echo str_replace("\\","/",__DIR__); ?> <br><?php echo $language->translate("PARENT_DIRECTORY");?>: <?php echo str_replace("\\","/",dirname(__DIR__)); ?></strong></h5>
                                         <form class="controlbox" name="setupDatabase" id="setupDatabase" action="" method="POST" data-smk-icon="glyphicon-remove-sign">
                                             <input type="hidden" name="action" value="createLocation" />
 
                                             <div class="form-group">
 
-                                                <input type="text" class="form-control material" name="database_Location" autofocus value="<?php echo dirname(__DIR__);?>" autocorrect="off" autocapitalize="off" required>
+                                                <input type="text" class="form-control material" name="database_Location" autofocus value="<?php echo str_replace("\\","/",dirname(__DIR__));?>" autocorrect="off" autocapitalize="off" required>
                                                 <h5><?php echo $language->translate("SET_DATABASE_LOCATION");?></h5>
                                                 <?php echo getTimezone();?>
                                                 <h5><?php echo $language->translate("SET_TIMEZONE");?></h5>
@@ -908,7 +916,7 @@ endif; ?>
                                     <?php if($USER->error!="") : ?>
                                     <p class="error">Error: <?php echo $USER->error; ?></p>
                                     <?php endif; ?>
-                                    <form name="log in" id="login" action="" method="POST" data-smk-icon="glyphicon-remove-sign">
+                                    <form name="log in" id="login" action="" method="POST">
                                         <h4 class="text-center"><?php echo $language->translate("LOGIN");?></h4>
                                         <div class="form-group">
                                             <input type="hidden" name="op" value="login">
@@ -1006,7 +1014,7 @@ endif; ?>
                 </div>
             </div>
         </div>
-        <?php endif; endif;?>
+        <?php endif; endif; ?>
         <?php if($configReady == "Yes") : if($USER->authenticated) : ?>
         <div style="background:<?=$topbar;?>;" class="logout-modal modal fade">
             <div class="table-wrapper" style="background: <?=$sidebar;?>">
@@ -1034,6 +1042,88 @@ endif; ?>
             </div>
         </div>
         <?php endif; endif;?>
+		<?php if(isset($_GET['inviteCode'])){ ?>
+		<div id="inviteSet" class="login-modal modal fade">
+			<div style="background:<?=$sidebar;?>;" class="table-wrapper">
+				<div class="table-row">
+					<div class="table-cell text-center">
+						<button style="color:<?=$topbartext;?>;" type="button" class="close" data-dismiss="modal" aria-label="Close">
+							<span aria-hidden="true">&times;</span>
+						</button>
+						<div class="login i-block">
+							<div class="content-box">
+								<div style="background:<?=$topbar;?>;" class="biggest-box">
+
+									<h1 style="color:<?=$topbartext;?>;" class="zero-m text-uppercase"><?php echo $language->translate("WELCOME");?></h1>
+
+								</div>
+								<div class="big-box text-left login-form">
+
+									<?php if($USER->error!="") : ?>
+									<p class="error">Error: <?php echo $USER->error; ?></p>
+									<?php endif; ?>
+									<form name="checkInviteForm" id="checkInviteForm" onsubmit="return false;" data-smk-icon="glyphicon-remove-sign">
+										<h4 class="text-center"><?php echo $language->translate("CHECK_INVITE");?></h4>
+										<div class="form-group">
+											<input style="font-size: 400%; height: 100%" type="text" class="form-control yellow-bg text-center text-uppercase" name="inviteCode" placeholder="<?php echo $language->translate("CODE");?>" autocomplete="off" autocorrect="off" autocapitalize="off" value="<?=$inviteCode;?>" maxlength="6" spellcheck="false" autofocus required>
+										</div>
+
+										<button id="checkInviteForm_submit" style="background:<?=$topbar;?>;" type="submit" class="btn btn-block btn-info text-uppercase waves" value="checkInvite"><text style="color:<?=$topbartext;?>;"><?php echo $language->translate("SUBMIT_CODE");?></text></button>
+
+									</form> 
+									
+									<div style="display: none" id="chooseMethod">
+										<h4 class="text-center"><?php echo $language->translate("HAVE_ACCOUNT");?></h4>
+										<button id="yesPlexButton" style="background:<?=$topbartext;?>;" class="btn btn-block btn-info text-uppercase waves"><text style="color:<?=$topbar;?>;"><?php echo $language->translate("YES");?></text></button>
+										<button id="noPlexButton" style="background:<?=$topbartext;?>;" class="btn btn-block btn-info text-uppercase waves"><text style="color:<?=$topbar;?>;"><?php echo $language->translate("NO");?></text></button>
+									</div>
+									
+									<form style="display:none" name="useInviteForm" id="useInviteForm" onsubmit="return false;" data-smk-icon="glyphicon-remove-sign">
+										<h4 class="text-center"><?php echo $language->translate("ENTER_PLEX_NAME");?></h4>
+										<h4 id="accountMade" style="display: none" class="text-center">
+											<span class="label label-primary"><?php echo $language->translate("ACCOUNT_MADE");?></span>
+										</h4>
+										<div id="accountSubmitted" style="display: none" class="panel panel-success">
+											<div class="panel-heading">
+												<h3 class="panel-title"><?php echo explosion($language->translate('ACCOUNT_SUBMITTED'), 0);?></h3>
+											</div>
+											<div class="panel-body">
+												<?php echo explosion($language->translate('ACCOUNT_SUBMITTED'), 1);?><br/>
+												<?php echo explosion($language->translate('ACCOUNT_SUBMITTED'), 2);?><br/>
+												<?php echo explosion($language->translate('ACCOUNT_SUBMITTED'), 3);?>
+											</div>
+										</div>
+										<div class="form-group">
+											<input style="font-size: 400%; height: 100%" type="hidden" class="form-control yellow-bg text-center text-uppercase" name="inviteCode" placeholder="<?php echo $language->translate("CODE");?>" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" value="<?=$inviteCode;?>" maxlength="6" required>
+											<input type="text" class="form-control material" name="inviteUser" placeholder="<?php echo $language->translate("USERNAME_EMAIL");?>" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" value="" autofocus required>
+										</div>
+
+										<button id="useInviteForm_submit" style="background:<?=$topbar;?>;" type="submit" class="btn btn-block btn-info text-uppercase waves" value="useInvite"><text style="color:<?=$topbartext;?>;"><?php echo $language->translate("JOIN");?></text></button>
+										<button id="plexYesGoBack" style="background:<?=$topbartext;?>;" class="btn btn-block btn-info text-uppercase waves"><text style="color:<?=$topbar;?>;"><?php echo $language->translate("GO_BACK");?></text></button>
+
+									</form>
+
+									<form style="display:none" name="joinPlexForm" id="joinPlexForm" onsubmit="return false;" data-smk-icon="glyphicon-remove-sign">
+										<h4 class="text-center"><?php echo $language->translate("CREATE_PLEX");?></h4>
+										<div class="form-group">
+											<input type="text" class="form-control material" name="joinUser" placeholder="<?php echo $language->translate("USERNAME");?>" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" value="" autofocus required>
+											<input type="text" class="form-control material" name="joinEmail" placeholder="<?php echo $language->translate("EMAIL");?>" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" value="" required>
+											<input type="password" class="form-control material" name="joinPassword" placeholder="<?php echo $language->translate("PASSWORD");?>" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" value="" required>
+										</div>
+
+										<button id="joinPlexForm_submit" style="background:<?=$topbar;?>;" type="submit" class="btn btn-block btn-info text-uppercase waves" value="useInvite"><text style="color:<?=$topbartext;?>;"><?php echo $language->translate("SIGN_UP");?></text></button>
+										<button id="plexNoGoBack" style="background:<?=$topbartext;?>;" class="btn btn-block btn-info text-uppercase waves"><text style="color:<?=$topbar;?>;"><?php echo $language->translate("GO_BACK");?></text></button>
+
+									</form> 
+
+								</div>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+		<?php } ?>
 
         <!--Scripts-->
         <script src="<?=$baseURL;?>bower_components/jquery/dist/jquery.min.js"></script>
@@ -1057,7 +1147,7 @@ endif; ?>
         <script src="<?=$baseURL;?>bower_components/smoke/dist/js/smoke.min.js"></script>
 
         <!--Notification-->
-        <script src="<?=$baseURL;?>js/notifications/notificationFx.js"></script>
+        <script src="<?=$baseURL;?>js/notifications/notificationFx.js?v=<?php echo INSTALLEDVERSION; ?>"></script>
 
         <!--Custom Scripts-->
         <script src="<?=$baseURL;?>js/common.js"></script>
@@ -1106,7 +1196,7 @@ endif; ?>
 
                     type: notifyType,
                     onClose: function () {
-                        $(".ns-box.ns-effect-thumbslider").fadeOut(400);
+                        $(".ns-box").fadeOut(400);
                     }
 
                 });
@@ -1117,10 +1207,10 @@ endif; ?>
 
         }
         $('#loginSubmit').click(function() {
-            if ($('#login').smkValidate()) {
+            /*if ($('#login').smkValidate()) {
                 console.log("validated");
             }
-            console.log("didnt validate");
+            console.log("didnt validate");*/
         });
         $('#registerSubmit').click(function() {
             if ($('#registration').smkValidate()) {
@@ -1130,7 +1220,6 @@ endif; ?>
             User.processRegistration();
         });
         $("#editInfo").click(function(){
-
             $( "div[id^='editInfoDiv']" ).toggle();
             $( "div[id^='buttonsDiv']" ).toggle();
         });
@@ -1146,6 +1235,14 @@ endif; ?>
             $("#switchCreateUser").toggle();
             $("#welcomeGoBack").toggle();
         });
+		$("#plexNoGoBack").click(function(){
+            $("#joinPlexForm").toggle();
+            $("#chooseMethod").toggle();
+        });
+		$("#plexYesGoBack").click(function(){
+            $("#useInviteForm").toggle();
+            $("#chooseMethod").toggle();
+        });	
         $("#welcomeGoBack2").click(function(){
             $( "form[id^='login']" ).toggle();
             $("#userPassForm").toggle();
@@ -1180,12 +1277,15 @@ endif; ?>
         $(".log-in").click(function(e){
             var e1 = document.querySelector(".log-in"),
                 e2 = document.querySelector(".login-modal");
-            cta(e1, e2, {relativeToWindow: true}, function () {
+            	cta(e1, e2, {relativeToWindow: true}, function () {
                 $('.login-modal').modal("show");
             });
-
             e.preventDefault();
         });
+		//InviteCode
+		<?php if(isset($_GET['inviteCode'])){ ?>
+		$('#inviteSet').modal("show");	
+		<?php } ?>
 
         //Logout
         $(".logout").click(function(e){
@@ -1215,6 +1315,61 @@ endif; ?>
         });
 
         $(document).ready(function(){
+			//PLEX INVITE SHIT
+			$('#checkInviteForm').on('submit', function () {
+                ajax_request('POST', 'validate-invite', {
+                    invitecode: $('#checkInviteForm [name=inviteCode]').val(),
+                }).done(function(data){ 
+					var result = JSON.stringify(data).includes("success");
+					if(result === true){
+						$('#checkInviteForm').hide();
+						$('#chooseMethod').show();
+						console.log(result);
+					}
+				});
+
+            });
+			$('#useInviteForm').on('submit', function () {
+                ajax_request('POST', 'use-invite', {
+                    invitecode: $('#useInviteForm [name=inviteCode]').val(),
+                    inviteuser: $('#useInviteForm [name=inviteUser]').val(),
+                }).done(function(data){ 
+					var result = JSON.stringify(data).includes("success");
+					console.log(result);
+					if(result === true){
+						//$('#checkInviteForm').hide();
+						//$('#chooseMethod').show();
+						$('#accountSubmitted').show();
+						console.log(result);
+					}
+				});
+
+            });
+			$('#joinPlexForm').on('submit', function () {
+                ajax_request('POST', 'join-plex', {
+                    joinuser: $('#joinPlexForm [name=joinUser]').val(),
+                    joinemail: $('#joinPlexForm [name=joinEmail]').val(),
+                    joinpassword: $('#joinPlexForm [name=joinPassword]').val(),
+                }).done(function(data){ 
+					var result = JSON.stringify(data).includes("success");
+					if(result === true){
+						$('#joinPlexForm').hide();
+						$('#useInviteForm').show();
+						$('#accountMade').show();
+						$('input[name=inviteUser]').val($('input[name=joinUser]').val());
+						console.log(result);
+					}
+				});
+
+            });
+			$("#yesPlexButton").click(function(){
+				$('#chooseMethod').hide();
+				$('#useInviteForm').show();
+			});
+			$("#noPlexButton").click(function(){
+				$('#chooseMethod').hide();
+				$('#joinPlexForm').show();
+			});
             $('#userCreateForm').submit(function(event) {
 
                 var formData = {
@@ -1342,8 +1497,12 @@ endif; ?>
                 });
 
             },500);
-
         });
+        $('#popout').on('click tap', function(){
+            var activeFrame = $('#content').find('.active').children('iframe');
+            console.log(activeFrame.attr('src'));
+            window.open(activeFrame.attr('src'), '_blank');
+        });    
         $('#reload').on('contextmenu', function(e){
 
             $("i[class^='mdi mdi-refresh']").attr("class", "mdi mdi-refresh fa-spin");
@@ -1370,13 +1529,21 @@ endif; ?>
             return false;
 
         });
-        $('#splitView').on('click tap', function(){
-
-            $('#splitView').hide();
+        $('#splitView').on('contextmenu', function(e){
+			e.stopPropagation();
+            //$('#splitView').hide();
             $("#content").attr("class", "content");
             $("li[class^='tab-item rightActive']").attr("class", "tab-item");
             $("#contentRight").html('');
-
+			return false;
+        });
+		$('#splitView').on('click tap', function(){
+			var activeFrame = $('#content').find('.active');
+			var getCurrentTab = $("li[class^='tab-item active']");
+			getCurrentTab.removeClass('active');
+			getCurrentTab.find('img').removeClass('TabOpened');
+			$("img[class^='TabOpened']").parents("li").trigger("click");
+			activeFrame.remove();
         });
         <?php if($iconRotate == "true") : ?>   
         $("li[id^='settings.phpx']").on('click tap', function(){
@@ -1426,7 +1593,7 @@ endif; ?>
 
                 currentframe.attr("class", "iframe active");
                 document.title = thistitle;
-                window.location.href = '#' + thisname;
+                //window.location.href = '#' + thisname;
                 setHeight();
 
                 $("li[class^='tab-item active']").attr("class", "tab-item");
@@ -1441,9 +1608,9 @@ endif; ?>
 
                     $("#content div[class^='iframe active']").attr("class", "iframe hidden");
 
-                    $( '<div class="iframe active" data-content-url="'+thisid+'"><iframe scrolling="auto" sandbox="allow-forms allow-same-origin allow-pointer-lock allow-scripts allow-popups allow-modals allow-top-navigation" allowfullscreen="true" webkitallowfullscreen="true" frameborder="0" style="width:100%; height:100%; position: absolute;" src="'+thisid+'"></iframe></div>' ).appendTo( "#content" );
+                    $( '<div class="iframe active" data-content-name="'+thisname+'" data-content-url="'+thisid+'"><iframe scrolling="auto" sandbox="allow-forms allow-same-origin allow-pointer-lock allow-scripts allow-popups allow-modals allow-top-navigation" allowfullscreen="true" webkitallowfullscreen="true" frameborder="0" style="width:100%; height:100%; position: absolute;" src="'+thisid+'"></iframe></div>' ).appendTo( "#content" );
                     document.title = thistitle;
-                    window.location.href = '#' + thisname;
+                   // window.location.href = '#' + thisname;
 
                     setHeight();
 
@@ -1496,7 +1663,7 @@ endif; ?>
 
                     $("#contentRight div[class^='iframe active']").attr("class", "iframe hidden");
 
-                    $( '<div class="iframe active" data-content-url="'+thisid+'"><iframe scrolling="auto" sandbox="allow-forms allow-same-origin allow-pointer-lock allow-scripts allow-popups allow-modals allow-top-navigation" allowfullscreen="true" webkitallowfullscreen="true" frameborder="0" style="width:100%; height:100%; position: absolute;" src="'+thisid+'"></iframe></div>' ).appendTo( "#contentRight" );
+                    $( '<div class="iframe active" data-content-name="'+thisname+'" data-content-url="'+thisid+'"><iframe scrolling="auto" sandbox="allow-forms allow-same-origin allow-pointer-lock allow-scripts allow-popups allow-modals allow-top-navigation" allowfullscreen="true" webkitallowfullscreen="true" frameborder="0" style="width:100%; height:100%; position: absolute;" src="'+thisid+'"></iframe></div>' ).appendTo( "#contentRight" );
                     document.title = thistitle;
                     window.location.href = '#' + thisname;
 

+ 2 - 2
js/notifications/notificationFx.js

@@ -92,9 +92,9 @@
 		// dismiss after [options.ttl]ms
 		var self = this;
 		this.dismissttl = setTimeout( function() {
-			if( self.active ) {
+			//if( self.active ) {
 				self.dismiss();
-			}
+			//}
 		}, this.options.ttl );
 
 		// init events

+ 581 - 0
js/push.js

@@ -0,0 +1,581 @@
+/**
+ * Push
+ * =======
+ * A compact, cross-browser solution for the JavaScript Notifications API
+ *
+ * Credits
+ * -------
+ * Tsvetan Tsvetkov (ttsvetko)
+ * Alex Gibson (alexgibson)
+ *
+ * License
+ * -------
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015-2017 Tyler Nickerson
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @preserve
+ */
+
+(function (global, factory) {
+
+    'use strict';
+
+    /* Use AMD */
+    if (typeof define === 'function' && define.amd) {
+        define(function () {
+            return new (factory(global, global.document))();
+        });
+    }
+    /* Use CommonJS */
+    else if (typeof module !== 'undefined' && module.exports) {
+        module.exports = new (factory(global, global.document))();
+    }
+    /* Use Browser */
+    else {
+        global.Push = new (factory(global, global.document))();
+    }
+
+})(typeof window !== 'undefined' ? window : this, function (w, d) {
+
+    var Push = function () {
+
+        /**********************
+            Local Variables
+        /**********************/
+
+        var
+        self = this,
+        isUndefined   = function (obj) { return obj === undefined; },
+        isString   = function (obj) { return typeof obj === 'string' },
+        isFunction = function (obj) { return obj && {}.toString.call(obj) === '[object Function]'; },
+
+        /* ID to use for new notifications */
+        currentId = 0,
+
+        /* Message to show if there is no suport to Push Notifications */
+        incompatibilityErrorMessage = 'PushError: push.js is incompatible with browser.',
+
+        /* Map of open notifications */
+        notifications = {},
+
+        /* Testing variable for the last service worker path used */
+        lastWorkerPath = null,
+
+        /**********************
+            Helper Functions
+        /**********************/
+
+        /**
+         * Closes a notification
+         * @param {Notification} notification
+         * @return {Boolean} boolean denoting whether the operation was successful
+         */
+        closeNotification = function (id) {
+            var errored = false,
+                notification = notifications[id];
+
+            if (typeof notification !== 'undefined') {
+                /* Safari 6+, Chrome 23+ */
+                if (notification.close) {
+                    notification.close();
+                /* Legacy webkit browsers */
+                } else if (notification.cancel) {
+                    notification.cancel();
+                /* IE9+ */
+                } else if (w.external && w.external.msIsSiteMode) {
+                    w.external.msSiteModeClearIconOverlay();
+                } else {
+                    errored = true;
+                    throw new Error('Unable to close notification: unknown interface');
+                }
+
+                if (!errored) {
+                    return removeNotification(id);
+                }
+            }
+
+            return false;
+        },
+
+        /**
+         * Adds a notification to the global dictionary of notifications
+         * @param {Notification} notification
+         * @return {Integer} Dictionary key of the notification
+         */
+        addNotification = function (notification) {
+            var id = currentId;
+            notifications[id] = notification;
+            currentId++;
+            return id;
+        },
+
+        /**
+         * Removes a notification with the given ID
+         * @param  {Integer} id - Dictionary key/ID of the notification to remove
+         * @return {Boolean} boolean denoting success
+         */
+        removeNotification = function (id) {
+            var dict = {},
+                success = false,
+                key;
+
+            for (key in notifications) {
+                if (notifications.hasOwnProperty(key)) {
+                    if (key != id) {
+                        dict[key] = notifications[key];
+                    } else {
+                        // We're successful if we omit the given ID from the new array
+                        success = true;
+                    }
+                }
+            }
+            // Overwrite the current notifications dictionary with the filtered one
+            notifications = dict;
+            return success;
+        },
+
+        prepareNotification = function (id, options) {
+            var wrapper;
+
+            /* Wrapper used to get/close notification later on */
+            wrapper = {
+                get: function () {
+                    return notifications[id];
+                },
+
+                close: function () {
+                    closeNotification(id);
+                }
+            };
+
+            /* Autoclose timeout */
+            if (options.timeout) {
+                setTimeout(function () {
+                    wrapper.close();
+                }, options.timeout);
+            }
+
+            return wrapper;
+        },
+
+        /**
+         * Callback function for the 'create' method
+         * @return {void}
+         */
+        createCallback = function (title, options, resolve) {
+            var notification,
+                onClose;
+
+            /* Set empty settings if none are specified */
+            options = options || {};
+
+            /* Set the last service worker path for testing */
+            self.lastWorkerPath = options.serviceWorker || 'serviceWorker.js';
+
+            /* onClose event handler */
+            onClose = function (id) {
+                /* A bit redundant, but covers the cases when close() isn't explicitly called */
+                removeNotification(id);
+                if (isFunction(options.onClose)) {
+                    options.onClose.call(this, notification);
+                }
+            };
+
+            /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */
+            if (w.Notification) {
+                try {
+                    notification =  new w.Notification(
+                        title,
+                        {
+                            icon: (isString(options.icon) || isUndefined(options.icon)) ? options.icon : options.icon.x32,
+                            body: options.body,
+                            tag: options.tag,
+                            requireInteraction: options.requireInteraction,
+                            silent: options.silent
+                        }
+                    );
+                } catch (e) {
+                    if (w.navigator) {
+                        /* Register ServiceWorker using lastWorkerPath */
+                        w.navigator.serviceWorker.register(self.lastWorkerPath);
+                        w.navigator.serviceWorker.ready.then(function(registration) {
+                            var localData = {
+                                id: currentId,
+                                link: options.link,
+                                origin: document.location.href,
+                                onClick: (isFunction(options.onClick)) ? options.onClick.toString() : '',
+                                onClose: (isFunction(options.onClose)) ? options.onClose.toString() : ''
+                            };
+
+                            if (typeof options.data !== 'undefined' && options.data !== null)
+                                localData = Object.assign(localData, options.data);
+
+                            /* Show the notification */
+                            registration.showNotification(
+                                title,
+                                {
+                                    icon: options.icon,
+                                    body: options.body,
+                                    vibrate: options.vibrate,
+                                    tag: options.tag,
+                                    data: localData,
+                                    requireInteraction: options.requireInteraction
+                                }
+                            ).then(function() {
+                                var id;
+
+                                /* Find the most recent notification and add it to the global array */
+                                registration.getNotifications().then(function(notifications) {
+                                    id = addNotification(notifications[notifications.length - 1]);
+
+                                    /* Send an empty message so the ServiceWorker knows who the client is */
+                                    registration.active.postMessage('');
+
+                                    /* Listen for close requests from the ServiceWorker */
+                                    navigator.serviceWorker.addEventListener('message', function (event) {
+                                        var data = JSON.parse(event.data);
+
+                                        if (data.action === 'close' && Number.isInteger(data.id))
+                                            removeNotification(data.id);
+                                    });
+
+                                    resolve(prepareNotification(id, options));
+                                });
+                            });
+                        });
+                    }
+                }
+
+            /* Legacy webkit browsers */
+            } else if (w.webkitNotifications) {
+
+                notification = w.webkitNotifications.createNotification(
+                    options.icon,
+                    title,
+                    options.body
+                );
+
+                notification.show();
+
+            /* Firefox Mobile */
+            } else if (navigator.mozNotification) {
+
+                notification = navigator.mozNotification.createNotification(
+                    title,
+                    options.body,
+                    options.icon
+                );
+
+                notification.show();
+
+            /* IE9+ */
+            } else if (w.external && w.external.msIsSiteMode()) {
+
+                //Clear any previous notifications
+                w.external.msSiteModeClearIconOverlay();
+                w.external.msSiteModeSetIconOverlay(
+                    ((isString(options.icon) || isUndefined(options.icon))
+                    ? options.icon
+                    : options.icon.x16), title
+                );
+                w.external.msSiteModeActivate();
+
+                notification = {};
+            } else {
+                throw new Error('Unable to create notification: unknown interface');
+            }
+
+            if (typeof(notification) !== 'undefined') {
+                var id = addNotification(notification),
+                    wrapper = prepareNotification(id, options);
+
+                /* Notification callbacks */
+                if (isFunction(options.onShow))
+                    notification.addEventListener('show', options.onShow);
+
+                if (isFunction(options.onError))
+                    notification.addEventListener('error', options.onError);
+
+                if (isFunction(options.onClick))
+                    notification.addEventListener('click', options.onClick);
+
+                notification.addEventListener('close', function() {
+                    onClose(id);
+                });
+
+                notification.addEventListener('cancel', function() {
+                    onClose(id);
+                });
+
+                /* Return the wrapper so the user can call close() */
+                resolve(wrapper);
+            }
+
+            resolve({}); // By default, pass an empty wrapper
+        },
+
+        /**
+         * Permission types
+         * @enum {String}
+         */
+        Permission = {
+            DEFAULT: 'default',
+            GRANTED: 'granted',
+            DENIED: 'denied'
+        },
+
+        Permissions = [Permission.GRANTED, Permission.DEFAULT, Permission.DENIED];
+
+        /* Allow enums to be accessible from Push object */
+        self.Permission = Permission;
+
+        /*****************
+            Permissions
+        /*****************/
+
+        /**
+         * Requests permission for desktop notifications
+         * @param {Function} callback - Function to execute once permission is granted
+         * @return {void}
+         */
+        self.Permission.request = function (onGranted, onDenied) {
+            var existing = self.Permission.get();
+
+            /* Return if Push not supported */
+            if (!self.isSupported) {
+                throw new Error(incompatibilityErrorMessage);
+            }
+
+            /* Default callback */
+            callback = function (result) {
+                switch (result) {
+
+                    case self.Permission.GRANTED:
+                        if (onGranted) onGranted();
+                        break;
+
+                    case self.Permission.DENIED:
+                        if (onDenied) onDenied();
+                        break;
+
+                }
+
+            };
+
+            /* Permissions already set */
+            if (existing !== self.Permission.DEFAULT) {
+                callback(existing);
+            }
+            /* Safari 6+, Chrome 23+ */
+            else if (w.Notification && w.Notification.requestPermission) {
+                Notification.requestPermission(callback);
+            }
+            /* Legacy webkit browsers */
+            else if (w.webkitNotifications && w.webkitNotifications.checkPermission) {
+                w.webkitNotifications.requestPermission(callback);
+            } else {
+                throw new Error(incompatibilityErrorMessage);
+            }
+        };
+
+        /**
+         * Returns whether Push has been granted permission to run
+         * @return {Boolean}
+         */
+        self.Permission.has = function () {
+            return Permission.get() === Permission.GRANTED;
+        };
+
+        /**
+         * Gets the permission level
+         * @return {Permission} The permission level
+         */
+        self.Permission.get = function () {
+
+            var permission;
+
+            /* Return if Push not supported */
+            if (!self.isSupported) { throw new Error(incompatibilityErrorMessage); }
+
+            /* Safari 6+, Chrome 23+ */
+            if (w.Notification && w.Notification.permissionLevel) {
+                permission = w.Notification.permissionLevel;
+
+            /* Legacy webkit browsers */
+            } else if (w.webkitNotifications && w.webkitNotifications.checkPermission) {
+                permission = Permissions[w.webkitNotifications.checkPermission()];
+
+            /* Firefox 23+ */
+            } else if (w.Notification && w.Notification.permission) {
+                permission = w.Notification.permission;
+
+            /* Firefox Mobile */
+            } else if (navigator.mozNotification) {
+                permission = Permission.GRANTED;
+
+            /* IE9+ */
+            } else if (w.external && w.external.msIsSiteMode() !== undefined) {
+                permission = w.external.msIsSiteMode() ? Permission.GRANTED : Permission.DEFAULT;
+            } else {
+                throw new Error(incompatibilityErrorMessage);
+            }
+
+            return permission;
+
+        };
+
+        /*********************
+            Other Functions
+        /*********************/
+
+        /**
+         * Detects whether the user's browser supports notifications
+         * @return {Boolean}
+         */
+        self.isSupported = (function () {
+
+             var isSupported = false;
+
+             try {
+
+                 isSupported =
+
+                     /* Safari, Chrome */
+                     !!(w.Notification ||
+
+                     /* Chrome & ff-html5notifications plugin */
+                     w.webkitNotifications ||
+
+                     /* Firefox Mobile */
+                     navigator.mozNotification ||
+
+                     /* IE9+ */
+                     (w.external && w.external.msIsSiteMode() !== undefined));
+
+             } catch (e) {}
+
+             return isSupported;
+
+         })();
+
+         /**
+          * Creates and displays a new notification
+          * @param {Array} options
+          * @return {Promise}
+          */
+        self.create = function (title, options) {
+            var promiseCallback;
+
+            /* Fail if the browser is not supported */
+            if (!self.isSupported) {
+                throw new Error(incompatibilityErrorMessage);
+            }
+
+            /* Fail if no or an invalid title is provided */
+            if (!isString(title)) {
+                throw new Error('PushError: Title of notification must be a string');
+            }
+
+            /* Request permission if it isn't granted */
+            if (!self.Permission.has()) {
+                promiseCallback = function(resolve, reject) {
+                    self.Permission.request(function() {
+                        try {
+                            createCallback(title, options, resolve);
+                        } catch (e) {
+                            reject(e);
+                        }
+                    }, function() {
+                        reject("Permission request declined");
+                    });
+                };
+            } else {
+                promiseCallback = function(resolve, reject) {
+                    try {
+                        createCallback(title, options, resolve);
+                    } catch (e) {
+                        reject(e);
+                    }
+                };
+            }
+
+            return new Promise(promiseCallback);
+        };
+
+        /**
+         * Returns the notification count
+         * @return {Integer} The notification count
+         */
+        self.count = function () {
+            var count = 0,
+                key;
+
+            for (key in notifications)
+                count++;
+
+            return count;
+        },
+
+        /**
+         * Internal function that returns the path of the last service worker used
+         * For testing purposes only
+         * @return {String} The service worker path
+         */
+        self.__lastWorkerPath = function () {
+            return self.lastWorkerPath;
+        },
+
+        /**
+         * Closes a notification with the given tag
+         * @param {String} tag - Tag of the notification to close
+         * @return {Boolean} boolean denoting success
+         */
+        self.close = function (tag) {
+            var key;
+            for (key in notifications) {
+                notification = notifications[key];
+                /* Run only if the tags match */
+                if (notification.tag === tag) {
+                    /* Call the notification's close() method */
+                    return closeNotification(key);
+                }
+            }
+        };
+
+        /**
+         * Clears all notifications
+         * @return {void}
+         */
+        self.clear = function () {
+            var success = true;
+
+            for (key in notifications)
+                success = success && closeNotification(key);
+
+            return success;
+        };
+    };
+
+    return Push;
+
+});

+ 55 - 2
lang/de.ini

@@ -195,7 +195,7 @@ WEEK = "Woche"
 NZBGET_URL = "NZBGet URL"
 NZBGET_PORT = "NZBGet Port"
 QUEUE = "Warteschlange"
-HISTORY = "Historie"
+HISTORY = "Verlauf"
 FILE = "Datei"
 STATUS = "Status"
 CATEGORY = "Kategorie"
@@ -251,4 +251,57 @@ GIT_BRANCH = "Github Branch, die zur erzwungenen Installation genutz wird (nur z
 GIT_CHECK = "Auf neues 'master' Release prüfen"
 GIT_FORCE = "Installation der Branch erzwingen"
 GIT_FORCE_CONFIRM = "Sicher diese Branch installieren? Von neuen auf alte Versionen zu wechseln wird weder empfohlen, noch unterstützt."
-SPEED_TEST = "Speed Test"
+SPEED_TEST = "Speed Test"
+NOTICE_COLOR = "Notice Color"
+NOTICE_TITLE = "Notice Title"
+NOTICE_MESSAGE = "Notice Message"
+SHOW_NAMES = "Show Names"
+NOTICE_LAYOUT = "Notice Layout"
+RECENT_ITEMS_LIMIT = "Recent Items Limit"
+ALLOW_SEARCH = "Allow Search"
+CHECK_INVITE = "Enter Invite Code to Procced"
+CODE = "Invite Code"
+INVITE_CODE = "Invite Code"
+DATE_SENT = "Date Sent"
+DATE_USED = "Date Used"
+VALID = "Valid"
+SUBMIT_CODE = "Submit Code"
+IFRAME_CAN_BE_FRAMED = "iFrame Can Be Framed"
+IFRAME_CANNOT_BE_FRAMED = "iFrame Cannot Be Framed"
+CODE_SUCCESS = "Invite Code Has Been Validated"
+CODE_ERROR = "Invite Code is incorrect or not valid"
+HAVE_ACCOUNT = "Do You Have A PLEX Account Already?"
+USERNAME_EMAIL = "Username or E-Mail"
+INVITE_SUCCESS = "You have now been invited, please check your email to accept"
+INVITE_ERROR = "Invite Code not valid, contact admin"
+CREATE_PLEX = "Create PLEX Account"
+JOIN = "Join"
+SIGN_UP = "Sign-up"
+JOIN_SUCCESS = "You have successfully signed up for PLEX, please click join now"
+JOIN_ERROR = "An error occured signing up for PLEX - Username or email might be in use - Try again"
+SEND_INVITE = "Create/Mail Invite"
+USED_BY = "Used By"
+ACCOUNT_MADE = "PLEX Account is now created, Click Join now"
+USERNAME_NAME = "Username or Name"
+ACCOUNT_SUBMITTED = "PLEX Invite Sent|1. Check Email and Accept Invite|2. Close This Modal with Small 'x' on Top Right|3. Sign in"
+PLEX_TAB_NAME = "PLEX Tab Name [only use this if your PLEX URL above is a sub-domain - i.e. https://plex.domain.com]"
+IPINFO_TOKEN = "Get Token from https://ipinfo.io/account/registration as Default will expire"
+GET_PLEX_TOKEN = "Get PLEX Token"
+EMAIL_INVITE_HEADER = "Join My|Server"
+EMAIL_INVITE_TITLE = "LOOK WHO JUST GOT AN INVITE"
+EMAIL_INVITE_MESSAGE = "Here is an invite to join my|server.  The code to join is:"
+EMAIL_INVITE_BUTTON = "JOIN MY|SERVER"
+EMAIL_INVITE_SUBTITLE = "What do I do?"
+EMAIL_INVITE_SUBMESSAGE = "You can click the link above to have it auto fill in the code for you or you could follow this link here:|to take you to my site to fill in the code."
+EMAIL_RESET_HEADER = "Reset Password"
+EMAIL_RESET_TITLE = "LOOK WHO FORGOT THEIR PASSWORD"
+EMAIL_RESET_MESSAGE = "So, you forgot your password huh?  That sucks...  Don't worry, I got you covered.  Here is your new password, it may be freaking long but all you have to do is copy and login to change your password.  Super-Long-New-Password:"
+EMAIL_RESET_BUTTON = "Login"
+EMAIL_RESET_SUBTITLE = "What do I do?"
+EMAIL_RESET_SUBMESSAGE = "You can click the link above to go to my site to login.  Once logged in, click on your image or user icon on top right and change your password."
+EMAIL_NEWUSER_HEADER = "New User"
+EMAIL_NEWUSER_TITLE = "LOOK WHO JUST JOINED THE COOL CLUB"
+EMAIL_NEWUSER_MESSAGE = "Welcome, to my website.  I have many things here... many, many, many shiny things.  Have a look around :)"
+EMAIL_NEWUSER_BUTTON = "Login"
+EMAIL_NEWUSER_SUBTITLE = "What do I do?"
+EMAIL_NEWUSER_SUBMESSAGE = "Now that you have signed up, you can basically do whatever you like.  Enjoy"

+ 57 - 4
lang/en.ini

@@ -177,8 +177,8 @@ PLEX_TOKEN = "Plex Token"
 RECENT_MOVIES = "Recent Movies"
 RECENT_TV = "Recent TV"
 RECENT_MUSIC = "Recent Music"
-PLAYING_NOW = "Playing Now"
-PLAYING_NOW_ON_PLEX = "Playing Now on PLEX"
+PLAYING_NOW = "Now Playing"
+PLAYING_NOW_ON_PLEX = "Now Playing on PLEX"
 RECENTLY_ADDED_TO_PLEX = "Recently Added to PLEX"
 MOVIES = "Recent Movies"
 TV_SHOWS = "Recent TV Shows"
@@ -223,7 +223,7 @@ SMTP_HOST_SENDER_EMAIL = "SMTP Sender Email"
 EMBY_URL = "Emby URL"
 EMBY_PORT = "Emby Port"
 EMBY_TOKEN = "Emby Token"
-PLAYING_NOW_ON_EMBY = "Playing Now on EMBY"
+PLAYING_NOW_ON_EMBY = "Now Playing on EMBY"
 RECENTLY_ADDED_TO_EMBY = "Recently Added to EMBY"
 AUTHTYPE = "Which databases should be used to allow login"
 AUTHBACKEND = "Select backend to use"
@@ -251,4 +251,57 @@ GIT_BRANCH = "Github branch to use when force installing (Leave this alone unles
 GIT_CHECK = "Check for new 'master' releases"
 GIT_FORCE = "Force Install Branch"
 GIT_FORCE_CONFIRM = "Are you sure you want to install this branch? Going from a newer version to an older verison is not recommended or supported."
-SPEED_TEST = "Speed Test"
+SPEED_TEST = "Speed Test"
+NOTICE_COLOR = "Notice Color"
+NOTICE_TITLE = "Notice Title"
+NOTICE_MESSAGE = "Notice Message"
+SHOW_NAMES = "Show Names"
+NOTICE_LAYOUT = "Notice Layout"
+RECENT_ITEMS_LIMIT = "Recent Items Limit"
+ALLOW_SEARCH = "Allow Search"
+CHECK_INVITE = "Enter Invite Code to Procced"
+CODE = "Invite Code"
+INVITE_CODE = "Invite Code"
+DATE_SENT = "Date Sent"
+DATE_USED = "Date Used"
+VALID = "Valid"
+SUBMIT_CODE = "Submit Code"
+IFRAME_CAN_BE_FRAMED = "iFrame Can Be Framed"
+IFRAME_CANNOT_BE_FRAMED = "iFrame Cannot Be Framed"
+CODE_SUCCESS = "Invite Code Has Been Validated"
+CODE_ERROR = "Invite Code is incorrect or not valid"
+HAVE_ACCOUNT = "Do You Have A PLEX Account Already?"
+USERNAME_EMAIL = "Username or E-Mail"
+INVITE_SUCCESS = "You have now been invited, please check your email to accept"
+INVITE_ERROR = "Invite Code not valid, contact admin"
+CREATE_PLEX = "Create PLEX Account"
+JOIN = "Join"
+SIGN_UP = "Sign-up"
+JOIN_SUCCESS = "You have successfully signed up for PLEX, please click join now"
+JOIN_ERROR = "An error occured signing up for PLEX - Username or email might be in use - Try again"
+SEND_INVITE = "Create/Mail Invite"
+USED_BY = "Used By"
+ACCOUNT_MADE = "PLEX Account is now created, Click Join now"
+USERNAME_NAME = "Username or Name"
+ACCOUNT_SUBMITTED = "PLEX Invite Sent|1. Check Email and Accept Invite|2. Close This Modal with Small 'x' on Top Right|3. Sign in"
+PLEX_TAB_NAME = "PLEX Tab Name [only use this if your PLEX URL above is a sub-domain - i.e. https://plex.domain.com]"
+IPINFO_TOKEN = "Get Token from https://ipinfo.io/account/registration as Default will expire"
+GET_PLEX_TOKEN = "Get PLEX Token"
+EMAIL_INVITE_HEADER = "Join My|Server"
+EMAIL_INVITE_TITLE = "LOOK WHO JUST GOT AN INVITE"
+EMAIL_INVITE_MESSAGE = "Here is an invite to join my|server.  The code to join is:"
+EMAIL_INVITE_BUTTON = "JOIN MY|SERVER"
+EMAIL_INVITE_SUBTITLE = "What do I do?"
+EMAIL_INVITE_SUBMESSAGE = "You can click the link above to have it auto fill in the code for you or you could follow this link here:|to take you to my site to fill in the code."
+EMAIL_RESET_HEADER = "Reset Password"
+EMAIL_RESET_TITLE = "LOOK WHO FORGOT THEIR PASSWORD"
+EMAIL_RESET_MESSAGE = "So, you forgot your password huh?  That sucks...  Don't worry, I got you covered.  Here is your new password, it may be freaking long but all you have to do is copy and login to change your password.  Super-Long-New-Password:"
+EMAIL_RESET_BUTTON = "Login"
+EMAIL_RESET_SUBTITLE = "What do I do?"
+EMAIL_RESET_SUBMESSAGE = "You can click the link above to go to my site to login.  Once logged in, click on your image or user icon on top right and change your password."
+EMAIL_NEWUSER_HEADER = "New User"
+EMAIL_NEWUSER_TITLE = "LOOK WHO JUST JOINED THE COOL CLUB"
+EMAIL_NEWUSER_MESSAGE = "Welcome, to my website.  I have many things here... many, many, many shiny things.  Have a look around :)"
+EMAIL_NEWUSER_BUTTON = "Login"
+EMAIL_NEWUSER_SUBTITLE = "What do I do?"
+EMAIL_NEWUSER_SUBMESSAGE = "Now that you have signed up, you can basically do whatever you like.  Enjoy"

+ 57 - 4
lang/es.ini

@@ -177,8 +177,8 @@ PLEX_TOKEN = "Plex Token"
 RECENT_MOVIES = "Recent Movies"
 RECENT_TV = "Recent TV"
 RECENT_MUSIC = "Recent Music"
-PLAYING_NOW = "Playing Now"
-PLAYING_NOW_ON_PLEX = "Playing Now on PLEX"
+PLAYING_NOW = "Now Playing"
+PLAYING_NOW_ON_PLEX = "Now Playing on PLEX"
 RECENTLY_ADDED_TO_PLEX = "Recently Added to PLEX"
 MOVIES = "Movies"
 TV_SHOWS = "TV Shows"
@@ -223,7 +223,7 @@ SMTP_HOST_SENDER_EMAIL = "SMTP Sender Email"
 EMBY_URL = "Emby URL"
 EMBY_PORT = "Emby Port"
 EMBY_TOKEN = "Emby Token"
-PLAYING_NOW_ON_EMBY = "Playing Now on EMBY"
+PLAYING_NOW_ON_EMBY = "Now Playing on EMBY"
 RECENTLY_ADDED_TO_EMBY = "Recently Added to EMBY"
 AUTHTYPE = "Which databases should be used to allow login"
 AUTHBACKEND = "Select backend to use"
@@ -251,4 +251,57 @@ GIT_BRANCH = "Github branch to use when force installing (Leave this alone unles
 GIT_CHECK = "Check for new 'master' releases"
 GIT_FORCE = "Force Install Branch"
 GIT_FORCE_CONFIRM = "Are you sure you want to install this branch? Going from a newer version to an older verison is not recommended or supported."
-SPEED_TEST = "Speed Test"
+SPEED_TEST = "Speed Test"
+NOTICE_COLOR = "Notice Color"
+NOTICE_TITLE = "Notice Title"
+NOTICE_MESSAGE = "Notice Message"
+SHOW_NAMES = "Show Names"
+NOTICE_LAYOUT = "Notice Layout"
+RECENT_ITEMS_LIMIT = "Recent Items Limit"
+ALLOW_SEARCH = "Allow Search"
+CHECK_INVITE = "Enter Invite Code to Procced"
+CODE = "Invite Code"
+INVITE_CODE = "Invite Code"
+DATE_SENT = "Date Sent"
+DATE_USED = "Date Used"
+VALID = "Valid"
+SUBMIT_CODE = "Submit Code"
+IFRAME_CAN_BE_FRAMED = "iFrame Can Be Framed"
+IFRAME_CANNOT_BE_FRAMED = "iFrame Cannot Be Framed"
+CODE_SUCCESS = "Invite Code Has Been Validated"
+CODE_ERROR = "Invite Code is incorrect or not valid"
+HAVE_ACCOUNT = "Do You Have A PLEX Account Already?"
+USERNAME_EMAIL = "Username or E-Mail"
+INVITE_SUCCESS = "You have now been invited, please check your email to accept"
+INVITE_ERROR = "Invite Code not valid, contact admin"
+CREATE_PLEX = "Create PLEX Account"
+JOIN = "Join"
+SIGN_UP = "Sign-up"
+JOIN_SUCCESS = "You have successfully signed up for PLEX, please click join now"
+JOIN_ERROR = "An error occured signing up for PLEX - Username or email might be in use - Try again"
+SEND_INVITE = "Create/Mail Invite"
+USED_BY = "Used By"
+ACCOUNT_MADE = "PLEX Account is now created, Click Join now"
+USERNAME_NAME = "Username or Name"
+ACCOUNT_SUBMITTED = "PLEX Invite Sent|1. Check Email and Accept Invite|2. Close This Modal with Small 'x' on Top Right|3. Sign in"
+PLEX_TAB_NAME = "PLEX Tab Name [only use this if your PLEX URL above is a sub-domain - i.e. https://plex.domain.com]"
+IPINFO_TOKEN = "Get Token from https://ipinfo.io/account/registration as Default will expire"
+GET_PLEX_TOKEN = "Get PLEX Token"
+EMAIL_INVITE_HEADER = "Join My|Server"
+EMAIL_INVITE_TITLE = "LOOK WHO JUST GOT AN INVITE"
+EMAIL_INVITE_MESSAGE = "Here is an invite to join my|server.  The code to join is:"
+EMAIL_INVITE_BUTTON = "JOIN MY|SERVER"
+EMAIL_INVITE_SUBTITLE = "What do I do?"
+EMAIL_INVITE_SUBMESSAGE = "You can click the link above to have it auto fill in the code for you or you could follow this link here:|to take you to my site to fill in the code."
+EMAIL_RESET_HEADER = "Reset Password"
+EMAIL_RESET_TITLE = "LOOK WHO FORGOT THEIR PASSWORD"
+EMAIL_RESET_MESSAGE = "So, you forgot your password huh?  That sucks...  Don't worry, I got you covered.  Here is your new password, it may be freaking long but all you have to do is copy and login to change your password.  Super-Long-New-Password:"
+EMAIL_RESET_BUTTON = "Login"
+EMAIL_RESET_SUBTITLE = "What do I do?"
+EMAIL_RESET_SUBMESSAGE = "You can click the link above to go to my site to login.  Once logged in, click on your image or user icon on top right and change your password."
+EMAIL_NEWUSER_HEADER = "New User"
+EMAIL_NEWUSER_TITLE = "LOOK WHO JUST JOINED THE COOL CLUB"
+EMAIL_NEWUSER_MESSAGE = "Welcome, to my website.  I have many things here... many, many, many shiny things.  Have a look around :)"
+EMAIL_NEWUSER_BUTTON = "Login"
+EMAIL_NEWUSER_SUBTITLE = "What do I do?"
+EMAIL_NEWUSER_SUBMESSAGE = "Now that you have signed up, you can basically do whatever you like.  Enjoy"

+ 57 - 4
lang/fr.ini

@@ -177,8 +177,8 @@ PLEX_TOKEN = "Plex Token"
 RECENT_MOVIES = "Recent Movies"
 RECENT_TV = "Recent TV"
 RECENT_MUSIC = "Recent Music"
-PLAYING_NOW = "Playing Now"
-PLAYING_NOW_ON_PLEX = "Playing Now on PLEX"
+PLAYING_NOW = "Now Playing"
+PLAYING_NOW_ON_PLEX = "Now Playing on PLEX"
 RECENTLY_ADDED_TO_PLEX = "Recently Added to PLEX"
 MOVIES = "Movies"
 TV_SHOWS = "TV Shows"
@@ -223,7 +223,7 @@ SMTP_HOST_SENDER_EMAIL = "SMTP Sender Email"
 EMBY_URL = "Emby URL"
 EMBY_PORT = "Emby Port"
 EMBY_TOKEN = "Emby Token"
-PLAYING_NOW_ON_EMBY = "Playing Now on EMBY"
+PLAYING_NOW_ON_EMBY = "Now Playing on EMBY"
 RECENTLY_ADDED_TO_EMBY = "Recently Added to EMBY"
 AUTHTYPE = "Which databases should be used to allow login"
 AUTHBACKEND = "Select backend to use"
@@ -251,4 +251,57 @@ GIT_BRANCH = "Github branch to use when force installing (Leave this alone unles
 GIT_CHECK = "Check for new 'master' releases"
 GIT_FORCE = "Force Install Branch"
 GIT_FORCE_CONFIRM = "Are you sure you want to install this branch? Going from a newer version to an older verison is not recommended or supported."
-SPEED_TEST = "Speed Test"
+SPEED_TEST = "Speed Test"
+NOTICE_COLOR = "Notice Color"
+NOTICE_TITLE = "Notice Title"
+NOTICE_MESSAGE = "Notice Message"
+SHOW_NAMES = "Show Names"
+NOTICE_LAYOUT = "Notice Layout"
+RECENT_ITEMS_LIMIT = "Recent Items Limit"
+ALLOW_SEARCH = "Allow Search"
+CHECK_INVITE = "Enter Invite Code to Procced"
+CODE = "Invite Code"
+INVITE_CODE = "Invite Code"
+DATE_SENT = "Date Sent"
+DATE_USED = "Date Used"
+VALID = "Valid"
+SUBMIT_CODE = "Submit Code"
+IFRAME_CAN_BE_FRAMED = "iFrame Can Be Framed"
+IFRAME_CANNOT_BE_FRAMED = "iFrame Cannot Be Framed"
+CODE_SUCCESS = "Invite Code Has Been Validated"
+CODE_ERROR = "Invite Code is incorrect or not valid"
+HAVE_ACCOUNT = "Do You Have A PLEX Account Already?"
+USERNAME_EMAIL = "Username or E-Mail"
+INVITE_SUCCESS = "You have now been invited, please check your email to accept"
+INVITE_ERROR = "Invite Code not valid, contact admin"
+CREATE_PLEX = "Create PLEX Account"
+JOIN = "Join"
+SIGN_UP = "Sign-up"
+JOIN_SUCCESS = "You have successfully signed up for PLEX, please click join now"
+JOIN_ERROR = "An error occured signing up for PLEX - Username or email might be in use - Try again"
+SEND_INVITE = "Create/Mail Invite"
+USED_BY = "Used By"
+ACCOUNT_MADE = "PLEX Account is now created, Click Join now"
+USERNAME_NAME = "Username or Name"
+ACCOUNT_SUBMITTED = "PLEX Invite Sent|1. Check Email and Accept Invite|2. Close This Modal with Small 'x' on Top Right|3. Sign in"
+PLEX_TAB_NAME = "PLEX Tab Name [only use this if your PLEX URL above is a sub-domain - i.e. https://plex.domain.com]"
+IPINFO_TOKEN = "Get Token from https://ipinfo.io/account/registration as Default will expire"
+GET_PLEX_TOKEN = "Get PLEX Token"
+EMAIL_INVITE_HEADER = "Join My|Server"
+EMAIL_INVITE_TITLE = "LOOK WHO JUST GOT AN INVITE"
+EMAIL_INVITE_MESSAGE = "Here is an invite to join my|server.  The code to join is:"
+EMAIL_INVITE_BUTTON = "JOIN MY|SERVER"
+EMAIL_INVITE_SUBTITLE = "What do I do?"
+EMAIL_INVITE_SUBMESSAGE = "You can click the link above to have it auto fill in the code for you or you could follow this link here:|to take you to my site to fill in the code."
+EMAIL_RESET_HEADER = "Reset Password"
+EMAIL_RESET_TITLE = "LOOK WHO FORGOT THEIR PASSWORD"
+EMAIL_RESET_MESSAGE = "So, you forgot your password huh?  That sucks...  Don't worry, I got you covered.  Here is your new password, it may be freaking long but all you have to do is copy and login to change your password.  Super-Long-New-Password:"
+EMAIL_RESET_BUTTON = "Login"
+EMAIL_RESET_SUBTITLE = "What do I do?"
+EMAIL_RESET_SUBMESSAGE = "You can click the link above to go to my site to login.  Once logged in, click on your image or user icon on top right and change your password."
+EMAIL_NEWUSER_HEADER = "New User"
+EMAIL_NEWUSER_TITLE = "LOOK WHO JUST JOINED THE COOL CLUB"
+EMAIL_NEWUSER_MESSAGE = "Welcome, to my website.  I have many things here... many, many, many shiny things.  Have a look around :)"
+EMAIL_NEWUSER_BUTTON = "Login"
+EMAIL_NEWUSER_SUBTITLE = "What do I do?"
+EMAIL_NEWUSER_SUBMESSAGE = "Now that you have signed up, you can basically do whatever you like.  Enjoy"

+ 57 - 4
lang/it.ini

@@ -177,8 +177,8 @@ PLEX_TOKEN = "Plex Token"
 RECENT_MOVIES = "Recent Movies"
 RECENT_TV = "Recent TV"
 RECENT_MUSIC = "Recent Music"
-PLAYING_NOW = "Playing Now"
-PLAYING_NOW_ON_PLEX = "Playing Now on PLEX"
+PLAYING_NOW = "Now Playing"
+PLAYING_NOW_ON_PLEX = "Now Playing on PLEX"
 RECENTLY_ADDED_TO_PLEX = "Recently Added to PLEX"
 MOVIES = "Movies"
 TV_SHOWS = "TV Shows"
@@ -223,7 +223,7 @@ SMTP_HOST_SENDER_EMAIL = "SMTP Sender Email"
 EMBY_URL = "Emby URL"
 EMBY_PORT = "Emby Port"
 EMBY_TOKEN = "Emby Token"
-PLAYING_NOW_ON_EMBY = "Playing Now on EMBY"
+PLAYING_NOW_ON_EMBY = "Now Playing on EMBY"
 RECENTLY_ADDED_TO_EMBY = "Recently Added to EMBY"
 AUTHTYPE = "Which databases should be used to allow login"
 AUTHBACKEND = "Select backend to use"
@@ -251,4 +251,57 @@ GIT_BRANCH = "Github branch to use when force installing (Leave this alone unles
 GIT_CHECK = "Check for new 'master' releases"
 GIT_FORCE = "Force Install Branch"
 GIT_FORCE_CONFIRM = "Are you sure you want to install this branch? Going from a newer version to an older verison is not recommended or supported."
-SPEED_TEST = "Speed Test"
+SPEED_TEST = "Speed Test"
+NOTICE_COLOR = "Notice Color"
+NOTICE_TITLE = "Notice Title"
+NOTICE_MESSAGE = "Notice Message"
+SHOW_NAMES = "Show Names"
+NOTICE_LAYOUT = "Notice Layout"
+RECENT_ITEMS_LIMIT = "Recent Items Limit"
+ALLOW_SEARCH = "Allow Search"
+CHECK_INVITE = "Enter Invite Code to Procced"
+CODE = "Invite Code"
+INVITE_CODE = "Invite Code"
+DATE_SENT = "Date Sent"
+DATE_USED = "Date Used"
+VALID = "Valid"
+SUBMIT_CODE = "Submit Code"
+IFRAME_CAN_BE_FRAMED = "iFrame Can Be Framed"
+IFRAME_CANNOT_BE_FRAMED = "iFrame Cannot Be Framed"
+CODE_SUCCESS = "Invite Code Has Been Validated"
+CODE_ERROR = "Invite Code is incorrect or not valid"
+HAVE_ACCOUNT = "Do You Have A PLEX Account Already?"
+USERNAME_EMAIL = "Username or E-Mail"
+INVITE_SUCCESS = "You have now been invited, please check your email to accept"
+INVITE_ERROR = "Invite Code not valid, contact admin"
+CREATE_PLEX = "Create PLEX Account"
+JOIN = "Join"
+SIGN_UP = "Sign-up"
+JOIN_SUCCESS = "You have successfully signed up for PLEX, please click join now"
+JOIN_ERROR = "An error occured signing up for PLEX - Username or email might be in use - Try again"
+SEND_INVITE = "Create/Mail Invite"
+USED_BY = "Used By"
+ACCOUNT_MADE = "PLEX Account is now created, Click Join now"
+USERNAME_NAME = "Username or Name"
+ACCOUNT_SUBMITTED = "PLEX Invite Sent|1. Check Email and Accept Invite|2. Close This Modal with Small 'x' on Top Right|3. Sign in"
+PLEX_TAB_NAME = "PLEX Tab Name [only use this if your PLEX URL above is a sub-domain - i.e. https://plex.domain.com]"
+IPINFO_TOKEN = "Get Token from https://ipinfo.io/account/registration as Default will expire"
+GET_PLEX_TOKEN = "Get PLEX Token"
+EMAIL_INVITE_HEADER = "Join My|Server"
+EMAIL_INVITE_TITLE = "LOOK WHO JUST GOT AN INVITE"
+EMAIL_INVITE_MESSAGE = "Here is an invite to join my|server.  The code to join is:"
+EMAIL_INVITE_BUTTON = "JOIN MY|SERVER"
+EMAIL_INVITE_SUBTITLE = "What do I do?"
+EMAIL_INVITE_SUBMESSAGE = "You can click the link above to have it auto fill in the code for you or you could follow this link here:|to take you to my site to fill in the code."
+EMAIL_RESET_HEADER = "Reset Password"
+EMAIL_RESET_TITLE = "LOOK WHO FORGOT THEIR PASSWORD"
+EMAIL_RESET_MESSAGE = "So, you forgot your password huh?  That sucks...  Don't worry, I got you covered.  Here is your new password, it may be freaking long but all you have to do is copy and login to change your password.  Super-Long-New-Password:"
+EMAIL_RESET_BUTTON = "Login"
+EMAIL_RESET_SUBTITLE = "What do I do?"
+EMAIL_RESET_SUBMESSAGE = "You can click the link above to go to my site to login.  Once logged in, click on your image or user icon on top right and change your password."
+EMAIL_NEWUSER_HEADER = "New User"
+EMAIL_NEWUSER_TITLE = "LOOK WHO JUST JOINED THE COOL CLUB"
+EMAIL_NEWUSER_MESSAGE = "Welcome, to my website.  I have many things here... many, many, many shiny things.  Have a look around :)"
+EMAIL_NEWUSER_BUTTON = "Login"
+EMAIL_NEWUSER_SUBTITLE = "What do I do?"
+EMAIL_NEWUSER_SUBMESSAGE = "Now that you have signed up, you can basically do whatever you like.  Enjoy"

+ 57 - 4
lang/nl.ini

@@ -177,8 +177,8 @@ PLEX_TOKEN = "Plex Token"
 RECENT_MOVIES = "Recent Movies"
 RECENT_TV = "Recent TV"
 RECENT_MUSIC = "Recent Music"
-PLAYING_NOW = "Playing Now"
-PLAYING_NOW_ON_PLEX = "Playing Now on PLEX"
+PLAYING_NOW = "Now Playing"
+PLAYING_NOW_ON_PLEX = "Now Playing on PLEX"
 RECENTLY_ADDED_TO_PLEX = "Recently Added to PLEX"
 MOVIES = "Movies"
 TV_SHOWS = "TV Shows"
@@ -223,7 +223,7 @@ SMTP_HOST_SENDER_EMAIL = "SMTP Sender Email"
 EMBY_URL = "Emby URL"
 EMBY_PORT = "Emby Port"
 EMBY_TOKEN = "Emby Token"
-PLAYING_NOW_ON_EMBY = "Playing Now on EMBY"
+PLAYING_NOW_ON_EMBY = "Now Playing on EMBY"
 RECENTLY_ADDED_TO_EMBY = "Recently Added to EMBY"
 AUTHTYPE = "Which databases should be used to allow login"
 AUTHBACKEND = "Select backend to use"
@@ -251,4 +251,57 @@ GIT_BRANCH = "Github branch to use when force installing (Leave this alone unles
 GIT_CHECK = "Check for new 'master' releases"
 GIT_FORCE = "Force Install Branch"
 GIT_FORCE_CONFIRM = "Are you sure you want to install this branch? Going from a newer version to an older verison is not recommended or supported."
-SPEED_TEST = "Speed Test"
+SPEED_TEST = "Speed Test"
+NOTICE_COLOR = "Notice Color"
+NOTICE_TITLE = "Notice Title"
+NOTICE_MESSAGE = "Notice Message"
+SHOW_NAMES = "Show Names"
+NOTICE_LAYOUT = "Notice Layout"
+RECENT_ITEMS_LIMIT = "Recent Items Limit"
+ALLOW_SEARCH = "Allow Search"
+CHECK_INVITE = "Enter Invite Code to Procced"
+CODE = "Invite Code"
+INVITE_CODE = "Invite Code"
+DATE_SENT = "Date Sent"
+DATE_USED = "Date Used"
+VALID = "Valid"
+SUBMIT_CODE = "Submit Code"
+IFRAME_CAN_BE_FRAMED = "iFrame Can Be Framed"
+IFRAME_CANNOT_BE_FRAMED = "iFrame Cannot Be Framed"
+CODE_SUCCESS = "Invite Code Has Been Validated"
+CODE_ERROR = "Invite Code is incorrect or not valid"
+HAVE_ACCOUNT = "Do You Have A PLEX Account Already?"
+USERNAME_EMAIL = "Username or E-Mail"
+INVITE_SUCCESS = "You have now been invited, please check your email to accept"
+INVITE_ERROR = "Invite Code not valid, contact admin"
+CREATE_PLEX = "Create PLEX Account"
+JOIN = "Join"
+SIGN_UP = "Sign-up"
+JOIN_SUCCESS = "You have successfully signed up for PLEX, please click join now"
+JOIN_ERROR = "An error occured signing up for PLEX - Username or email might be in use - Try again"
+SEND_INVITE = "Create/Mail Invite"
+USED_BY = "Used By"
+ACCOUNT_MADE = "PLEX Account is now created, Click Join now"
+USERNAME_NAME = "Username or Name"
+ACCOUNT_SUBMITTED = "PLEX Invite Sent|1. Check Email and Accept Invite|2. Close This Modal with Small 'x' on Top Right|3. Sign in"
+PLEX_TAB_NAME = "PLEX Tab Name [only use this if your PLEX URL above is a sub-domain - i.e. https://plex.domain.com]"
+IPINFO_TOKEN = "Get Token from https://ipinfo.io/account/registration as Default will expire"
+GET_PLEX_TOKEN = "Get PLEX Token"
+EMAIL_INVITE_HEADER = "Join My|Server"
+EMAIL_INVITE_TITLE = "LOOK WHO JUST GOT AN INVITE"
+EMAIL_INVITE_MESSAGE = "Here is an invite to join my|server.  The code to join is:"
+EMAIL_INVITE_BUTTON = "JOIN MY|SERVER"
+EMAIL_INVITE_SUBTITLE = "What do I do?"
+EMAIL_INVITE_SUBMESSAGE = "You can click the link above to have it auto fill in the code for you or you could follow this link here:|to take you to my site to fill in the code."
+EMAIL_RESET_HEADER = "Reset Password"
+EMAIL_RESET_TITLE = "LOOK WHO FORGOT THEIR PASSWORD"
+EMAIL_RESET_MESSAGE = "So, you forgot your password huh?  That sucks...  Don't worry, I got you covered.  Here is your new password, it may be freaking long but all you have to do is copy and login to change your password.  Super-Long-New-Password:"
+EMAIL_RESET_BUTTON = "Login"
+EMAIL_RESET_SUBTITLE = "What do I do?"
+EMAIL_RESET_SUBMESSAGE = "You can click the link above to go to my site to login.  Once logged in, click on your image or user icon on top right and change your password."
+EMAIL_NEWUSER_HEADER = "New User"
+EMAIL_NEWUSER_TITLE = "LOOK WHO JUST JOINED THE COOL CLUB"
+EMAIL_NEWUSER_MESSAGE = "Welcome, to my website.  I have many things here... many, many, many shiny things.  Have a look around :)"
+EMAIL_NEWUSER_BUTTON = "Login"
+EMAIL_NEWUSER_SUBTITLE = "What do I do?"
+EMAIL_NEWUSER_SUBMESSAGE = "Now that you have signed up, you can basically do whatever you like.  Enjoy"

+ 57 - 4
lang/pl.ini

@@ -177,8 +177,8 @@ PLEX_TOKEN = "Plex Token"
 RECENT_MOVIES = "Recent Movies"
 RECENT_TV = "Recent TV"
 RECENT_MUSIC = "Recent Music"
-PLAYING_NOW = "Playing Now"
-PLAYING_NOW_ON_PLEX = "Playing Now on PLEX"
+PLAYING_NOW = "Now Playing"
+PLAYING_NOW_ON_PLEX = "Now Playing on PLEX"
 RECENTLY_ADDED_TO_PLEX = "Recently Added to PLEX"
 MOVIES = "Movies"
 TV_SHOWS = "TV Shows"
@@ -223,7 +223,7 @@ SMTP_HOST_SENDER_EMAIL = "SMTP Sender Email"
 EMBY_URL = "Emby URL"
 EMBY_PORT = "Emby Port"
 EMBY_TOKEN = "Emby Token"
-PLAYING_NOW_ON_EMBY = "Playing Now on EMBY"
+PLAYING_NOW_ON_EMBY = "Now Playing on EMBY"
 RECENTLY_ADDED_TO_EMBY = "Recently Added to EMBY"
 AUTHTYPE = "Which databases should be used to allow login"
 AUTHBACKEND = "Select backend to use"
@@ -251,4 +251,57 @@ GIT_BRANCH = "Github branch to use when force installing (Leave this alone unles
 GIT_CHECK = "Check for new 'master' releases"
 GIT_FORCE = "Force Install Branch"
 GIT_FORCE_CONFIRM = "Are you sure you want to install this branch? Going from a newer version to an older verison is not recommended or supported."
-SPEED_TEST = "Speed Test"
+SPEED_TEST = "Speed Test"
+NOTICE_COLOR = "Notice Color"
+NOTICE_TITLE = "Notice Title"
+NOTICE_MESSAGE = "Notice Message"
+SHOW_NAMES = "Show Names"
+NOTICE_LAYOUT = "Notice Layout"
+RECENT_ITEMS_LIMIT = "Recent Items Limit"
+ALLOW_SEARCH = "Allow Search"
+CHECK_INVITE = "Enter Invite Code to Procced"
+CODE = "Invite Code"
+INVITE_CODE = "Invite Code"
+DATE_SENT = "Date Sent"
+DATE_USED = "Date Used"
+VALID = "Valid"
+SUBMIT_CODE = "Submit Code"
+IFRAME_CAN_BE_FRAMED = "iFrame Can Be Framed"
+IFRAME_CANNOT_BE_FRAMED = "iFrame Cannot Be Framed"
+CODE_SUCCESS = "Invite Code Has Been Validated"
+CODE_ERROR = "Invite Code is incorrect or not valid"
+HAVE_ACCOUNT = "Do You Have A PLEX Account Already?"
+USERNAME_EMAIL = "Username or E-Mail"
+INVITE_SUCCESS = "You have now been invited, please check your email to accept"
+INVITE_ERROR = "Invite Code not valid, contact admin"
+CREATE_PLEX = "Create PLEX Account"
+JOIN = "Join"
+SIGN_UP = "Sign-up"
+JOIN_SUCCESS = "You have successfully signed up for PLEX, please click join now"
+JOIN_ERROR = "An error occured signing up for PLEX - Username or email might be in use - Try again"
+SEND_INVITE = "Create/Mail Invite"
+USED_BY = "Used By"
+ACCOUNT_MADE = "PLEX Account is now created, Click Join now"
+USERNAME_NAME = "Username or Name"
+ACCOUNT_SUBMITTED = "PLEX Invite Sent|1. Check Email and Accept Invite|2. Close This Modal with Small 'x' on Top Right|3. Sign in"
+PLEX_TAB_NAME = "PLEX Tab Name [only use this if your PLEX URL above is a sub-domain - i.e. https://plex.domain.com]"
+IPINFO_TOKEN = "Get Token from https://ipinfo.io/account/registration as Default will expire"
+GET_PLEX_TOKEN = "Get PLEX Token"
+EMAIL_INVITE_HEADER = "Join My|Server"
+EMAIL_INVITE_TITLE = "LOOK WHO JUST GOT AN INVITE"
+EMAIL_INVITE_MESSAGE = "Here is an invite to join my|server.  The code to join is:"
+EMAIL_INVITE_BUTTON = "JOIN MY|SERVER"
+EMAIL_INVITE_SUBTITLE = "What do I do?"
+EMAIL_INVITE_SUBMESSAGE = "You can click the link above to have it auto fill in the code for you or you could follow this link here:|to take you to my site to fill in the code."
+EMAIL_RESET_HEADER = "Reset Password"
+EMAIL_RESET_TITLE = "LOOK WHO FORGOT THEIR PASSWORD"
+EMAIL_RESET_MESSAGE = "So, you forgot your password huh?  That sucks...  Don't worry, I got you covered.  Here is your new password, it may be freaking long but all you have to do is copy and login to change your password.  Super-Long-New-Password:"
+EMAIL_RESET_BUTTON = "Login"
+EMAIL_RESET_SUBTITLE = "What do I do?"
+EMAIL_RESET_SUBMESSAGE = "You can click the link above to go to my site to login.  Once logged in, click on your image or user icon on top right and change your password."
+EMAIL_NEWUSER_HEADER = "New User"
+EMAIL_NEWUSER_TITLE = "LOOK WHO JUST JOINED THE COOL CLUB"
+EMAIL_NEWUSER_MESSAGE = "Welcome, to my website.  I have many things here... many, many, many shiny things.  Have a look around :)"
+EMAIL_NEWUSER_BUTTON = "Login"
+EMAIL_NEWUSER_SUBTITLE = "What do I do?"
+EMAIL_NEWUSER_SUBMESSAGE = "Now that you have signed up, you can basically do whatever you like.  Enjoy"

Файловите разлики са ограничени, защото са твърде много
+ 624 - 56
settings.php


+ 127 - 43
user.php

@@ -21,7 +21,14 @@
     $notifyExplode = explode("-", NOTIFYEFFECT);
     define('FAIL_LOG', 'loginLog.json');
     @date_default_timezone_set(TIMEZONE);
-	
+    function guestHash($start, $end){
+        $ip   = $_SERVER['REMOTE_ADDR'];
+        $ip    = md5($ip);
+        return substr($ip, $start, $end);
+    }
+
+    define('GUEST_HASH', "guest-".guestHash(0, 5));
+
 	class User
 	{
 		// =======================================================================
@@ -104,7 +111,7 @@
 		// this will tell us whether the client that requested the page is authenticated or not.
 		var $authenticated = false;
 		// the guest user name
-		const GUEST_USER  = "guest user";
+		const GUEST_USER  = GUEST_HASH;
 		// this will contain the user name for the user doing the page request
 		var $username = User::GUEST_USER;
 		// if this is a properly logged in user, this will contain the data directory location for this user
@@ -126,7 +133,7 @@
             $mail->SMTPAuth = SMTPHOSTAUTH;
             $mail->Username = SMTPHOSTUSERNAME;
             $mail->Password = SMTPHOSTPASSWORD;
-            $mail->SMTPSecure = 'tls';
+            $mail->SMTPSecure = SMTPHOSTTYPE;
             $mail->Port = SMTPHOSTPORT;
             $mail->setFrom(SMTPHOSTSENDEREMAIL, SMTPHOSTSENDERNAME);
             $mail->addReplyTo(SMTPHOSTSENDEREMAIL, SMTPHOSTSENDERNAME);
@@ -134,7 +141,13 @@
             $mail->addAddress($email, $username);
             $mail->Subject = $subject;
             $mail->Body    = $body;
-            $mail->send();
+            //$mail->send();
+            if(!$mail->send()) {
+                $this->error('Mailer Error: ' . $mail->ErrorInfo);
+                $this->error = 'Mailer Error: ' . $mail->ErrorInfo;
+            } else {
+                $this->info('E-Mail sent!');
+            }
             
         }
        
@@ -183,6 +196,8 @@
 				// anything else won't change authentication status.
 				elseif($operation == "register") { $this->register($registration_callback); }
 				elseif($operation == "update") { $this->update(); }
+				elseif($operation == "invite") { $this->invite(); }
+				elseif($operation == "deleteinvite") { $this->deleteInvite(); }
 				// we only allow password resetting if we can send notification mails
 				elseif($operation == "reset" && User::use_mail) { $this->reset_password(); }
 			}
@@ -279,23 +294,19 @@
 			if($registered && User::use_mail)
 			{
 				// send email notification
-				$from = User::MAILER_NAME;
-				$replyto = User::MAILER_REPLYTO;
-				$domain_name = User::DOMAIN_NAME;
-				$subject = User::DOMAIN_NAME . " registration";
-				$body = <<<EOT
-	Hi,
-	this is an automated message to let you know that someone signed up at $domain_name with the user name "$username", using this email address as mailing address.
-	Because of the way our user registration works, we have no idea which password was used to register this account (it gets one-way hashed by the browser before it is sent to our user registration system, so that we don't know your password either), so if you registered this account, hopefully you wrote your password down somewhere.
-	However, if you ever forget your password, you can click the "I forgot my password" link in the log-in section for $domain_name and you will be sent an email containing a new, ridiculously long and complicated password that you can use to log in. You can change your password after logging in, but that's up to you. No one's going to guess it, or brute force it, but if other people can read your emails, it's generally a good idea to change passwords.
-	If you were not the one to register this account, you can either contact us the normal way or —much easier— you can ask the system to reset the password for the account, after which you can simply log in with the temporary password and delete the account. That'll teach whoever pretended to be you not to mess with you!
-	Of course, if you did register it yourself, welcome to $domain_name!
-	- the $domain_name team
-EOT;
-				$headers = "From: $from\r\n";
-				$headers .= "Reply-To: $replyto\r\n";
-				$headers .= "X-Mailer: PHP/" . phpversion();
-				//mail($email, $subject, $body, $headers);
+				$subject = "Welcome to ".DOMAIN;
+				$language = new setLanguage;
+				$domain = getServerPath();
+				$body = orgEmail(
+					$header = $language->translate('EMAIL_NEWUSER_HEADER'),
+					$title = $language->translate('EMAIL_NEWUSER_TITLE'), 
+					$user = $username, 
+					$mainMessage =$language->translate('EMAIL_NEWUSER_MESSAGE'),
+					$button = $language->translate('EMAIL_NEWUSER_BUTTON'),
+					$buttonURL = $domain, 
+					$subTitle = $language->translate('EMAIL_NEWUSER_SUBTITLE'), 
+					$subMessage = $language->translate('EMAIL_NEWUSER_SUBMESSAGE')
+					);
                 $this->startEmail($email, $username, $subject, $body);
 			}
 			return $registered;
@@ -320,6 +331,25 @@ EOT;
 			// step 2: if validation passed, update the user's information
 			return $this->update_user($username, $email, $sha1, $role);
 		}
+		/**
+		 * Called when the requested POST operation is "invite"
+		 */
+		function invite()
+		{
+			// get relevant values
+            @$username = trim($_POST["username"]);
+			@$email = trim($_POST["email"]);
+			@$server = trim($_POST["server"]);
+			// step 1: someone could have bypassed the javascript validation, so validate again.
+			if($email !="" && preg_match(User::emailregexp, $email)==0) {
+				$this->info("<strong>invite error:</strong> email address did not pass validation");
+				writeLog("error", "$email didn't pass validation");
+				return false; 
+			}
+			// step 2: if validation passed, send the user's information for invite
+			return $this->invite_user($username, $email, $server);
+			writeLog("success", "passing invite info for $email");
+		}
 		/**
 		 * Reset a user's password
 		 */
@@ -332,7 +362,7 @@ EOT;
 				$this->info("email address did not pass validation");
 				return false; }
 			// step 2: if validation passed, see if there is a matching user, and reset the password if there is
-			$newpassword = $this->random_ascii_string(64);
+			$newpassword = $this->random_ascii_string(20);
 			$sha1 = sha1($newpassword);
 			$query = "SELECT username, token FROM users WHERE email = '$email'";
 			$username = "";
@@ -343,26 +373,23 @@ EOT;
 			// step 2b: if there was a user to reset a password for, reset it.
 			$dbpassword = $this->token_hash_password($username, $sha1, $token);
 			$update = "UPDATE users SET password = '$dbpassword' WHERE email= '$email'";
+   			writeLog("success", "$username has reset their password");
 			$this->database->exec($update);
-            $this->info("Email has been sent with new password");
+            //$this->info("Email has been sent with new password");
 			// step 3: notify the user of the new password
-			$from = User::MAILER_NAME;
-			$replyto = User::MAILER_REPLYTO;
-			$domain_name = User::DOMAIN_NAME;
-			$subject = User::DOMAIN_NAME . " password reset request";
-			$body = <<<EOT
-	Hi,
-	this is an automated message to let you know that someone requested a password reset for the $domain_name user account with user name "$username", which is linked to this email address.
-	We've reset the password to the following 64 character string, so make sure to copy/paste it without any leading or trailing spaces:
-	$newpassword
-	If you didn't even know this account existed, now is the time to log in and delete it. How dare people use your email address to register accounts! Of course, if you did register it yourself, but you didn't request the reset, some jerk is apparently reset-spamming. We hope he gets run over by a steam shovel driven by rabid ocelots or something.
-	Then again, it's far more likely that you did register this account, and you simply forgot the password so you asked for the reset yourself, in which case: here's your new password, and thank you for your patronage at $domain_name!
-	- the $domain_name team
-EOT;
-			$headers = "From: $from\r\n";
-			$headers .= "Reply-To: $replyto\r\n";
-			$headers .= "X-Mailer: PHP/" . phpversion();
-			//mail($email, $subject, $body, $headers);
+			$subject = DOMAIN . " Password Reset";
+			$language = new setLanguage;
+			$domain = getServerPath();
+			$body = orgEmail(
+					$header = $language->translate('EMAIL_RESET_HEADER'),
+					$title = $language->translate('EMAIL_RESET_TITLE'), 
+					$user = $username, 
+					$mainMessage =$language->translate('EMAIL_RESET_MESSAGE')."<br/>".$newpassword,
+					$button = $language->translate('EMAIL_RESET_BUTTON'),
+					$buttonURL = $domain, 
+					$subTitle = $language->translate('EMAIL_RESET_SUBTITLE'), 
+					$subMessage = $language->translate('EMAIL_RESET_SUBMESSAGE')
+					);
             $this->startEmail($email, $username, $subject, $body);
 		}
 	// ------------------
@@ -549,6 +576,7 @@ EOT;
 			$query = "SELECT * FROM users WHERE username = '$username'";
 			foreach($this->database->query($query) as $data) {
 				$this->info("created user account for $username");
+    			writeLog("success", "$username has just registered");
 				$this->update_user_token($username, $sha1, false);
 				// make the user's data directory
 				$dir = USER_HOME . $username;
@@ -639,6 +667,7 @@ EOT;
 					file_put_contents(FAIL_LOG, $buildLog($username, "good_auth"));
 					chmod(FAIL_LOG, 0660);
 					setcookie("cookiePassword", COOKIEPASSWORD, time() + (86400 * 7), "/", DOMAIN);
+     				writeLog("success", "$username has logged in");
 					return true; 
 				} else if (AUTHBACKENDCREATE !== 'false' && $surface) {
 					// Create User
@@ -656,6 +685,7 @@ EOT;
 			} else if (!$authSuccess) {
 				// authentication failed
 				//$this->info("password mismatch for $username");
+    			writeLog("error", "$username tried to sign-in with the wrong password");
 				file_put_contents(FAIL_LOG, $buildLog($username, "bad_auth"));
 				chmod(FAIL_LOG, 0660);
 				if(User::unsafe_reporting) { $this->error = "incorrect password for $username."; $this->error("incorrect password for $username."); }
@@ -686,8 +716,60 @@ EOT;
 				$dbpassword = $this->token_hash_password($username, $sha1, $this->get_user_token($username));
 				$update = "UPDATE users SET password = '$dbpassword' WHERE username = '$username'";
 				$this->database->exec($update); }
+   			writeLog("success", "information for $username has been updated");
 			$this->info("updated the information for <strong>$username</strong>");
 		}
+		/**
+		 * Drop a invite from the system
+		 */
+		function deleteInvite()
+		{
+			@$id = trim($_POST["id"]);
+			$delete = "DELETE FROM invites WHERE id = '$id' COLLATE NOCASE";
+			$this->database->exec($delete);
+			$this->info("Plex Invite: <strong>$id</strong> has been deleted out of Organizr");
+    		writeLog("success", "PLEX INVITE: $id has been deleted");
+			return true;
+		}
+		
+		/**
+		 * Invite using a user's information
+		 */
+		function invite_user($username = "none", $email, $server)
+		{
+			//lang shit
+			$language = new setLanguage;
+			$domain = getServerPath();
+			$topImage = $domain."images/organizr-logo-h.png";
+			$uServer = strtoupper($server);
+			$now = date("Y-m-d H:i:s");
+			$inviteCode = randomCode(6);
+			$username = (!empty($username) ? $username : strtoupper($server) . " User");
+			$link = getServerPath()."?inviteCode=".$inviteCode;
+			if($email !="") {
+				$insert = "INSERT INTO invites (username, email, code, valid, date) ";
+				$insert .= "VALUES ('".strtolower($username)."', '$email', '$inviteCode', 'Yes', '$now') ";
+				$this->database->exec($insert);
+			}
+   			writeLog("success", "$email has been invited to the $server server");
+			$this->info("$email has been invited to the $server server");
+			if($insert && User::use_mail)
+			{
+				// send email notification
+				$subject = DOMAIN . " $uServer ".$language->translate('INVITE_CODE');
+				$body = orgEmail(
+					$header = explosion($language->translate('EMAIL_INVITE_HEADER'), 0)." ".$uServer." ".explosion($language->translate('EMAIL_INVITE_HEADER'), 1),
+					$title = $language->translate('EMAIL_INVITE_TITLE'), 
+					$user = $username, 
+					$mainMessage = explosion($language->translate('EMAIL_INVITE_MESSAGE'), 0)." ".$uServer." ".explosion($language->translate('EMAIL_INVITE_MESSAGE'), 1)." ".$inviteCode,
+					$button = explosion($language->translate('EMAIL_INVITE_BUTTON'), 0)." ".$uServer." ".explosion($language->translate('EMAIL_INVITE_BUTTON'), 1),
+					$buttonURL = $link, 
+					$subTitle = $language->translate('EMAIL_INVITE_SUBTITLE'), 
+					$subMessage = explosion($language->translate('EMAIL_INVITE_SUBMESSAGE'), 0)." <a href='".$domain."?inviteCode'>".$domain."</a> ".explosion($language->translate('EMAIL_INVITE_SUBMESSAGE'), 1)
+					);
+                $this->startEmail($email, $username, $subject, $body);
+			}
+		}
 		/**
 		 * Log a user out.
 		 */
@@ -706,6 +788,7 @@ EOT;
             unset($_COOKIE['cookiePassword']);
             setcookie("cookiePassword", '', time() - 3600, '/', DOMAIN);
             setcookie("cookiePassword", '', time() - 3600, '/');
+   			writeLog("success", "$username has signed out");
 			return true;
 		}
 		/**
@@ -717,9 +800,10 @@ EOT;
 			$this->database->exec($delete);
 			$this->info("<strong>$username</strong> has been kicked out of Organizr");
 			//$this->resetSession();
-            $dir = USER_HOME . $username;
-            if(!rmdir($dir)) { $this->error("could not delete user directory $dir"); }
-            $this->info("and we deleted user directory $dir");
+    		$dir = USER_HOME . $username;
+    		if(!rmdir($dir)) { $this->error("could not delete user directory $dir"); }
+    		$this->info("and we deleted user directory $dir");
+    		writeLog("success", "$username has been deleted");
 			return true;
 		}
 		/**

+ 1 - 1
vendor/phpmailer/phpmailer/class.phpmailer.php

@@ -312,7 +312,7 @@ class PHPMailer
      * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
      * @var integer
      */
-    public $Timeout = 300;
+    public $Timeout = 10;
 
     /**
      * SMTP class debug output mode.

Някои файлове не бяха показани, защото твърде много файлове са промени