Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormattab <matthieu.aubry@gmail.com>2013-04-23 10:21:56 +0400
committermattab <matthieu.aubry@gmail.com>2013-04-23 10:21:56 +0400
commit7933de115b3fc7e5dc9bc58b7e979cdefb89d16d (patch)
treecac674c41ed0cd1304de1d6ffa12614513549d6d /plugins/SegmentEditor
parent1eff94332f296e0fdaa8d34bfc81bf261f1006e9 (diff)
Refs #2135
* BETA release of Segment editor. * API to add/update/delete/get recorded segments * cron script will pre-process segments marked as such * UI to create, update, apply segments TODOs @mattab - test with NON super user (anon/view/admin) - testing (and more testing)-- try to break it (it's not hard...) - UI: Saving the segment for reuse / pre-processing - Loading message: "This can take a while based on the amount of data that needs to be pre-archived" - i18n - Save & Apply this segment ==> "Test this segment" which would call VisitsSummary.get for early feedback ? + hoping for Piotr superJS Ninjas skills with some jquery/js that I can't figure out
Diffstat (limited to 'plugins/SegmentEditor')
-rw-r--r--plugins/SegmentEditor/API.php229
-rw-r--r--plugins/SegmentEditor/Controller.php52
-rw-r--r--plugins/SegmentEditor/SegmentEditor.php106
-rw-r--r--plugins/SegmentEditor/images/ajax-loader.gifbin0 -> 847 bytes
-rw-r--r--plugins/SegmentEditor/images/bg-inverted-corners.pngbin0 -> 968 bytes
-rw-r--r--plugins/SegmentEditor/images/bg-segment-search.pngbin0 -> 1068 bytes
-rw-r--r--plugins/SegmentEditor/images/bg-select.pngbin0 -> 1035 bytes
-rw-r--r--plugins/SegmentEditor/images/close.pngbin0 -> 288 bytes
-rw-r--r--plugins/SegmentEditor/images/close_btn.pngbin0 -> 928 bytes
-rw-r--r--plugins/SegmentEditor/images/dashboard_h_bg_hover.pngbin0 -> 333 bytes
-rw-r--r--plugins/SegmentEditor/images/down_arrow.pngbin0 -> 2898 bytes
-rw-r--r--plugins/SegmentEditor/images/icon-users.pngbin0 -> 1728 bytes
-rw-r--r--plugins/SegmentEditor/images/reset_search.pngbin0 -> 1021 bytes
-rw-r--r--plugins/SegmentEditor/images/scroller.pngbin0 -> 3329 bytes
-rw-r--r--plugins/SegmentEditor/images/search_btn.pngbin0 -> 2825 bytes
-rw-r--r--plugins/SegmentEditor/images/segment-close.pngbin0 -> 1302 bytes
-rw-r--r--plugins/SegmentEditor/images/segment-move.pngbin0 -> 1447 bytes
-rw-r--r--plugins/SegmentEditor/images/slide.pngbin0 -> 2831 bytes
-rw-r--r--plugins/SegmentEditor/images/up_arrow.pngbin0 -> 2881 bytes
-rw-r--r--plugins/SegmentEditor/templates/Segmentation.css559
-rw-r--r--plugins/SegmentEditor/templates/Segmentation.js988
-rw-r--r--plugins/SegmentEditor/templates/jquery.jscrollpane.css120
-rw-r--r--plugins/SegmentEditor/templates/jquery.jscrollpane.js1342
-rw-r--r--plugins/SegmentEditor/templates/jquery.mousewheel.js84
-rw-r--r--plugins/SegmentEditor/templates/selector.tpl141
25 files changed, 3621 insertions, 0 deletions
diff --git a/plugins/SegmentEditor/API.php b/plugins/SegmentEditor/API.php
new file mode 100644
index 0000000000..19083f8e42
--- /dev/null
+++ b/plugins/SegmentEditor/API.php
@@ -0,0 +1,229 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_SegmentEditor
+ */
+
+/**
+ * The SegmentEditor API lets you add, update, delete custom Segments, and list saved segments.
+ *
+ * @package Piwik_SegmentEditor
+ */
+class Piwik_SegmentEditor_API
+{
+ static private $instance = null;
+
+ /**
+ * @return Piwik_SegmentEditor_API
+ */
+ static public function getInstance()
+ {
+ if (self::$instance == null) {
+ self::$instance = new self;
+ }
+ return self::$instance;
+ }
+
+ protected function checkSegmentValue($definition, $idSite)
+ {
+ try {
+ $segment = new Piwik_Segment($definition, $idSite);
+ $segment->getHash();
+ } catch (Exception $e) {
+ throw new Exception("The specified segment is invalid: " . $e->getMessage());
+ }
+ }
+
+ protected function checkSegmentName($name)
+ {
+ if (empty($name)) {
+ throw new Exception("Invalid name for this custom segment.");
+ }
+ }
+
+ protected function checkEnabledAllUsers($enabledAllUsers)
+ {
+ $enabledAllUsers = (int)$enabledAllUsers;
+ if ($enabledAllUsers
+ && !Piwik::isUserIsSuperUser()
+ ) {
+ throw new Exception("&enabledAllUsers=1 requires Super User access");
+ }
+ return $enabledAllUsers;
+ }
+
+
+ /**
+ * @param $idSite
+ * @throws Exception
+ */
+ protected function checkIdSite($idSite)
+ {
+ if (empty($idSite)) {
+ if (!Piwik::isUserIsSuperUser()) {
+ throw new Exception("idSite is required, unless you are Super User and can create the segment across all websites");
+ }
+ } else {
+ if (!is_numeric($idSite)) {
+ throw new Exception("idSite should be a numeric value");
+ }
+ Piwik::checkUserHasViewAccess($idSite);
+ }
+ }
+
+ protected function checkAutoArchive($autoArchive, $idSite)
+ {
+ $autoArchive = (int)$autoArchive;
+ if ($autoArchive) {
+ $exception = new Exception("To prevent abuse, autoArchive=1 requires Super User or Admin access.");
+ if (empty($idSite)) {
+ if (!Piwik::isUserIsSuperUser()) {
+ throw $exception;
+ }
+ } else {
+ if (!Piwik::isUserHasAdminAccess($idSite)) {
+ throw $exception;
+ }
+ }
+ }
+ return $autoArchive;
+ }
+
+ public function delete($idSegment)
+ {
+ $segment = $this->getSegmentOrFail($idSegment);
+ $db = Zend_Registry::get('db');
+ $db->delete(Piwik_Common::prefixTable('segment'), 'idsegment = ' . $idSegment);
+ return true;
+ }
+
+ public function update($idSegment, $name, $definition, $idSite = false, $autoArchive = false, $enabledAllUsers = false)
+ {
+ $this->checkIdSite($idSite);
+ $this->checkSegmentName($name);
+ $this->checkSegmentValue($definition, $idSite);
+ $enabledAllUsers = $this->checkEnabledAllUsers($enabledAllUsers);
+ $autoArchive = $this->checkAutoArchive($autoArchive, $idSite);
+
+ $segment = $this->getSegmentOrFail($idSegment);
+ $bind = array(
+ 'name' => $name,
+ 'definition' => $definition,
+ 'enable_all_users' => $enabledAllUsers,
+ 'enable_only_idsite' => $idSite,
+ 'auto_archive' => $autoArchive,
+ 'ts_last_edit' => Piwik_Date::now()->getDatetime(),
+ );
+
+ $db = Zend_Registry::get('db');
+ $db->update(Piwik_Common::prefixTable("segment"),
+ $bind,
+ "idsegment = $idSegment"
+ );
+ return true;
+ }
+
+
+ public function add($name, $definition, $idSite = false, $autoArchive = false, $enabledAllUsers = false)
+ {
+ Piwik::checkUserIsNotAnonymous();
+ $this->checkIdSite($idSite);
+ $this->checkSegmentName($name);
+ $this->checkSegmentValue($definition, $idSite);
+ $enabledAllUsers = $this->checkEnabledAllUsers($enabledAllUsers);
+ $autoArchive = $this->checkAutoArchive($autoArchive, $idSite);
+
+ $db = Zend_Registry::get('db');
+ $bind = array(
+ 'name' => $name,
+ 'definition' => $definition,
+ 'login' => Piwik::getCurrentUserLogin(),
+ 'enable_all_users' => $enabledAllUsers,
+ 'enable_only_idsite' => $idSite,
+ 'auto_archive' => $autoArchive,
+ 'ts_created' => Piwik_Date::now()->getDatetime(),
+ 'deleted' => 0,
+ );
+ $db->insert(Piwik_Common::prefixTable("segment"), $bind);
+ return $db->lastInsertId();
+ }
+
+ public function getSegmentsToAutoArchive($idSite = false)
+ {
+ Piwik::checkUserIsSuperUser();
+
+ $sqlRestrictSite = '';
+ $bind = array();
+ if ($idSite) {
+ $sqlRestrictSite = 'OR enable_only_idsite = ?';
+ $bind = array($idSite);
+ }
+ $segments = Zend_Registry::get('db')->fetchAll("SELECT *
+ FROM " . Piwik_Common::prefixTable("segment") . "
+ WHERE auto_archive = 1
+ AND deleted = 0
+ AND (enable_only_idsite IS NULL " . $sqlRestrictSite . " )", $bind
+ );
+ return $segments;
+ }
+
+ public function get($idSegment)
+ {
+ Piwik::checkUserHasSomeViewAccess();
+ if (!is_numeric($idSegment)) {
+ throw new Exception("idSegment should be numeric.");
+ }
+ $segment = Zend_Registry::get('db')->fetchRow("SELECT * " .
+ " FROM " . Piwik_Common::prefixTable("segment") .
+ " WHERE idsegment = ?", $idSegment);
+
+ if (empty($segment)) {
+ return false;
+ }
+ try {
+ Piwik::checkUserIsSuperUserOrTheUser($segment['login']);
+ } catch (Exception $e) {
+ throw new Exception("You can only manage your own segments (unless you are Super User).");
+ }
+
+ if ($segment['deleted']) {
+ throw new Exception("This segment is marked as deleted.");
+ }
+ return $segment;
+ }
+
+ /**
+ * @param $idSegment
+ * @throws Exception
+ */
+ protected function getSegmentOrFail($idSegment)
+ {
+ $segment = $this->get($idSegment);
+
+ if (empty($segment)) {
+ throw new Exception("Requested segment not found");
+ }
+ return $segment;
+ }
+
+ public function getAll($idSite)
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+
+ $sql = "SELECT * " .
+ " FROM " . Piwik_Common::prefixTable("segment") .
+ " WHERE (enable_only_idsite = ? OR enable_only_idsite IS NULL)
+ AND (enable_all_users = 1 OR login = ?)
+ AND deleted = 0
+ ORDER BY name ASC";
+ $bind = array($idSite, Piwik::getCurrentUserLogin());
+ $segments = Zend_Registry::get('db')->fetchAll($sql, $bind);
+
+ return $segments;
+ }
+}
diff --git a/plugins/SegmentEditor/Controller.php b/plugins/SegmentEditor/Controller.php
new file mode 100644
index 0000000000..848171044f
--- /dev/null
+++ b/plugins/SegmentEditor/Controller.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_SegmentEditor
+ */
+
+/**
+ * @package Piwik_SegmentEditor
+ */
+class Piwik_SegmentEditor_Controller extends Piwik_Controller
+{
+
+ public function getSelector()
+ {
+ $view = Piwik_View::factory('selector');
+ $idSite = Piwik_Common::getRequestVar('idSite');
+ $this->setGeneralVariablesView($view);
+ $segments = Piwik_API_API::getInstance()->getSegmentsMetadata($idSite);
+
+ $segmentsByCategory = $customVariablesSegments = array();
+ foreach($segments as $segment) {
+ if($segment['category'] == 'Visit'
+ && $segment['type'] == 'metric') {
+ $segment['category'] .= ' (' . lcfirst(Piwik_Translate('General_Metrics')) . ')';
+ }
+ $segmentsByCategory[$segment['category']][] = $segment;
+ }
+ uksort($segmentsByCategory, array($this, 'sortCustomVariablesLast'));
+
+ $view->segmentsByCategory = $segmentsByCategory;
+
+
+ $savedSegments = Piwik_SegmentEditor_API::getInstance()->getAll($idSite);
+ $view->savedSegmentsJson = Piwik_Common::json_encode($savedSegments);
+
+ $out = $view->render();
+ echo $out;
+ }
+
+ public function sortCustomVariablesLast($a, $b)
+ {
+ if($a == Piwik_Translate('CustomVariables_CustomVariables')) {
+ return 1;
+ }
+ return -1;
+ }
+}
diff --git a/plugins/SegmentEditor/SegmentEditor.php b/plugins/SegmentEditor/SegmentEditor.php
new file mode 100644
index 0000000000..989eee6928
--- /dev/null
+++ b/plugins/SegmentEditor/SegmentEditor.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_SegmentEditor
+ */
+
+/**
+ * @package Piwik_SegmentEditor
+ */
+class Piwik_SegmentEditor extends Piwik_Plugin
+{
+ public function getInformation()
+ {
+ return array(
+ 'description' => 'Create and reuse custom visitor Segments with the Segment Editor.',
+ 'author' => 'Piwik',
+ 'author_homepage' => 'http://piwik.org/',
+ 'version' => Piwik_Version::VERSION,
+ );
+ }
+
+ public function getListHooksRegistered()
+ {
+ return array(
+ 'Piwik.getKnownSegmentsToArchiveForSite' => 'getKnownSegmentsToArchiveForSite',
+ 'Piwik.getKnownSegmentsToArchiveAllSites' => 'getKnownSegmentsToArchiveAllSites',
+ 'AssetManager.getJsFiles' => 'getJsFiles',
+ 'AssetManager.getCssFiles' => 'getCssFiles',
+ 'template_nextToCalendar' => 'getSegmentEditorHtml',
+ );
+ }
+
+ function getSegmentEditorHtml($notification)
+ {
+ $out =& $notification->getNotificationObject();
+ $controller = new Piwik_SegmentEditor_Controller();
+ $out .= $controller->getSelector();
+ }
+
+ public function getKnownSegmentsToArchiveAllSites($notification)
+ {
+ $segments =& $notification->getNotificationObject();
+ $segmentToAutoArchive = Piwik_SegmentEditor_API::getInstance()->getSegmentsToAutoArchive();
+ if (!empty($segmentToAutoArchive)) {
+ $segments = array_merge($segments, $segmentToAutoArchive);
+ }
+ }
+
+ public function getKnownSegmentsToArchiveForSite($notification)
+ {
+ $segments =& $notification->getNotificationObject();
+ $idSite = $notification->getNotificationInfo();
+ $segmentToAutoArchive = Piwik_SegmentEditor_API::getInstance()->getSegmentsToAutoArchive($idSite);
+
+ foreach ($segmentToAutoArchive as $segmentInfo) {
+ $segments[] = $segmentInfo['definition'];
+ }
+ $segments = array_unique($segments);
+ }
+
+ public function install()
+ {
+ $queries[] = 'CREATE TABLE `' . Piwik_Common::prefixTable('segment') . '` (
+ `idsegment` INT(11) NOT NULL AUTO_INCREMENT,
+ `name` VARCHAR(255) NOT NULL,
+ `definition` TEXT NOT NULL,
+ `login` VARCHAR(100) NOT NULL,
+ `enable_all_users` tinyint(4) NOT NULL default 0,
+ `enable_only_idsite` INTEGER(11) NULL,
+ `auto_archive` tinyint(4) NOT NULL default 0,
+ `ts_created` TIMESTAMP NULL,
+ `ts_last_edit` TIMESTAMP NULL,
+ `deleted` tinyint(4) NOT NULL default 0,
+ PRIMARY KEY (`idsegment`)
+ ) DEFAULT CHARSET=utf8';
+ try {
+ foreach ($queries as $query) {
+ Piwik_Exec($query);
+ }
+ } catch (Exception $e) {
+ if (!Zend_Registry::get('db')->isErrNo($e, '1050')) {
+ throw $e;
+ }
+ }
+ }
+
+ public function getJsFiles($notification)
+ {
+ $jsFiles = & $notification->getNotificationObject();
+ $jsFiles[] = "plugins/SegmentEditor/templates/jquery.jscrollpane.js";
+ $jsFiles[] = "plugins/SegmentEditor/templates/Segmentation.js";
+ }
+
+ public function getCssFiles($notification)
+ {
+ $cssFiles = & $notification->getNotificationObject();
+ $cssFiles[] = "plugins/SegmentEditor/templates/Segmentation.css";
+ $cssFiles[] = "plugins/SegmentEditor/templates/jquery.jscrollpane.css";
+ }
+
+}
diff --git a/plugins/SegmentEditor/images/ajax-loader.gif b/plugins/SegmentEditor/images/ajax-loader.gif
new file mode 100644
index 0000000000..bc545850ad
--- /dev/null
+++ b/plugins/SegmentEditor/images/ajax-loader.gif
Binary files differ
diff --git a/plugins/SegmentEditor/images/bg-inverted-corners.png b/plugins/SegmentEditor/images/bg-inverted-corners.png
new file mode 100644
index 0000000000..b4602e230c
--- /dev/null
+++ b/plugins/SegmentEditor/images/bg-inverted-corners.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/bg-segment-search.png b/plugins/SegmentEditor/images/bg-segment-search.png
new file mode 100644
index 0000000000..3a9a257eec
--- /dev/null
+++ b/plugins/SegmentEditor/images/bg-segment-search.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/bg-select.png b/plugins/SegmentEditor/images/bg-select.png
new file mode 100644
index 0000000000..a69f9a69c5
--- /dev/null
+++ b/plugins/SegmentEditor/images/bg-select.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/close.png b/plugins/SegmentEditor/images/close.png
new file mode 100644
index 0000000000..f2a1380b19
--- /dev/null
+++ b/plugins/SegmentEditor/images/close.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/close_btn.png b/plugins/SegmentEditor/images/close_btn.png
new file mode 100644
index 0000000000..6cf122e27a
--- /dev/null
+++ b/plugins/SegmentEditor/images/close_btn.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/dashboard_h_bg_hover.png b/plugins/SegmentEditor/images/dashboard_h_bg_hover.png
new file mode 100644
index 0000000000..37e4bd54e8
--- /dev/null
+++ b/plugins/SegmentEditor/images/dashboard_h_bg_hover.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/down_arrow.png b/plugins/SegmentEditor/images/down_arrow.png
new file mode 100644
index 0000000000..a364892c7d
--- /dev/null
+++ b/plugins/SegmentEditor/images/down_arrow.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/icon-users.png b/plugins/SegmentEditor/images/icon-users.png
new file mode 100644
index 0000000000..d17d4501be
--- /dev/null
+++ b/plugins/SegmentEditor/images/icon-users.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/reset_search.png b/plugins/SegmentEditor/images/reset_search.png
new file mode 100644
index 0000000000..cb1d9e80a2
--- /dev/null
+++ b/plugins/SegmentEditor/images/reset_search.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/scroller.png b/plugins/SegmentEditor/images/scroller.png
new file mode 100644
index 0000000000..ddcab93426
--- /dev/null
+++ b/plugins/SegmentEditor/images/scroller.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/search_btn.png b/plugins/SegmentEditor/images/search_btn.png
new file mode 100644
index 0000000000..34abc4132a
--- /dev/null
+++ b/plugins/SegmentEditor/images/search_btn.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/segment-close.png b/plugins/SegmentEditor/images/segment-close.png
new file mode 100644
index 0000000000..266e0595a0
--- /dev/null
+++ b/plugins/SegmentEditor/images/segment-close.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/segment-move.png b/plugins/SegmentEditor/images/segment-move.png
new file mode 100644
index 0000000000..25ee071db9
--- /dev/null
+++ b/plugins/SegmentEditor/images/segment-move.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/slide.png b/plugins/SegmentEditor/images/slide.png
new file mode 100644
index 0000000000..657b9a6de5
--- /dev/null
+++ b/plugins/SegmentEditor/images/slide.png
Binary files differ
diff --git a/plugins/SegmentEditor/images/up_arrow.png b/plugins/SegmentEditor/images/up_arrow.png
new file mode 100644
index 0000000000..c7cb24c2b4
--- /dev/null
+++ b/plugins/SegmentEditor/images/up_arrow.png
Binary files differ
diff --git a/plugins/SegmentEditor/templates/Segmentation.css b/plugins/SegmentEditor/templates/Segmentation.css
new file mode 100644
index 0000000000..6a12723699
--- /dev/null
+++ b/plugins/SegmentEditor/templates/Segmentation.css
@@ -0,0 +1,559 @@
+/* ADDITIONAL STYLES*/
+.searchFound {
+ border: 0px solid red;
+}
+.others {
+ border: 0px solid green;
+}
+.clear {
+ clear: both;
+}
+.segment-row-inputs {
+ margin-bottom: 5px;
+}
+.hovered {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ border: 2px dashed #000 !important;
+ padding: 0px;
+}
+.metricListBlock {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ width: 292px;
+ margin-right: 11px;
+ border: 2px dashed #EFEFEB;
+}
+.metricListBlock > select {
+ margin: 0 !important;
+ width: 98% !important;
+ margin-left: 2px !important;
+}
+.metricMatchBlock {
+ width: 120px;
+ margin-right: 11px;
+}
+.metricValueBlock {
+ width: 352px;
+}
+div.scrollable {
+ height: 100%;
+ overflow: hidden;
+ overflow-y: auto;
+}
+.no_results {
+ position: absolute;
+ margin: -225px 0 0 10px;
+}
+.segment-element {
+ border: 1px solid #a9a399;
+ background-color: #f1f0eb;
+ padding: 6px 4px;
+ border-radius: 3px 3px 3px 3px;
+}
+.segment-element .custom_select_search {
+ width: 146px;
+ height: 21px;
+ background: url(../images/bg-segment-search.png) 0 10px no-repeat;
+ padding: 10px 0 0 0;
+ margin: 10px 0 10px 15px;
+ border-top: 1px solid #dcdacf;
+ position: relative;
+}
+.segment-element .custom_select_search input[type="text"] {
+ font: 11px Arial;
+ color: #454545;
+ width: 125px;
+ padding: 3px 0px 3px 5px;
+ border: none;
+ background: none;
+}
+.segment-element .custom_select_search a {
+ position: absolute;
+ width: 13px;
+ height: 13px;
+ right: 5px;
+ top: 14px;
+ background: url(../images/reset_search.png);
+}
+.segment-element .segment-nav {
+ width: 180px;
+ position: absolute;
+ top: 7px;
+ left: 5px;
+ bottom: 135px;
+ float: left;
+ width: 170px;
+}
+.segment-element .segment-nav h4 {
+ font: bold 14px Arial;
+ padding: 0 0 8px 0;
+}
+.segment-element .segment-nav h4.visits {
+ padding-left: 28px;
+ background: url(../images/icon-users.png) 0 0 no-repeat;
+}
+.segment-element .segment-nav h4 a {
+ color: #255792;
+ text-decoration: none;
+}
+.segment-element .segment-nav div > ul {
+ padding: 0 0 0 15px;
+}
+.segment-element .segment-nav div > ul > li {
+ padding: 2px 0;
+ line-height: 14px;
+}
+.segment-element .segment-nav div > ul > li li {
+ padding: 1px;
+ border-radius: 3px 3px 3px 3px;
+}
+.segment-element .segment-nav div > ul > li li:hover {
+ padding: 0;
+ border: 1px solid #cfccbd;
+ border-bottom: 1px solid #7c7a72;
+ background-color: #e9e7e3;
+}
+.segment-element .segment-nav div > ul > li li:hover a {
+ cursor: move;
+ padding: 1px 0 2px 8px;
+ border-top: 1px solid #fff;
+ background: #eae8e3 url(../images/segment-move.png) 100% 50% no-repeat;
+}
+.segment-element .segment-nav div > ul > li li a {
+ padding: 2px 0 2px 8px;
+ font-weight: normal;
+ display: block;
+}
+.segment-element .segment-nav div > ul > li ul {
+ margin: 2px 0 -3px 10px;
+}
+.segment-element .segment-nav div > ul > li a {
+ color: #5d5342;
+ font: bold 11px Arial;
+ text-decoration: none;
+ text-shadow: 0 1px 0 #fff;
+}
+.segment-element .segment-content {
+ min-height: 300px;
+ padding: 0 0 20px 203px;
+}
+.segment-element .segment-content h3 {
+ font: bold 16px Arial;
+ color: #505050;
+ margin: 11px 0 0 0;
+ text-shadow: 0 1px 0 #fff;
+}
+.segment-element .segment-content h3 a {
+ font-size: 11px;
+ text-decoration: none;
+ margin: -1px 0 0 0;
+}
+.segment-element .segment-content .segment-rows {
+ padding: 4px;
+ margin: 0 3px 0 0;
+ background: #fff;
+ border: 1px solid #a9a399;
+ border-radius: 3px 3px 3px 3px;
+ position: relative;
+ box-shadow: 0 12px 6px -10px rgba(0, 0, 0, 0.42);
+}
+.segment-element .segment-content .segment-add-row,
+.segment-element .segment-content .segment-add-or {
+ font: bold 14px Arial;
+ background: #fff;
+ color: #b9b9b9;
+ text-align: center;
+ position: relative;
+}
+.segment-element .segment-content .segment-add-row > div,
+.segment-element .segment-content .segment-add-or > div {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ border: 2px dashed #fff;
+ padding: 10px 0;
+}
+.segment-element .segment-content .segment-add-row > div a,
+.segment-element .segment-content .segment-add-or > div a {
+ color: #b9b9b9;
+ text-decoration: none;
+}
+.segment-element .segment-content .segment-add-row > div a span,
+.segment-element .segment-content .segment-add-or > div a span {
+ color: #255792;
+}
+.segment-element .segment-content .segment-add-row {
+ margin: 0 3px 0 0;
+ padding: 0 12px;
+ border: 1px solid #a9a399;
+ border-radius: 3px 3px 3px 3px;
+ box-shadow: 0 12px 6px -10px rgba(0, 0, 0, 0.42);
+}
+.segment-element .segment-content .segment-add-or {
+ text-shadow: 0 1px 0 #fff;
+ display: inline-block;
+ width: 98%;
+ padding: 0 1%;
+ background: #efefeb;
+ border-radius: 3px 3px 3px 3px;
+}
+.segment-element .segment-content .segment-add-or > div {
+ border: 2px dashed #EFEFEB;
+}
+.segment-element .segment-content .segment-row {
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ display: inline-block;
+ position: relative;
+ width: 811px;
+ padding: 12px 1%;
+ background: #efefeb;
+ padding: 7px 5px 0 5px;
+}
+.segment-element .segment-content .segment-row .segment-close {
+ top: 15px;
+ right: 6px;
+ position: absolute;
+ width: 15px;
+ height: 15px;
+ background: url(../images/segment-close.png) 0 0 no-repeat;
+}
+.segment-element .segment-content .segment-row .segment-loading {
+ display:none;
+ top: 25px;
+ right: 30px;
+ position: absolute;
+ width: 15px;
+ height: 15px;
+ background: url(../../MultiSites/images/loading-blue.gif) 0 0 no-repeat;
+}
+
+.segment-element .segment-content .segment-or {
+ display: inline-block;
+ margin: 0 0 0 6%;
+ position: relative;
+ background: #efefeb;
+ padding: 5px 28px;
+ color: #4f4f4f;
+ font: bold 14px Arial;
+ text-shadow: 0 1px 0 #fff;
+}
+.segment-element .segment-content .segment-or:before,
+.segment-element .segment-content .segment-or:after {
+ content: '';
+ position: absolute;
+ background: #fff;
+ border: 1px solid #efefeb;
+ width: 10px;
+ top: -1px;
+ bottom: -1px;
+}
+.segment-element .segment-content .segment-or:before {
+ border-left: none;
+ left: 0px;
+ border-radius: 0 5px 5px 0;
+}
+.segment-element .segment-content .segment-or:after {
+ border-right: none;
+ right: 0px;
+ border-radius: 5px 0 0 5px;
+}
+.segment-element .segment-content .segment-and {
+ display: inline-block;
+ margin: -1px 0 -1px 6%;
+ z-index: 1;
+ position: relative;
+ background: #fff;
+ padding: 5px 35px;
+ color: #4f4f4f;
+ font: bold 14px Arial;
+ text-shadow: 0 1px 0 #fff;
+}
+.segment-element .segment-content .segment-and:before,
+.segment-element .segment-content .segment-and:after {
+ content: '';
+ position: absolute;
+ background: url(../images/bg-inverted-corners.png);
+ background: #f1f0eb\0/;
+ border: 1px solid #a9a399;
+ width: 10px;
+ top: 0px;
+ bottom: 0px;
+}
+.segment-element .segment-content .segment-and:before {
+ border-left: none;
+ left: 0px;
+ border-radius: 0 5px 5px 0;
+}
+.segment-element .segment-content .segment-and:after {
+ border-right: none;
+ right: 0px;
+ border-radius: 5px 0 0 5px;
+}
+.segment-element .segment-content .segment-input {
+ float: left;
+ padding: 6px 0 5px 3px;
+ border: 2px dashed #EFEFEB;
+ margin-right: 3px;
+}
+.segment-element .segment-content .segment-input label {
+ display: block;
+ margin: 0 0 5px 0;
+ font: 11px Arial;
+ color: #505050;
+}
+.segment-element .segment-content .segment-input select,
+.segment-element .segment-content .segment-input input {
+ display: block;
+ font: 16px Arial;
+ color: #255792;
+ width: 96%;
+ padding: 7px 2%;
+ border-radius: 2px 2px 2px 2px;
+}
+.segment-element .segment-content .segment-input input {
+ padding: 8px 2%;
+}
+.segment-element .segment-top {
+ font: 11px Arial;
+ color: #505050;
+ text-align: right;
+ padding: 3px 7px 0 0;
+}
+.segment-element .segment-top a {
+ text-decoration: none;
+}
+.segment-element .segment-top a.dropdown {
+ padding: 0 17px 0 0;
+ background: url(../../../themes/default/images/sort_subtable_desc.png) 100% -2px no-repeat;
+}
+.segment-element .segment-footer {
+ background: #eae8e3;
+ border-top: 1px solid #a9a399;
+ text-align: right;
+ padding: 7px 10px;
+ margin: 0 -4px -6px -4px;
+}
+.segment-element .segment-footer a.delete {
+ color:red;
+}
+.segment-element .segment-footer a {
+ font: 14px Arial;
+ color: #255792;
+ margin: 0 5px;
+ text-decoration: none;
+}
+
+.segment-element .segment-footer button {
+ width: 178px;
+ height: 30px;
+ background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDE3OCAzMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PGxpbmVhckdyYWRpZW50IGlkPSJoYXQwIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjUwJSIgeTE9IjEwMCUiIHgyPSI1MCUiIHkyPSItMS40MjEwODU0NzE1MjAyZS0xNCUiPgo8c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjODM3OTZiIiBzdG9wLW9wYWNpdHk9IjEiLz4KPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjYWJhMzkzIiBzdG9wLW9wYWNpdHk9IjEiLz4KICAgPC9saW5lYXJHcmFkaWVudD4KCjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxNzgiIGhlaWdodD0iMzAiIGZpbGw9InVybCgjaGF0MCkiIC8+Cjwvc3ZnPg==);
+ background-image: -moz-linear-gradient(bottom, #83796b 0%, #aba393 100%);
+ background-image: -o-linear-gradient(bottom, #83796b 0%, #aba393 100%);
+ background-image: -webkit-linear-gradient(bottom, #83796b 0%, #aba393 100%);
+ background-image: linear-gradient(bottom, #83796b 0%, #aba393 100%);
+ color: #fff;
+ font-family: "Arial";
+ font-size: 16px;
+ font-weight: bold;
+ text-shadow: 0 1px 0px #251e15;
+ border-radius: 4px 4px 4px 4px;
+ border: none;
+ margin: 0 0 0 15px;
+ background: #978f80\0/;
+}
+.segmentationContainer {
+ position: absolute;
+ z-index: 2;
+ background: #f7f7f7;
+ border: 1px solid #e4e5e4;
+ padding: 5px 10px 6px 5px;
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ color: #444;
+ font-size: 14px;
+}
+.segmentationContainer:hover {
+ background: #f1f0eb;
+ border-color: #a9a399;
+}
+.segmentationContainer > span > b {
+ color: #255792;
+}
+.segmentationContainer .submenu {
+ font-size: 13px;
+ font-weight: bold;
+ min-width: 250px;
+}
+.segmentationContainer .submenu ul {
+ color: #5D5342;
+ float: none;
+ font-size: 11px;
+ font-weight: normal;
+ line-height: 20px;
+ list-style: none outside none;
+ margin-left: 5px;
+ margin-right: 0px;
+ padding-top: 10px;
+}
+.segmentationContainer .submenu ul li {
+ padding: 2px 0px 1px 10px;
+ margin: 3px 0 0 0;
+ cursor: pointer;
+}
+.segmentationContainer .submenu ul li:hover {
+ color: #255792;
+ margin: 0;
+ border: 1px solid #d5d2c6;
+ border-bottom: 2px solid #918f88;
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ background: #eae8e3;
+}
+.segmentationContainer ul.submenu {
+ padding-top: 5px;
+ display: none;
+ float: left;
+}
+.segmentationContainer ul.submenu > li span.editSegment {
+ display: block;
+ float: right;
+ width: 45px;
+ text-align: center;
+ font-weight: normal;
+}
+.segmentationContainer.visible {
+ width: 250px;
+}
+.segmentationContainer.visible ul.submenu {
+ display: block;
+ list-style: none;
+}
+.segmentationContainer.visible .add_new_segment {
+ display: block;
+ background: url("../images/dashboard_h_bg_hover.png") repeat-x scroll 0 0 #847b6d;
+ border: 0 none;
+ border-radius: 4px 4px 4px 4px;
+ clear: both;
+ color: #FFFFFF;
+ float: right;
+ margin: 12px 0 10px;
+ padding: 3px 10px;
+ text-decoration: none;
+}
+.segmentationContainer > ul.submenu > li {
+ padding: 5px 0;
+ clear: both;
+ cursor: pointer;
+}
+span.segmentationTitle {
+ background: url(../../../themes/default/images/sort_subtable_desc.png) right center no-repeat !important;
+ padding-right: 20px !important;
+ width: 230px !important;
+ display: block !important;
+ cursor: pointer;
+}
+.add_new_segment {
+ display: none;
+}
+.segmentList {
+ max-height: 250px;
+}
+.jspVerticalBar {
+ background: transparent!important;
+}
+/* ADDITIONAL STYLES*/
+body > a.ddmetric {
+ display: block;
+ cursor: move;
+ padding: 1px 0 2px 18px;
+ background: #eae8e3 url(../images/segment-move.png) 100% 50% no-repeat;
+ color: #5d5342;
+ font: normal 11px Arial;
+ text-decoration: none;
+ text-shadow: 0 1px 0 #fff;
+ border: 1px solid #cfccbd;
+ border-top: 1px solid #fff;
+ border-bottom: 1px solid #7c7a72;
+}
+.segment-element .segment-nav div > ul > li ul {
+ margin-left: 0;
+}
+.segment-element .segment-nav div > ul > li li a,
+.segment-element .segment-nav div > ul > li li a:hover {
+ padding-right: 18px;
+}
+.hovered {
+ border-color: #a0a0a0!Important;
+}
+a.metric_category {
+ display: block;
+ width: 100%;
+}
+.segment-content > h3 {
+ padding-bottom: 7px;
+}
+.no_results {
+ margin: 0;
+ position: relative;
+}
+.no_results a {
+ cursor: normal;
+}
+.ui-widget-segmentation {
+ border: 1px solid #D4D4D4 !important;
+}
+.clearfix {
+ zoom: 1;
+}
+.clearfix:after {
+ display: block;
+ visibility: hidden;
+ height: 0;
+ clear: both;
+ content: ".";
+}
+
+#available_segments a.dropdown {
+ background: url("../../../themes/default/images/sort_subtable_desc.png") no-repeat scroll 100% -2px transparent !important;
+ padding: 0 17px 0 0 !important;
+}
+.notification{
+ float: left;
+}
+.metricValueBlock input
+{
+ padding:8px !important;
+}
+.ajaxError {
+ border: 0 !important;
+ width: auto !important;
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+.segmentationContainer{
+ z-index:99!important;
+}
+.segment-element{
+ z-index:999!important;
+}
+#dashboardSettings{
+ z-index:120;
+}
+#periodString{
+ z-index:121;
+}
+.segmentationSelectorContainer {
+ margin: 8px;
+}
+.grayed, .grayed:hover{
+ color:#BBB6AD !important;
+}
+.ui-autocomplete { position: absolute; cursor: default;z-index:1000 !important;} \ No newline at end of file
diff --git a/plugins/SegmentEditor/templates/Segmentation.js b/plugins/SegmentEditor/templates/Segmentation.js
new file mode 100644
index 0000000000..4813c30fb2
--- /dev/null
+++ b/plugins/SegmentEditor/templates/Segmentation.js
@@ -0,0 +1,988 @@
+/*!
+ * Piwik - Web Analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+Segmentation = (function($) {
+
+ var segmentation = function segmentation(config) {
+
+ var self = this;
+ // set defaults for widget
+ self.currentSegmentStr = "";
+ self.targetId = "segmentEditorPanel";
+ self.segmentAccess = "read";
+ self.segmentList = [];
+ // -----------
+ for(var item in config)
+ {
+ self[item] = config[item];
+ }
+
+ self.timer = ""; // variable for further use in timing events
+ self.searchAllowed = true;
+ //----------
+
+ self.availableMatches = [];
+ self.availableMatches["metric"] = [];
+ self.availableMatches["metric"]["=="] = "Equals";
+ self.availableMatches["metric"]["!="] = "Not Equals";
+ self.availableMatches["metric"]["<="] = "At most";
+ self.availableMatches["metric"][">="] = "At least";
+ self.availableMatches["metric"]["<"] = "Less than";
+ self.availableMatches["metric"][">"] = "Greater than";
+
+ self.availableMatches["dimension"] = [];
+ self.availableMatches["dimension"]["=="] = "Is";
+ self.availableMatches["dimension"]["!="] = "Is not";
+ self.availableMatches["dimension"]["=@"] = "Contains";
+ self.availableMatches["dimension"]["!@"] = "Does not contain";
+
+ segmentation.prototype.getSegment = function(){
+ var self = this;
+ return self.currentSegmentStr;
+ }
+
+ var setSegment = function(segmentStr){
+ self.currentSegmentStr = segmentStr;
+ }
+
+ segmentation.prototype.shortenSegmentName = function(name, length){
+
+ if(typeof length === "undefined") length = 30;
+ if(typeof name === "undefined") name = "";
+ var i;
+
+ if(name.length > length)
+ {
+ for(i = length; i > 0; i--){
+ if(name[i] === " "){
+ break;
+ }
+ }
+ if(i == 0){
+ i = length-3;
+ }
+
+ return name.slice(0,i)+"...";
+ }
+ return name;
+ }
+
+ var markCurrentSegment = function(){
+ var current = self.getSegment();
+// window.setTimeout(function(){
+ var segmentationTitle = $(self.content).find(".segmentationTitle");
+ if( current != "")
+ {
+ var foundItems = $(self.content).find('div.segmentList > ul > li[data-definition="'+current+'"]');
+ if( foundItems.length > 0)
+ {
+ var name = $(foundItems).first().find("span.segname").text();
+ segmentationTitle.html("<b>"+name+"</b>");
+ }
+ else{
+ segmentationTitle.html("<b>Custom Segment</b>");
+ }
+ }
+ else {
+ $(self.content).find(".segmentationTitle").text("All visits");
+ }
+// }, 20);
+ }
+
+ var getAndDiv = function(){
+ if(typeof andDiv === "undefined"){
+ var andDiv = $("#SegmentEditor > div.segment-and").clone();
+ }
+ return andDiv.clone();
+ }
+
+ var getOrDiv = function(){
+ if(typeof orDiv === "undefined"){
+ var orDiv = $("#SegmentEditor > div.segment-or").clone();
+ }
+ return orDiv.clone();
+ }
+
+ var getMockedInputSet = function(){
+ if(typeof mockedInputSet === "undefined"){
+ var mockedInputSet = $("#SegmentEditor div.segment-row-inputs").clone();
+ }
+ return mockedInputSet.clone();
+ }
+
+ var getMockedInputRowHtml = function(){
+ if(typeof mockedInputRow === "undefined"){
+ var mockedInputRow = '<div class="segment-row"><a class="segment-close" href="#"></a><div class="segment-row-inputs">'+getMockedInputSet().html()+'</div></div>';
+ }
+ return mockedInputRow;
+ }
+
+ var getMockedFormRow = function(){
+ if(typeof mockedFormRow === "undefined")
+ {
+ var mockedFormRow = $("#SegmentEditor div.segment-rows").clone();
+ $(mockedFormRow).find(".segment-row").append(getMockedInputSet()).after(getAddOrBlockButtonHtml).after(getOrDiv());
+ }
+ return mockedFormRow.clone();
+ }
+
+ var getInitialStateRowsHtml = function(){
+ if(typeof initialStateRows === "undefined"){
+ var content = $("#SegmentEditor div.initial-state-rows").html();
+ var initialStateRows = $(content).clone();
+ }
+ return initialStateRows;
+ }
+
+ var revokeInitialStateRows = function(){
+ $(self.form).find(".segment-add-row").remove();
+ $(self.form).find(".segment-and").remove();
+ }
+
+ var appendSpecifiedRowHtml= function(metric) {
+ $(self.form).find(".segment-content > h3").after(getMockedFormRow());
+ $(self.form).find(".segment-content").append(getAndDiv());
+ $(self.form).find(".segment-content").append(getAddNewBlockButtonHtml());
+ doDragDropBindings();
+ $(self.form).find(".metricList").val(metric).trigger("change");
+ }
+
+ var appendComplexRowHtml = function(block){
+ var key;
+ var newRow = getMockedFormRow();
+
+ var x = $(newRow).find(".metricMatchBlock select");
+ $(newRow).find(".metricListBlock select").val(block[0].metric);
+ $(newRow).find(".metricMatchBlock select").val(block[0].match);
+ $(newRow).find(".metricValueBlock input").val(block[0].value);
+
+ if(block.length > 1) {
+ $(newRow).find(".segment-add-or").remove();
+ for(key = 1; key < block.length;key++) {
+ var newSubRow = $(getMockedInputRowHtml()).clone();
+ $(newSubRow).find(".metricListBlock select").val(block[key].metric);
+ $(newSubRow).find(".metricMatchBlock select").val(block[key].match);
+ $(newSubRow).find(".metricValueBlock input").val(block[key].value);
+ $(newRow).append(newSubRow).append(getOrDiv());
+ }
+ $(newRow).append(getAddOrBlockButtonHtml());
+ }
+ $(self.form).find(".segment-content").append(newRow).append(getAndDiv());
+ }
+
+ var applyInitialStateModification = function(){
+ $(self.form).find(".segment-add-row").remove();
+ $(self.form).find(".segment-content").append(getInitialStateRowsHtml());
+ doDragDropBindings();
+ }
+
+ var getListHtml = function() {
+ var html = $("#SegmentEditor > .listHtml").clone();
+ var segment, injClass;
+ var listHtml = '<li data-idsegment="" data-definition=""><span class="segname">All Visits (default)</span></li> ';
+ if(self.segmentList.length > 0){
+ for(var key in self.segmentList)
+ {
+ segment = self.segmentList[key];
+ if(jQuery.inArray(segment.definition, self.currentSegmentsGlobal) > -1){
+ injClass = 'class="grayed"';
+ }
+ else{
+ injClass = "";
+ }
+ listHtml += '<li data-idsegment="'+segment.idsegment+'" data-definition=\''+segment.definition+'\' '
+ + injClass +' title="'+segment.name+'"><span class="segname">'
+ + self.shortenSegmentName(segment.name)+'</span>';
+ if(self.segmentAccess == "write") {
+ listHtml += '<span class="editSegment">[edit]</span>';
+ }
+ listHtml += '</li>';
+ }
+ $(html).find(".segmentList > ul").append(listHtml);
+ if(self.segmentAccess === "write"){
+ $(html).find(".add_new_segment").html(_pk_translate('General_AddNewSegment_js'));
+ }
+ else{
+ $(html).find(".add_new_segment").hide();;
+ }
+ }
+ else
+ {
+ $(html).find(".segmentList > ul").append(listHtml);
+ }
+ return html;
+ }
+
+ var getFormHtml = function() {
+ var html = $("#SegmentEditor > .segment-element").clone();
+ // set left margin to center form
+ //$("body").append(html);
+ var segmentsDropdown = $(html).find("#available_segments_select");
+ var segment, newOption;
+ newOption = '<option data-idsegment="" data-definition="" >New segment</option>';
+ segmentsDropdown.append(newOption);
+ for(var key in self.segmentList)
+ {
+ segment = self.segmentList[key];
+ newOption = '<option data-idsegment="'+segment.idsegment+'" data-definition=\''+segment.definition+'\' title="'+segment.name+'">'+self.shortenSegmentName(segment.name)+'</option>';
+ segmentsDropdown.append(newOption);
+ }
+ $(html).find(".segment-content > h3").after(getInitialStateRowsHtml()).show();
+ return html;
+ }
+
+ var doListBindings = function()
+ {
+ self.jscroll = self.content.find(".segmentList").jScrollPane({
+ autoReinitialise: true,
+ showArrows:true
+ }).data().jsp;
+
+ self.content.find(".add_new_segment").unbind().on("click", function(event){
+ event.stopPropagation();
+ closeAllOpenLists();
+ addForm();
+ doDragDropBindings();
+ });
+
+ }
+
+ var closeAllOpenLists = function() {
+ $(".segmentationContainer").each(function() {
+ if($(this).hasClass("visible"))
+ $(this).trigger("click");
+ });
+ }
+
+
+ var findAndExplodeByMatch = function(metric){
+ var matches = ["==" , "!=" , "<=", ">=", "=@" , "!@","<",">"];
+ var newMetric = {};
+ var minPos = metric.length;
+ var match, index;
+ var singleChar = false;
+
+ for(var key=0; key < matches.length; key++)
+ {
+ match = matches[key];
+ index = metric.indexOf(match);
+ if( index != -1){
+ if(index < minPos){
+ minPos = index;
+ if(match == ">" || match == "<"){
+ singleChar = true;
+ }
+ }
+ }
+ }
+
+ if(minPos < metric.length){
+ // sth found - explode
+ if(singleChar == true){
+ newMetric.metric = metric.substr(0,minPos);
+ newMetric.match = metric.substr(minPos,1);
+ newMetric.value = metric.substr(minPos+1);
+
+ }
+ else{
+ newMetric.metric = metric.substr(0,minPos);
+ newMetric.match = metric.substr(minPos,2);
+ newMetric.value = metric.substr(minPos+2);
+
+ }
+ // if value is only "" -> change to empty string
+ if(newMetric.value == '""')
+ {
+ newMetric.value = "";
+ }
+ }
+ return newMetric;
+ }
+
+ var parseSegmentStr = function(segmentStr)
+ {
+ var blocks;
+ blocks = segmentStr.split(";");
+ for(var key in blocks){
+ blocks[key] = blocks[key].split(",");
+ for(var innerkey = 0; innerkey < blocks[key].length; innerkey++){
+ blocks[key][innerkey] = findAndExplodeByMatch(blocks[key][innerkey]);
+ }
+ }
+ return blocks;
+ }
+
+ var openEditForm = function(segment){
+ addForm();
+ $(self.form).find(".segment-content > h3 > span").text(segment.name);
+ $(self.form).find('#available_segments_select > option[data-idsegment="'+segment.idsegment+'"]').prop("selected",true);
+ $(self.form).find('#available_segments a.dropList').html(self.shortenSegmentName(segment.name, 16));
+
+ if(segment.definition != ""){
+ revokeInitialStateRows();
+ var blocks = parseSegmentStr(segment.definition);
+ for(var key in blocks){
+ appendComplexRowHtml(blocks[key]);
+ }
+ $(self.form).find(".segment-content").append(getAddNewBlockButtonHtml());
+ }
+ $(self.form).find(".metricList").each( function(){
+ $(this).trigger("change", true);
+ });
+ doDragDropBindings();
+ }
+
+ var bindListEvents = function(){
+ $(self.content).off("click").on("click", function(event){
+ // hide all other modals connected with this widget
+ if(self.content.hasClass("visible")){
+ if($(event.target).hasClass("jspDrag") === true)
+ {
+ event.stopPropagation();
+ }
+ else{
+ self.jscroll.destroy();
+ self.content.removeClass("visible");
+ }
+ }
+ else{
+ // for each visible segmentationContainer -> trigger click event to close and kill scrollpane - very important !
+ closeAllOpenLists();
+ self.content.addClass("visible");
+ doListBindings();
+ }
+ });
+
+ $(self.content).off("click",".editSegment").on("click", ".editSegment", function(e){
+ $(this).parents(".segmentationContainer").trigger("click");
+ var target = $(this).parent("li");
+ var segment = {};
+ segment.idsegment = target.attr("data-idsegment");
+ segment.definition = target.attr("data-definition");
+ segment.name = target.attr("title");
+ openEditForm(segment);
+ e.stopPropagation();
+ e.preventDefault();
+ });
+
+ $(self.content).off("click", ".segmentList li").on("click", ".segmentList li", function(e){
+ if($(e.currentTarget).hasClass("grayed") !== true){
+ var segment = {};
+ segment.idsegment = $(this).attr("data-idsegment");
+ segment.definition = $(this).attr("data-definition");
+ segment.name = $(this).attr("title");
+ self.segmentSelectMethod(segment);
+ setSegment(segment.definition);
+ markCurrentSegment();
+ }
+ });
+ };
+
+ var bindChangeMetricSelectEvent = function()
+ {
+ $(".segment-content").off("change","select.metricList").on("change", "select.metricList", function(e, persist){
+ if(typeof persist === "undefined"){
+ persist = false;
+ }
+ alterMatchesList(this, persist);
+ doDragDropBindings();
+ autoSuggestValues(this, persist);
+ } );
+ }
+
+ var autoSuggestValues = function(select, persist) {
+ var type = $(select).find("option:selected").attr("value");
+ if(!persist) {
+ var parents = $(select).parents('.segment-row');
+ var loadingElement = parents.find(".segment-loading");
+ loadingElement.show();
+ currentValue = parents.find(".metricValueBlock input").val();
+ segmentName = $('option:selected',select).attr('value');
+
+ // Request auto-suggest values
+ var ajaxHandler = new ajaxHelper();
+ ajaxHandler.addParams({
+ module: 'API',
+ format: 'json',
+ method: 'API.getSuggestedValuesForSegment',
+ segmentName: segmentName,
+ idSite: piwik.idSite
+ }, 'GET');
+ ajaxHandler.setCallback(function(response) {
+ loadingElement.hide();
+ console.log("RECEIVED Auto suggested:");
+ console.log(response);
+ console.log("How can we display it as autosuggest below this field?");
+ });
+ ajaxHandler.send(true);
+ }
+ }
+
+ var alterMatchesList = function(select, persist){
+ var oldMatch;
+ var type = $(select).find("option:selected").attr("data-type");
+ var matchSelector = $(select).parents(".segment-input").siblings(".metricMatchBlock").find("select");
+ if(persist === true){
+ oldMatch = matchSelector.find("option:selected").val();
+ }
+ else{
+ oldMatch = "";
+ }
+
+ if(type === "dimension" || type === "metric"){
+ matchSelector.empty();
+ var optionsHtml = "";
+ for(var key in self.availableMatches[type]){
+ optionsHtml += '<option value="'+key+'">'+self.availableMatches[type][key]+'</option>';
+ }
+ }
+
+ matchSelector.append(optionsHtml);
+ matchSelector.val(oldMatch);
+ }
+
+ var getAddNewBlockButtonHtml = function()
+ {
+ if(typeof addNewBlockButton === "undefined")
+ {
+ var addNewBlockButton = $("#SegmentEditor > div.segment-add-row").clone();
+ }
+ return addNewBlockButton.clone();
+
+ }
+
+ var getAddOrBlockButtonHtml = function(){
+ if(typeof addOrBlockButton === "undefined")
+ {
+ var addOrBlockButton = $("#SegmentEditor div.segment-add-or").clone();
+ }
+ return addOrBlockButton.clone();
+ }
+
+ var placeSegmentationFormControls = function(){
+ doDragDropBindings();
+ $(self.form).find(".scrollable").jScrollPane({
+ showArrows: true,
+ autoReinitialise: true,
+ verticalArrowPositions: 'os',
+ horizontalArrowPositions: 'os'
+ });
+ }
+
+ var bindFormEvents = function(){
+
+ $(self.form).on("click", "a", function(e){
+ e.preventDefault();
+ });
+
+ $('#closeSegmentationForm').on("click", function() {
+ self.form.remove();
+ });
+
+ $(self.form).off("click", "a.editSegmentName").on("click", "a.editSegmentName", function(e){
+ var oldName = $(e.currentTarget).parents("h3").find("span").text();
+ $(e.currentTarget).parents("h3").find("span").hide();
+ $(e.currentTarget).hide();
+ $(e.currentTarget).before('<input id="edit_segment_name"/>');
+ $(e.currentTarget).siblings("#edit_segment_name").focus().val(oldName);
+ });
+
+ $(self.form).off("blur", "input#edit_segment_name").on("blur", "input#edit_segment_name", function(e){
+ var newName = $(this).val();
+ $(e.currentTarget).parents("h3").find("span").text(newName).show();
+ $(self.form).find("a.editSegmentName").show();
+ $(this).remove();
+ });
+
+ $(self.form).on("click", '.segment-element', function(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ });
+
+ $(self.form).find("#available_segments_select").bind("change", function(e){
+ var option = $(e.currentTarget).find('option:selected');
+ var segment = {};
+ segment.idsegment = option.attr("data-idsegment");
+ segment.name = option.attr("title");
+ segment.definition = option.attr("data-definition");
+ openEditForm(segment);
+
+ });
+
+ // attach event that shows/hides child elements of each metric category
+ $(self.form).find(".segment-nav > div > ul > li > a").each( function(){
+ $(this).on("click", function(e){
+ $(e.currentTarget).siblings("ul").toggle();
+ });
+ });
+
+ $(self.form).off("click", ".custom_select_search a").on("click", ".custom_select_search a", function(e){
+ $(self.form).find("#segmentSearch").val("").trigger("keyup").val("Search");
+ });
+
+ // attach event that will clear search input upon focus if its content is default
+ $(self.form).find("#segmentSearch").on("focus", function(e){
+ var search = $(e.currentTarget).val();
+ if(search == "Search")
+ $(e.currentTarget).val("");
+ });
+
+
+ // attach event that will set search input value upon blur if its content is not null
+ $(self.form).find("#segmentSearch").on("blur", function(e){
+ var search = $(e.currentTarget).val();
+ if(search == ""){
+ clearSearchMetricHighlight();
+ $(e.currentTarget).val("Search");
+ }
+ });
+
+ // bind search action triggering - only when input text is longer than 2 chars
+ $(self.form).find("#segmentSearch").on("keyup", function(e){
+ var search = $(e.currentTarget).val();
+ if( search.length >= 2)
+ {
+ clearTimeout(self.timer);
+ self.searchAllowed = true;
+ self.timer = setTimeout(function(){
+ searchSegments(search);
+ }, 500);
+ }
+ else{
+ self.searchAllowed = false;
+ clearSearchMetricHighlight();
+ }
+ });
+
+
+ $(self.form).on("click", ".delete", function(){
+
+ var segmentName = $(self.form).find(".segment-content > h3 > span").text();
+ var segmentId = $(self.form).find("#available_segments_select option:selected").attr("data-idsegment")
+ var params = {
+ "idsegment" : segmentId
+ };
+ $('#confirm').find('#name').text( segmentName );
+ if(segmentId != ""){
+ piwikHelper.modalConfirm( '#confirm', {
+ yes: function(){
+ self.deleteMethod(params);
+ }
+ });
+ }
+ });
+
+ $(self.form).on("click", "a.close", function(e){
+ self.form.unbind().remove();
+ });
+
+ $("body").on("keyup", function(e){
+ if(e.keyCode == "27"){
+ $(self.form).remove();
+ }
+ });
+
+ bindChangeMetricSelectEvent();
+
+ placeSegmentationFormControls();
+
+ }
+
+ var doDragDropBindings = function(){
+ $(self.form).find(".segment-nav div > ul > li > ul > li").sortable({
+ cursor: 'move',
+ revert: 10,
+ revertDuration: 0,
+ snap: false,
+ helper: 'clone',
+ appendTo: 'body'
+ });
+
+ $(self.form).find(".metricListBlock").droppable({
+ hoverClass: "hovered",
+ drop: function( event, ui ) {
+ $(this).find("select").val(ui.draggable.parent().attr("data-metric")).trigger("change");
+ }
+ });
+
+ $(self.form).find(".segment-add-row > div").droppable({
+ hoverClass: "hovered",
+ drop: function( event, ui ) {
+ $(this).find("a").trigger("click", [ui.draggable.parent().attr("data-metric")]);
+ if($(this).find("a > span").length == 0){
+ revokeInitialStateRows();
+ appendSpecifiedRowHtml([ui.draggable.parent().attr("data-metric")]);
+ }
+ }
+ });
+
+ $(self.form).find(".segment-add-or > div").droppable({
+ hoverClass: "hovered",
+ drop: function( event, ui ) {
+ $(this).find("a").trigger("click", [ui.draggable.parent().attr("data-metric")]);
+ }
+ });
+
+
+ }
+
+ var searchSegments = function(search){
+ // pre-proces search string to normalized form
+ search = normalizeSearchString(search);
+ // ---
+ // clear all previous search highlights and hide all categories
+ // to allow further showing only matching ones, while others remain invisible
+ clearSearchMetricHighlight();
+ $(self.form).find('.segment-nav div > ul > li').hide();
+
+ // 1 - do most obvious selection -> mark whole categories matching search string
+ // also expand whole category
+ $(self.form).find('.segment-nav div > ul > li[data*="'+search+'"]').each( function(){
+ $(this).addClass("searchFound");
+ $(this).find("ul").show();
+ $(this).find("li").show();
+ $(this).show();
+ });
+
+ // 2 - among all unselected categories find metrics which match and mark parent as search result
+ $(self.form).find(".segment-nav div > ul > li:not(.searchFound)").each(function(){
+ if($(this).find('li[data*="'+search+'"]').length > 0)
+ {
+ $(this).addClass("searchFound");
+ $(this).find('li').hide();
+ $(this).find('li[data*="'+search+'"]').show();
+ $(this).find('ul').show();
+ $(this).show();
+ }
+ });
+
+ if( $(self.form).find("li.searchFound").length == 0)
+ {
+ $(self.form).find("div > ul").prepend('<li class="no_results"><a>No results</a></li>').show();
+
+ }
+ // check if search allow flag was revoked - then clear all search results
+ if(self.searchAllowed == false)
+ {
+ clearSearchMetricHighlight();
+ self.searchAllowed = true;
+ }
+
+ }
+
+ var clearSearchMetricHighlight = function(){
+ $(self.form).find('.no_results').remove();
+ $(self.form).find('.segment-nav div > ul > li').removeClass("searchFound").show();
+ $(self.form).find('.segment-nav div > ul > li').removeClass("others").show();
+ $(self.form).find('.segment-nav div > ul > li > ul > li').show();
+ $(self.form).find('.segment-nav div > ul > li > ul').hide();
+ }
+
+ var normalizeSearchString = function(search){
+ search = search.replace(/^\s+|\s+$/g, ''); // trim
+ search = search.toLowerCase();
+ // remove accents, swap ñ for n, etc
+ var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;";
+ var to = "aaaaeeeeiiiioooouuuunc------";
+ for (var i=0, l=from.length ; i<l ; i++) {
+ search = search.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
+ }
+
+ search = search.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
+ .replace(/\s+/g, '_') // collapse whitespace and replace by underscore
+ .replace(/-+/g, '-'); // collapse dashes
+ return search;
+ }
+
+ var bindSegmentManipulationEvents = function(){
+ // upon clicking - add new segment block, then bind 'x' action to newly added row
+ $(self.form).on("click", ".segment-add-row a", function(event, data){
+ $(self.form).find(".segment-and:last").after(getAndDiv()).after(getMockedFormRow());
+ if(typeof data !== "undefined"){
+ $(self.form).find(".metricList:last").val(data);
+ }
+ $(self.form).find(".metricList:last").trigger('change')
+ doDragDropBindings();
+ });
+
+ $(self.form).on("click", ".segment-add-row span", function(event, data){
+ if(typeof data !== "undefined") {
+ $(self.form).find(".segment-and:last").after(getAndDiv()).after(getMockedFormRow());
+ $(self.form).find(".metricList:last").val(data).trigger('change');
+ doDragDropBindings();
+ }
+ });
+
+ // add new OR block
+ $(self.form).on("click", ".segment-add-or a", function(event, data){
+ $(event.currentTarget).parents(".segment-rows").find(".segment-or:last").after(getOrDiv()).after(getMockedInputRowHtml());
+ if(typeof data !== "undefined"){
+ $(event.currentTarget).parents(".segment-rows").find(".metricList:last").val(data);
+ }
+ $(event.currentTarget).parents(".segment-rows").find(".metricList:last").trigger('change');
+ doDragDropBindings();
+ });
+
+ $(self.form).on("click", ".segment-close", function(e){
+ var target = e.currentTarget;
+ var rowCnt = $(target).parents(".segment-rows").find(".segment-row").length;
+ var globalRowCnt = $(self.form).find(".segment-close").length;
+ if(rowCnt > 1){
+ $(target).parents(".segment-row").next().remove();
+ $(target).parents(".segment-row").remove();
+ }
+ else if(rowCnt == 1){
+ $(target).parents(".segment-rows").next().remove();
+ $(target).parents(".segment-rows").remove();
+ if(globalRowCnt == 1){
+ applyInitialStateModification();
+ }
+ }
+ });
+ }
+
+ var addForm = function(){
+
+ $("#segmentEditorPanel").find(".segment-element:visible").unbind().remove();
+ if(typeof self.form !== "undefined")
+ {
+ self.form.unbind().remove();
+ }
+ // remove any remaining forms
+
+ self.form = getFormHtml();
+ $("#segmentEditorPanel").prepend(self.form);
+
+ setLeftMargin('#segmentEditorPanel > .segment-element');
+ bindFormEvents();
+ bindSegmentManipulationEvents();
+ makeDropList("#enabledAllUsers" , "#enabledAllUsers_select");
+ makeDropList("#visible_to_website" , "#visible_to_website_select");
+ makeDropList("#available_segments" , "#available_segments_select");
+ $(self.form).find(".saveAndApply").bind("click", function(e){
+ e.preventDefault();
+ parseFormAndSave();
+ });
+
+
+ }
+
+ var parseForm = function(){
+ var segmentStr = "";
+ $(self.form).find(".segment-rows").each( function(){
+ var subSegmentStr = "";
+
+ $(this).find(".segment-row").each( function(){
+ if(subSegmentStr != ""){
+ subSegmentStr += ","; // OR operator
+ }
+ $(this).find(".segment-row-inputs").each( function(){
+ var metric = $(this).find(".metricList option:selected").val();
+ var match = $(this).find(".metricMatchBlock > select option:selected").val();
+ var value = $(this).find(".segment-input input").val();
+ /*if(value == ""){
+ value= '';
+ }*/
+ subSegmentStr += metric + match + encodeURIComponent(value);
+ });
+ });
+ if(segmentStr != "")
+ {
+ segmentStr += ";"; // add AND operator between segment blocks
+ }
+ segmentStr += subSegmentStr;
+ });
+ return segmentStr
+ }
+
+ var parseFormAndSave = function(){
+ var segmentName = $(self.form).find(".segment-content > h3 >span").text();
+ var segmentStr = parseForm();
+ var segmentId = $(self.form).find('#available_segments_select > option:selected').attr("data-idsegment");
+ var user = $(self.form).find("#enabledAllUsers_select option:selected").val();
+ var params = {
+ "name": segmentName,
+ "definition": segmentStr,
+ "enabledAllUsers": user,
+ "idSite": $('#visible_to_website option:selected').attr('value')
+ };
+ // determine if save or update should be performed
+ if(segmentId === ""){
+ self.addMethod(params);
+ }
+ else{
+ jQuery.extend(params, {
+ "idSegment": segmentId
+ });
+ self.updateMethod(params);
+ }
+ }
+
+ var makeDropList = function(spanId, selectId){
+ var select = $(self.form).find(selectId).hide();
+ var dropList = $( '<a class="dropList dropdown">' )
+ .insertAfter( select )
+ .text( select.children(':selected').text() )
+ .autocomplete({
+ delay: 0,
+ minLength: 0,
+ appendTo: "body",
+ source: function( request, response ) {
+ response( select.children( "option" ).map(function() {
+ var text = $( this ).text();
+ return {
+ label: text,
+ value: this.value,
+ option: this
+ };
+ }) );
+ },
+ select: function( event, ui ) {
+ ui.item.option.selected = true;
+ if(ui.item.value) {
+ dropList.text(ui.item.label);
+ $(self.form).find(selectId).trigger("change");
+ }
+ }
+ })
+ .click(function() {
+ // close all other droplists made by this form
+ $("a.dropList").autocomplete("close");
+ // close if already visible
+ if ( $(this).autocomplete( "widget" ).is(":visible") ) {
+ $(this).autocomplete("close");
+ return;
+ }
+ // pass empty string as value to search for, displaying all results
+ $(this).autocomplete( "search", "" );
+
+ });
+ $('body').on('mouseup',function(e){
+ if(!$(e.target).parents(spanId).length && !$(e.target).is(spanId) && !$(e.target).parents(spanId).length
+ && !$(e.target).parents(".ui-autocomplete").length && !$(e.target).is(".ui-autocomplete") && !$(e.target).parents(".ui-autocomplete").length
+ ) {
+ dropList.autocomplete("close");
+ }
+ });
+ }
+
+ var setLeftMargin = function(selector) {
+// setTimeout( function() {
+ $(selector).css({left: Math.max($('#periodString')[0].offsetWidth) + 10});
+// }, 500);
+ }
+
+ var initHtml = function() {
+ if(typeof self.content !== "undefined"){
+ self.content.unbind();
+ }
+ var html = getListHtml();
+
+ if(typeof self.content !== "undefined"){
+ self.content.html($(html).html());
+ } else {
+ $("#"+self.targetId).append(html);
+ self.content = $("#"+self.targetId).find(".segmentationContainer");
+ }
+ setLeftMargin('.segmentationContainer');
+
+ // assign content to object attribute to make it easil accesible through all widget methods
+ bindListEvents();
+ markCurrentSegment();
+ }
+ initHtml();
+ };
+
+ return segmentation;
+})(jQuery);
+
+
+$(document).ready( function(){
+ // ie. admin screens
+ if(typeof availableSegments == "undefined") {
+ return;
+ }
+
+ var changeSegment = function(params){
+ $('#segmentEditorPanel a.close').click();
+
+ return broadcast.propagateNewPage('segment=' + params.definition, true, true);
+ };
+
+ var addSegment = function(params){
+ var ajaxHandler = new ajaxHelper();
+ ajaxHandler.setLoadingElement();
+ jQuery.extend(params, {
+ "module": 'API',
+ "format": 'json',
+ "method": 'SegmentEditor.add'
+ });
+ ajaxHandler.addParams(params, 'GET');
+ ajaxHandler.useCallbackInCaseOfError();
+ ajaxHandler.setCallback(function (response) {
+ if (response && response.result == 'error') {
+ alert(response.message);
+ } else {
+ changeSegment(params);
+ }
+ });
+ ajaxHandler.send(true);
+ };
+
+ var updateSegment = function(params){
+ var ajaxHandler = new ajaxHelper();
+ ajaxHandler.setLoadingElement();
+ jQuery.extend(params, {
+ "module": 'API',
+ "format": 'json',
+ "method": 'SegmentEditor.update'
+ });
+ ajaxHandler.addParams(params, 'GET');
+ ajaxHandler.useCallbackInCaseOfError();
+ ajaxHandler.setCallback(function (response) {
+ if (response && response.result == 'error') {
+ alert(response.message);
+ } else {
+ changeSegment(params);
+ }
+ });
+ ajaxHandler.send(true);
+ };
+
+
+ var deleteSegment = function(params){
+ var ajaxHandler = new ajaxHelper();
+ ajaxHandler.addParams({
+ module: 'API',
+ format: 'json',
+ method: 'SegmentEditor.delete'
+ }, 'GET');
+ ajaxHandler.addParams({
+ idSegment: params.idsegment
+ }, 'POST');
+ ajaxHandler.redirectOnSuccess();
+ ajaxHandler.setLoadingElement();
+ ajaxHandler.send(true);
+ };
+
+ var testSegment = function(segmentStr){
+ console.log(segmentStr);
+ }
+
+ var segmentationFtw = new Segmentation({
+ "targetId" : "segmentList",
+ "segmentAccess" : "write",
+ "segmentList" : availableSegments,
+ "addMethod": addSegment,
+ "updateMethod": updateSegment,
+ "deleteMethod": deleteSegment,
+ "segmentSelectMethod": changeSegment,
+ "testSegmentMethod": testSegment,
+ "currentSegmentStr": broadcast.getValueFromHash('segment'),
+ "currentSegmentsGlobal": broadcast.getValueFromHash('segment')
+ });
+}); \ No newline at end of file
diff --git a/plugins/SegmentEditor/templates/jquery.jscrollpane.css b/plugins/SegmentEditor/templates/jquery.jscrollpane.css
new file mode 100644
index 0000000000..4e8b7b4b1e
--- /dev/null
+++ b/plugins/SegmentEditor/templates/jquery.jscrollpane.css
@@ -0,0 +1,120 @@
+/*
+ * CSS Styles that are needed by jScrollPane for it to operate correctly.
+ *
+ * Include this stylesheet in your site or copy and paste the styles below into your stylesheet - jScrollPane
+ * may not operate correctly without them.
+ */
+
+.jspContainer
+{
+ overflow: hidden;
+ position: relative;
+}
+
+.jspPane
+{
+ position: absolute;
+}
+
+.jspVerticalBar
+{
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 8px;
+ height: 100%;
+ background: red;
+}
+
+.jspHorizontalBar
+{
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 16px;
+ background: red;
+}
+
+.jspVerticalBar *,
+.jspHorizontalBar *
+{
+ margin: 0;
+ padding: 0;
+}
+
+.jspCap
+{
+ display: none;
+}
+
+.jspHorizontalBar .jspCap
+{
+ float: left;
+}
+
+.jspTrack
+{
+ background: #dde;
+ position: relative;
+}
+
+.jspDrag
+{
+ background: #bbd;
+ position: relative;
+ top: 0;
+ left: 0;
+ cursor: pointer;
+}
+
+.jspHorizontalBar .jspTrack,
+.jspHorizontalBar .jspDrag
+{
+ float: left;
+ height: 100%;
+}
+
+.jspArrow
+{
+ background: #50506d;
+ text-indent: -20000px;
+ display: block;
+ cursor: pointer;
+}
+
+.jspArrow.jspDisabled
+{
+ cursor: default;
+ background: #80808d;
+}
+
+.jspVerticalBar .jspArrow
+{
+ height: 16px;
+}
+
+.jspHorizontalBar .jspArrow
+{
+ width: 16px;
+ float: left;
+ height: 100%;
+}
+
+.jspVerticalBar .jspArrow:focus
+{
+ outline: none;
+}
+
+.jspCorner
+{
+ background: #eeeef4;
+ float: left;
+ height: 100%;
+}
+
+/* Yuk! CSS Hack for IE6 3 pixel bug :( */
+* html .jspCorner
+{
+ margin: 0 -3px 0 0;
+} \ No newline at end of file
diff --git a/plugins/SegmentEditor/templates/jquery.jscrollpane.js b/plugins/SegmentEditor/templates/jquery.jscrollpane.js
new file mode 100644
index 0000000000..88f190c692
--- /dev/null
+++ b/plugins/SegmentEditor/templates/jquery.jscrollpane.js
@@ -0,0 +1,1342 @@
+/*!
+ * jScrollPane - v2.0.0beta12 - 2012-09-27
+ * http://jscrollpane.kelvinluck.com/
+ *
+ * Copyright (c) 2010 Kelvin Luck
+ * Dual licensed under the MIT or GPL licenses.
+ */
+
+// Script: jScrollPane - cross browser customisable scrollbars
+//
+// *Version: 2.0.0beta12, Last updated: 2012-09-27*
+//
+// Project Home - http://jscrollpane.kelvinluck.com/
+// GitHub - http://github.com/vitch/jScrollPane
+// Source - http://github.com/vitch/jScrollPane/raw/master/script/jquery.jscrollpane.js
+// (Minified) - http://github.com/vitch/jScrollPane/raw/master/script/jquery.jscrollpane.js
+//
+// About: License
+//
+// Copyright (c) 2012 Kelvin Luck
+// Dual licensed under the MIT or GPL Version 2 licenses.
+// http://jscrollpane.kelvinluck.com/MIT-LICENSE.txt
+// http://jscrollpane.kelvinluck.com/GPL-LICENSE.txt
+//
+// About: Examples
+//
+// All examples and demos are available through the jScrollPane example site at:
+// http://jscrollpane.kelvinluck.com/
+//
+// About: Support and Testing
+//
+// This plugin is tested on the browsers below and has been found to work reliably on them. If you run
+// into a problem on one of the supported browsers then please visit the support section on the jScrollPane
+// website (http://jscrollpane.kelvinluck.com/) for more information on getting support. You are also
+// welcome to fork the project on GitHub if you can contribute a fix for a given issue.
+//
+// jQuery Versions - tested in 1.4.2+ - reported to work in 1.3.x
+// Browsers Tested - Firefox 3.6.8, Safari 5, Opera 10.6, Chrome 5.0, IE 6, 7, 8
+//
+// About: Release History
+//
+// 2.0.0beta12 - (2012-09-27) fix for jQuery 1.8+
+// 2.0.0beta11 - (2012-05-14)
+// 2.0.0beta10 - (2011-04-17) cleaner required size calculation, improved keyboard support, stickToBottom/Left, other small fixes
+// 2.0.0beta9 - (2011-01-31) new API methods, bug fixes and correct keyboard support for FF/OSX
+// 2.0.0beta8 - (2011-01-29) touchscreen support, improved keyboard support
+// 2.0.0beta7 - (2011-01-23) scroll speed consistent (thanks Aivo Paas)
+// 2.0.0beta6 - (2010-12-07) scrollToElement horizontal support
+// 2.0.0beta5 - (2010-10-18) jQuery 1.4.3 support, various bug fixes
+// 2.0.0beta4 - (2010-09-17) clickOnTrack support, bug fixes
+// 2.0.0beta3 - (2010-08-27) Horizontal mousewheel, mwheelIntent, keyboard support, bug fixes
+// 2.0.0beta2 - (2010-08-21) Bug fixes
+// 2.0.0beta1 - (2010-08-17) Rewrite to follow modern best practices and enable horizontal scrolling, initially hidden
+// elements and dynamically sized elements.
+// 1.x - (2006-12-31 - 2010-07-31) Initial version, hosted at googlecode, deprecated
+
+(function ($, window, undefined) {
+
+ $.fn.jScrollPane = function (settings) {
+ // JScrollPane "class" - public methods are available through $('selector').data('jsp')
+ function JScrollPane(elem, s) {
+ var settings, jsp = this, pane, paneWidth, paneHeight, container, contentWidth, contentHeight,
+ percentInViewH, percentInViewV, isScrollableV, isScrollableH, verticalDrag, dragMaxY,
+ verticalDragPosition, horizontalDrag, dragMaxX, horizontalDragPosition,
+ verticalBar, verticalTrack, scrollbarWidth, verticalTrackHeight, verticalDragHeight, arrowUp, arrowDown,
+ horizontalBar, horizontalTrack, horizontalTrackWidth, horizontalDragWidth, arrowLeft, arrowRight,
+ reinitialiseInterval, originalPadding, originalPaddingTotalWidth, previousContentWidth,
+ wasAtTop = true, wasAtLeft = true, wasAtBottom = false, wasAtRight = false,
+ originalElement = elem.clone(false, false).empty(),
+ mwEvent = $.fn.mwheelIntent ? 'mwheelIntent.jsp' : 'mousewheel.jsp';
+
+ originalPadding = elem.css('paddingTop') + ' ' +
+ elem.css('paddingRight') + ' ' +
+ elem.css('paddingBottom') + ' ' +
+ elem.css('paddingLeft');
+ originalPaddingTotalWidth = (parseInt(elem.css('paddingLeft'), 10) || 0) +
+ (parseInt(elem.css('paddingRight'), 10) || 0);
+
+ function initialise(s) {
+
+ var /*firstChild, lastChild, */isMaintainingPositon, lastContentX, lastContentY,
+ hasContainingSpaceChanged, originalScrollTop, originalScrollLeft,
+ maintainAtBottom = false, maintainAtRight = false;
+
+ settings = s;
+
+ if (pane === undefined) {
+ originalScrollTop = elem.scrollTop();
+ originalScrollLeft = elem.scrollLeft();
+
+ elem.css(
+ {
+ overflow: 'hidden',
+ padding: 0
+ }
+ );
+ // TODO: Deal with where width/ height is 0 as it probably means the element is hidden and we should
+ // come back to it later and check once it is unhidden...
+ paneWidth = elem.innerWidth();
+ paneHeight = elem.innerHeight();
+
+ elem.width(paneWidth);
+
+ pane = $('<div class="jspPane" />').css('padding', originalPadding).append(elem.children());
+ container = $('<div class="jspContainer" />')
+ .css({
+ 'width': paneWidth + 'px',
+ 'height': paneHeight + 'px'
+ }
+ ).append(pane).appendTo(elem);
+
+ /*
+ // Move any margins from the first and last children up to the container so they can still
+ // collapse with neighbouring elements as they would before jScrollPane
+ firstChild = pane.find(':first-child');
+ lastChild = pane.find(':last-child');
+ elem.css(
+ {
+ 'margin-top': firstChild.css('margin-top'),
+ 'margin-bottom': lastChild.css('margin-bottom')
+ }
+ );
+ firstChild.css('margin-top', 0);
+ lastChild.css('margin-bottom', 0);
+ */
+ } else {
+ elem.css('width', '');
+
+ maintainAtBottom = settings.stickToBottom && isCloseToBottom();
+ maintainAtRight = settings.stickToRight && isCloseToRight();
+
+ hasContainingSpaceChanged = elem.innerWidth() + originalPaddingTotalWidth != paneWidth || elem.outerHeight() != paneHeight;
+
+ if (hasContainingSpaceChanged) {
+ paneWidth = elem.innerWidth() + originalPaddingTotalWidth;
+ paneHeight = elem.innerHeight();
+ container.css({
+ width: paneWidth + 'px',
+ height: paneHeight + 'px'
+ });
+ }
+
+ // If nothing changed since last check...
+ if (!hasContainingSpaceChanged && previousContentWidth == contentWidth && pane.outerHeight() == contentHeight) {
+ elem.width(paneWidth);
+ return;
+ }
+ previousContentWidth = contentWidth;
+
+ pane.css('width', '');
+ elem.width(paneWidth);
+
+ container.find('>.jspVerticalBar,>.jspHorizontalBar').remove().end();
+ }
+
+ pane.css('overflow', 'auto');
+ if (s.contentWidth) {
+ contentWidth = s.contentWidth;
+ } else {
+ contentWidth = pane[0].scrollWidth;
+ }
+ contentHeight = pane[0].scrollHeight;
+ pane.css('overflow', '');
+
+ percentInViewH = contentWidth / paneWidth;
+ percentInViewV = contentHeight / paneHeight;
+ isScrollableV = percentInViewV > 1;
+
+ isScrollableH = percentInViewH > 1;
+
+ //console.log(paneWidth, paneHeight, contentWidth, contentHeight, percentInViewH, percentInViewV, isScrollableH, isScrollableV);
+
+ if (!(isScrollableH || isScrollableV)) {
+ elem.removeClass('jspScrollable');
+ pane.css({
+ top: 0,
+ width: container.width() - originalPaddingTotalWidth
+ });
+ removeMousewheel();
+ removeFocusHandler();
+ removeKeyboardNav();
+ removeClickOnTrack();
+ } else {
+ elem.addClass('jspScrollable');
+
+ isMaintainingPositon = settings.maintainPosition && (verticalDragPosition || horizontalDragPosition);
+ if (isMaintainingPositon) {
+ lastContentX = contentPositionX();
+ lastContentY = contentPositionY();
+ }
+
+ initialiseVerticalScroll();
+ initialiseHorizontalScroll();
+ resizeScrollbars();
+
+ if (isMaintainingPositon) {
+ scrollToX(maintainAtRight ? (contentWidth - paneWidth ) : lastContentX, false);
+ scrollToY(maintainAtBottom ? (contentHeight - paneHeight) : lastContentY, false);
+ }
+
+ initFocusHandler();
+ initMousewheel();
+ initTouch();
+
+ if (settings.enableKeyboardNavigation) {
+ initKeyboardNav();
+ }
+ if (settings.clickOnTrack) {
+ initClickOnTrack();
+ }
+
+ observeHash();
+ if (settings.hijackInternalLinks) {
+ hijackInternalLinks();
+ }
+ }
+
+ if (settings.autoReinitialise && !reinitialiseInterval) {
+ reinitialiseInterval = setInterval(
+ function () {
+ initialise(settings);
+ },
+ settings.autoReinitialiseDelay
+ );
+ } else if (!settings.autoReinitialise && reinitialiseInterval) {
+ clearInterval(reinitialiseInterval);
+ }
+
+ originalScrollTop && elem.scrollTop(0) && scrollToY(originalScrollTop, false);
+ originalScrollLeft && elem.scrollLeft(0) && scrollToX(originalScrollLeft, false);
+
+ elem.trigger('jsp-initialised', [isScrollableH || isScrollableV]);
+ }
+
+ function initialiseVerticalScroll() {
+ if (isScrollableV) {
+
+ container.append(
+ $('<div class="jspVerticalBar" />').append(
+ $('<div class="jspCap jspCapTop" />'),
+ $('<div class="jspTrack" />').append(
+ $('<div class="jspDrag" />').append(
+ $('<div class="jspDragTop" />'),
+ $('<div class="jspDragBottom" />')
+ )
+ ),
+ $('<div class="jspCap jspCapBottom" />')
+ )
+ );
+
+ verticalBar = container.find('>.jspVerticalBar');
+ verticalTrack = verticalBar.find('>.jspTrack');
+ verticalDrag = verticalTrack.find('>.jspDrag');
+
+ if (settings.showArrows) {
+ arrowUp = $('<a class="jspArrow jspArrowUp" />').bind(
+ 'mousedown.jsp', getArrowScroll(0, -1)
+ ).bind('click.jsp', nil);
+ arrowDown = $('<a class="jspArrow jspArrowDown" />').bind(
+ 'mousedown.jsp', getArrowScroll(0, 1)
+ ).bind('click.jsp', nil);
+ if (settings.arrowScrollOnHover) {
+ arrowUp.bind('mouseover.jsp', getArrowScroll(0, -1, arrowUp));
+ arrowDown.bind('mouseover.jsp', getArrowScroll(0, 1, arrowDown));
+ }
+
+ appendArrows(verticalTrack, settings.verticalArrowPositions, arrowUp, arrowDown);
+ }
+
+ verticalTrackHeight = paneHeight;
+ container.find('>.jspVerticalBar>.jspCap:visible,>.jspVerticalBar>.jspArrow').each(
+ function () {
+ verticalTrackHeight -= $(this).outerHeight();
+ }
+ );
+
+
+ verticalDrag.hover(
+ function () {
+ verticalDrag.addClass('jspHover');
+ },
+ function () {
+ verticalDrag.removeClass('jspHover');
+ }
+ ).bind(
+ 'mousedown.jsp',
+ function (e) {
+ // Stop IE from allowing text selection
+ $('html').bind('dragstart.jsp selectstart.jsp', nil);
+
+ verticalDrag.addClass('jspActive');
+
+ var startY = e.pageY - verticalDrag.position().top;
+
+ $('html').bind(
+ 'mousemove.jsp',
+ function (e) {
+ positionDragY(e.pageY - startY, false);
+ }
+ ).bind('mouseup.jsp mouseleave.jsp', cancelDrag);
+ return false;
+ }
+ );
+ sizeVerticalScrollbar();
+ }
+ }
+
+ function sizeVerticalScrollbar() {
+ verticalTrack.height(verticalTrackHeight + 'px');
+ verticalDragPosition = 0;
+ scrollbarWidth = settings.verticalGutter + verticalTrack.outerWidth();
+
+ // Make the pane thinner to allow for the vertical scrollbar
+ pane.width(paneWidth - scrollbarWidth - originalPaddingTotalWidth);
+
+ // Add margin to the left of the pane if scrollbars are on that side (to position
+ // the scrollbar on the left or right set it's left or right property in CSS)
+ try {
+ if (verticalBar.position().left === 0) {
+ pane.css('margin-left', scrollbarWidth + 'px');
+ }
+ } catch (err) {
+ }
+ }
+
+ function initialiseHorizontalScroll() {
+ if (isScrollableH) {
+
+ container.append(
+ $('<div class="jspHorizontalBar" />').append(
+ $('<div class="jspCap jspCapLeft" />'),
+ $('<div class="jspTrack" />').append(
+ $('<div class="jspDrag" />').append(
+ $('<div class="jspDragLeft" />'),
+ $('<div class="jspDragRight" />')
+ )
+ ),
+ $('<div class="jspCap jspCapRight" />')
+ )
+ );
+
+ horizontalBar = container.find('>.jspHorizontalBar');
+ horizontalTrack = horizontalBar.find('>.jspTrack');
+ horizontalDrag = horizontalTrack.find('>.jspDrag');
+
+ if (settings.showArrows) {
+ arrowLeft = $('<a class="jspArrow jspArrowLeft" />').bind(
+ 'mousedown.jsp', getArrowScroll(-1, 0)
+ ).bind('click.jsp', nil);
+ arrowRight = $('<a class="jspArrow jspArrowRight" />').bind(
+ 'mousedown.jsp', getArrowScroll(1, 0)
+ ).bind('click.jsp', nil);
+ if (settings.arrowScrollOnHover) {
+ arrowLeft.bind('mouseover.jsp', getArrowScroll(-1, 0, arrowLeft));
+ arrowRight.bind('mouseover.jsp', getArrowScroll(1, 0, arrowRight));
+ }
+ appendArrows(horizontalTrack, settings.horizontalArrowPositions, arrowLeft, arrowRight);
+ }
+
+ horizontalDrag.hover(
+ function () {
+ horizontalDrag.addClass('jspHover');
+ },
+ function () {
+ horizontalDrag.removeClass('jspHover');
+ }
+ ).bind(
+ 'mousedown.jsp',
+ function (e) {
+ // Stop IE from allowing text selection
+ $('html').bind('dragstart.jsp selectstart.jsp', nil);
+
+ horizontalDrag.addClass('jspActive');
+
+ var startX = e.pageX - horizontalDrag.position().left;
+
+ $('html').bind(
+ 'mousemove.jsp',
+ function (e) {
+ positionDragX(e.pageX - startX, false);
+ }
+ ).bind('mouseup.jsp mouseleave.jsp', cancelDrag);
+ return false;
+ }
+ );
+ horizontalTrackWidth = container.innerWidth();
+ sizeHorizontalScrollbar();
+ }
+ }
+
+ function sizeHorizontalScrollbar() {
+ container.find('>.jspHorizontalBar>.jspCap:visible,>.jspHorizontalBar>.jspArrow').each(
+ function () {
+ horizontalTrackWidth -= $(this).outerWidth();
+ }
+ );
+
+ horizontalTrack.width(horizontalTrackWidth + 'px');
+ horizontalDragPosition = 0;
+ }
+
+ function resizeScrollbars() {
+ if (isScrollableH && isScrollableV) {
+ var horizontalTrackHeight = horizontalTrack.outerHeight(),
+ verticalTrackWidth = verticalTrack.outerWidth();
+ verticalTrackHeight -= horizontalTrackHeight;
+ $(horizontalBar).find('>.jspCap:visible,>.jspArrow').each(
+ function () {
+ horizontalTrackWidth += $(this).outerWidth();
+ }
+ );
+ horizontalTrackWidth -= verticalTrackWidth;
+ paneHeight -= verticalTrackWidth;
+ paneWidth -= horizontalTrackHeight;
+ horizontalTrack.parent().append(
+ $('<div class="jspCorner" />').css('width', horizontalTrackHeight + 'px')
+ );
+ sizeVerticalScrollbar();
+ sizeHorizontalScrollbar();
+ }
+ // reflow content
+ if (isScrollableH) {
+ pane.width((container.outerWidth() - originalPaddingTotalWidth) + 'px');
+ }
+ contentHeight = pane.outerHeight();
+ percentInViewV = contentHeight / paneHeight;
+
+ if (isScrollableH) {
+ horizontalDragWidth = Math.ceil(1 / percentInViewH * horizontalTrackWidth);
+ if (horizontalDragWidth > settings.horizontalDragMaxWidth) {
+ horizontalDragWidth = settings.horizontalDragMaxWidth;
+ } else if (horizontalDragWidth < settings.horizontalDragMinWidth) {
+ horizontalDragWidth = settings.horizontalDragMinWidth;
+ }
+ horizontalDrag.width(horizontalDragWidth + 'px');
+ dragMaxX = horizontalTrackWidth - horizontalDragWidth;
+ _positionDragX(horizontalDragPosition); // To update the state for the arrow buttons
+ }
+ if (isScrollableV) {
+ verticalDragHeight = Math.ceil(1 / percentInViewV * verticalTrackHeight);
+ if (verticalDragHeight > settings.verticalDragMaxHeight) {
+ verticalDragHeight = settings.verticalDragMaxHeight;
+ } else if (verticalDragHeight < settings.verticalDragMinHeight) {
+ verticalDragHeight = settings.verticalDragMinHeight;
+ }
+ verticalDrag.height(verticalDragHeight + 'px');
+ dragMaxY = verticalTrackHeight - verticalDragHeight;
+ _positionDragY(verticalDragPosition); // To update the state for the arrow buttons
+ }
+ }
+
+ function appendArrows(ele, p, a1, a2) {
+ var p1 = "before", p2 = "after", aTemp;
+
+ // Sniff for mac... Is there a better way to determine whether the arrows would naturally appear
+ // at the top or the bottom of the bar?
+ if (p == "os") {
+ p = /Mac/.test(navigator.platform) ? "after" : "split";
+ }
+ if (p == p1) {
+ p2 = p;
+ } else if (p == p2) {
+ p1 = p;
+ aTemp = a1;
+ a1 = a2;
+ a2 = aTemp;
+ }
+
+ ele[p1](a1)[p2](a2);
+ }
+
+ function getArrowScroll(dirX, dirY, ele) {
+ return function () {
+ arrowScroll(dirX, dirY, this, ele);
+ this.blur();
+ return false;
+ };
+ }
+
+ function arrowScroll(dirX, dirY, arrow, ele) {
+ arrow = $(arrow).addClass('jspActive');
+
+ var eve,
+ scrollTimeout,
+ isFirst = true,
+ doScroll = function () {
+ if (dirX !== 0) {
+ jsp.scrollByX(dirX * settings.arrowButtonSpeed);
+ }
+ if (dirY !== 0) {
+ jsp.scrollByY(dirY * settings.arrowButtonSpeed);
+ }
+ scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.arrowRepeatFreq);
+ isFirst = false;
+ };
+
+ doScroll();
+
+ eve = ele ? 'mouseout.jsp' : 'mouseup.jsp';
+ ele = ele || $('html');
+ ele.bind(
+ eve,
+ function () {
+ arrow.removeClass('jspActive');
+ scrollTimeout && clearTimeout(scrollTimeout);
+ scrollTimeout = null;
+ ele.unbind(eve);
+ }
+ );
+ }
+
+ function initClickOnTrack() {
+ removeClickOnTrack();
+ if (isScrollableV) {
+ verticalTrack.bind(
+ 'mousedown.jsp',
+ function (e) {
+ if (e.originalTarget === undefined || e.originalTarget == e.currentTarget) {
+ var clickedTrack = $(this),
+ offset = clickedTrack.offset(),
+ direction = e.pageY - offset.top - verticalDragPosition,
+ scrollTimeout,
+ isFirst = true,
+ doScroll = function () {
+ var offset = clickedTrack.offset(),
+ pos = e.pageY - offset.top - verticalDragHeight / 2,
+ contentDragY = paneHeight * settings.scrollPagePercent,
+ dragY = dragMaxY * contentDragY / (contentHeight - paneHeight);
+ if (direction < 0) {
+ if (verticalDragPosition - dragY > pos) {
+ jsp.scrollByY(-contentDragY);
+ } else {
+ positionDragY(pos);
+ }
+ } else if (direction > 0) {
+ if (verticalDragPosition + dragY < pos) {
+ jsp.scrollByY(contentDragY);
+ } else {
+ positionDragY(pos);
+ }
+ } else {
+ cancelClick();
+ return;
+ }
+ scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.trackClickRepeatFreq);
+ isFirst = false;
+ },
+ cancelClick = function () {
+ scrollTimeout && clearTimeout(scrollTimeout);
+ scrollTimeout = null;
+ $(document).unbind('mouseup.jsp', cancelClick);
+ };
+ doScroll();
+ $(document).bind('mouseup.jsp', cancelClick);
+ return false;
+ }
+ }
+ );
+ }
+
+ if (isScrollableH) {
+ horizontalTrack.bind(
+ 'mousedown.jsp',
+ function (e) {
+ if (e.originalTarget === undefined || e.originalTarget == e.currentTarget) {
+ var clickedTrack = $(this),
+ offset = clickedTrack.offset(),
+ direction = e.pageX - offset.left - horizontalDragPosition,
+ scrollTimeout,
+ isFirst = true,
+ doScroll = function () {
+ var offset = clickedTrack.offset(),
+ pos = e.pageX - offset.left - horizontalDragWidth / 2,
+ contentDragX = paneWidth * settings.scrollPagePercent,
+ dragX = dragMaxX * contentDragX / (contentWidth - paneWidth);
+ if (direction < 0) {
+ if (horizontalDragPosition - dragX > pos) {
+ jsp.scrollByX(-contentDragX);
+ } else {
+ positionDragX(pos);
+ }
+ } else if (direction > 0) {
+ if (horizontalDragPosition + dragX < pos) {
+ jsp.scrollByX(contentDragX);
+ } else {
+ positionDragX(pos);
+ }
+ } else {
+ cancelClick();
+ return;
+ }
+ scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.trackClickRepeatFreq);
+ isFirst = false;
+ },
+ cancelClick = function () {
+ scrollTimeout && clearTimeout(scrollTimeout);
+ scrollTimeout = null;
+ $(document).unbind('mouseup.jsp', cancelClick);
+ };
+ doScroll();
+ $(document).bind('mouseup.jsp', cancelClick);
+ return false;
+ }
+ }
+ );
+ }
+ }
+
+ function removeClickOnTrack() {
+ if (horizontalTrack) {
+ horizontalTrack.unbind('mousedown.jsp');
+ }
+ if (verticalTrack) {
+ verticalTrack.unbind('mousedown.jsp');
+ }
+ }
+
+ function cancelDrag() {
+ $('html').unbind('dragstart.jsp selectstart.jsp mousemove.jsp mouseup.jsp mouseleave.jsp');
+
+ if (verticalDrag) {
+ verticalDrag.removeClass('jspActive');
+ }
+ if (horizontalDrag) {
+ horizontalDrag.removeClass('jspActive');
+ }
+ }
+
+ function positionDragY(destY, animate) {
+ if (!isScrollableV) {
+ return;
+ }
+ if (destY < 0) {
+ destY = 0;
+ } else if (destY > dragMaxY) {
+ destY = dragMaxY;
+ }
+
+ // can't just check if(animate) because false is a valid value that could be passed in...
+ if (animate === undefined) {
+ animate = settings.animateScroll;
+ }
+ if (animate) {
+ jsp.animate(verticalDrag, 'top', destY, _positionDragY);
+ } else {
+ verticalDrag.css('top', destY);
+ _positionDragY(destY);
+ }
+
+ }
+
+ function _positionDragY(destY) {
+ if (destY === undefined) {
+ destY = verticalDrag.position().top;
+ }
+
+ container.scrollTop(0);
+ verticalDragPosition = destY;
+
+ var isAtTop = verticalDragPosition === 0,
+ isAtBottom = verticalDragPosition == dragMaxY,
+ percentScrolled = destY / dragMaxY,
+ destTop = -percentScrolled * (contentHeight - paneHeight);
+
+ if (wasAtTop != isAtTop || wasAtBottom != isAtBottom) {
+ wasAtTop = isAtTop;
+ wasAtBottom = isAtBottom;
+ elem.trigger('jsp-arrow-change', [wasAtTop, wasAtBottom, wasAtLeft, wasAtRight]);
+ }
+
+ updateVerticalArrows(isAtTop, isAtBottom);
+ pane.css('top', destTop);
+ elem.trigger('jsp-scroll-y', [-destTop, isAtTop, isAtBottom]).trigger('scroll');
+ }
+
+ function positionDragX(destX, animate) {
+ if (!isScrollableH) {
+ return;
+ }
+ if (destX < 0) {
+ destX = 0;
+ } else if (destX > dragMaxX) {
+ destX = dragMaxX;
+ }
+
+ if (animate === undefined) {
+ animate = settings.animateScroll;
+ }
+ if (animate) {
+ jsp.animate(horizontalDrag, 'left', destX, _positionDragX);
+ } else {
+ horizontalDrag.css('left', destX);
+ _positionDragX(destX);
+ }
+ }
+
+ function _positionDragX(destX) {
+ if (destX === undefined) {
+ destX = horizontalDrag.position().left;
+ }
+
+ container.scrollTop(0);
+ horizontalDragPosition = destX;
+
+ var isAtLeft = horizontalDragPosition === 0,
+ isAtRight = horizontalDragPosition == dragMaxX,
+ percentScrolled = destX / dragMaxX,
+ destLeft = -percentScrolled * (contentWidth - paneWidth);
+
+ if (wasAtLeft != isAtLeft || wasAtRight != isAtRight) {
+ wasAtLeft = isAtLeft;
+ wasAtRight = isAtRight;
+ elem.trigger('jsp-arrow-change', [wasAtTop, wasAtBottom, wasAtLeft, wasAtRight]);
+ }
+
+ updateHorizontalArrows(isAtLeft, isAtRight);
+ pane.css('left', destLeft);
+ elem.trigger('jsp-scroll-x', [-destLeft, isAtLeft, isAtRight]).trigger('scroll');
+ }
+
+ function updateVerticalArrows(isAtTop, isAtBottom) {
+ if (settings.showArrows) {
+ arrowUp[isAtTop ? 'addClass' : 'removeClass']('jspDisabled');
+ arrowDown[isAtBottom ? 'addClass' : 'removeClass']('jspDisabled');
+ }
+ }
+
+ function updateHorizontalArrows(isAtLeft, isAtRight) {
+ if (settings.showArrows) {
+ arrowLeft[isAtLeft ? 'addClass' : 'removeClass']('jspDisabled');
+ arrowRight[isAtRight ? 'addClass' : 'removeClass']('jspDisabled');
+ }
+ }
+
+ function scrollToY(destY, animate) {
+ var percentScrolled = destY / (contentHeight - paneHeight);
+ positionDragY(percentScrolled * dragMaxY, animate);
+ }
+
+ function scrollToX(destX, animate) {
+ var percentScrolled = destX / (contentWidth - paneWidth);
+ positionDragX(percentScrolled * dragMaxX, animate);
+ }
+
+ function scrollToElement(ele, stickToTop, animate) {
+ var e, eleHeight, eleWidth, eleTop = 0, eleLeft = 0, viewportTop, viewportLeft, maxVisibleEleTop, maxVisibleEleLeft, destY, destX;
+
+ // Legal hash values aren't necessarily legal jQuery selectors so we need to catch any
+ // errors from the lookup...
+ try {
+ e = $(ele);
+ } catch (err) {
+ return;
+ }
+ eleHeight = e.outerHeight();
+ eleWidth = e.outerWidth();
+
+ container.scrollTop(0);
+ container.scrollLeft(0);
+
+ // loop through parents adding the offset top of any elements that are relatively positioned between
+ // the focused element and the jspPane so we can get the true distance from the top
+ // of the focused element to the top of the scrollpane...
+ while (!e.is('.jspPane')) {
+ eleTop += e.position().top;
+ eleLeft += e.position().left;
+ e = e.offsetParent();
+ if (/^body|html$/i.test(e[0].nodeName)) {
+ // we ended up too high in the document structure. Quit!
+ return;
+ }
+ }
+
+ viewportTop = contentPositionY();
+ maxVisibleEleTop = viewportTop + paneHeight;
+ if (eleTop < viewportTop || stickToTop) { // element is above viewport
+ destY = eleTop - settings.verticalGutter;
+ } else if (eleTop + eleHeight > maxVisibleEleTop) { // element is below viewport
+ destY = eleTop - paneHeight + eleHeight + settings.verticalGutter;
+ }
+ if (destY) {
+ scrollToY(destY, animate);
+ }
+
+ viewportLeft = contentPositionX();
+ maxVisibleEleLeft = viewportLeft + paneWidth;
+ if (eleLeft < viewportLeft || stickToTop) { // element is to the left of viewport
+ destX = eleLeft - settings.horizontalGutter;
+ } else if (eleLeft + eleWidth > maxVisibleEleLeft) { // element is to the right viewport
+ destX = eleLeft - paneWidth + eleWidth + settings.horizontalGutter;
+ }
+ if (destX) {
+ scrollToX(destX, animate);
+ }
+
+ }
+
+ function contentPositionX() {
+ return -pane.position().left;
+ }
+
+ function contentPositionY() {
+ return -pane.position().top;
+ }
+
+ function isCloseToBottom() {
+ var scrollableHeight = contentHeight - paneHeight;
+ return (scrollableHeight > 20) && (scrollableHeight - contentPositionY() < 10);
+ }
+
+ function isCloseToRight() {
+ var scrollableWidth = contentWidth - paneWidth;
+ return (scrollableWidth > 20) && (scrollableWidth - contentPositionX() < 10);
+ }
+
+ function initMousewheel() {
+ container.unbind(mwEvent).bind(
+ mwEvent,
+ function (event, delta, deltaX, deltaY) {
+ var dX = horizontalDragPosition, dY = verticalDragPosition;
+ jsp.scrollBy(deltaX * settings.mouseWheelSpeed, -deltaY * settings.mouseWheelSpeed, false);
+ // return true if there was no movement so rest of screen can scroll
+ return dX == horizontalDragPosition && dY == verticalDragPosition;
+ }
+ );
+ }
+
+ function removeMousewheel() {
+ container.unbind(mwEvent);
+ }
+
+ function nil() {
+ return false;
+ }
+
+ function initFocusHandler() {
+ pane.find(':input,a').unbind('focus.jsp').bind(
+ 'focus.jsp',
+ function (e) {
+ scrollToElement(e.target, false);
+ }
+ );
+ }
+
+ function removeFocusHandler() {
+ pane.find(':input,a').unbind('focus.jsp');
+ }
+
+ function initKeyboardNav() {
+ var keyDown, elementHasScrolled, validParents = [];
+ isScrollableH && validParents.push(horizontalBar[0]);
+ isScrollableV && validParents.push(verticalBar[0]);
+
+ // IE also focuses elements that don't have tabindex set.
+ pane.focus(
+ function () {
+ elem.focus();
+ }
+ );
+
+ elem.attr('tabindex', 0)
+ .unbind('keydown.jsp keypress.jsp')
+ .bind(
+ 'keydown.jsp',
+ function (e) {
+ if (e.target !== this && !(validParents.length && $(e.target).closest(validParents).length)) {
+ return;
+ }
+ var dX = horizontalDragPosition, dY = verticalDragPosition;
+ switch (e.keyCode) {
+ case 40: // down
+ case 38: // up
+ case 34: // page down
+ case 32: // space
+ case 33: // page up
+ case 39: // right
+ case 37: // left
+ keyDown = e.keyCode;
+ keyDownHandler();
+ break;
+ case 35: // end
+ scrollToY(contentHeight - paneHeight);
+ keyDown = null;
+ break;
+ case 36: // home
+ scrollToY(0);
+ keyDown = null;
+ break;
+ }
+
+ elementHasScrolled = e.keyCode == keyDown && dX != horizontalDragPosition || dY != verticalDragPosition;
+ return !elementHasScrolled;
+ }
+ ).bind(
+ 'keypress.jsp', // For FF/ OSX so that we can cancel the repeat key presses if the JSP scrolls...
+ function (e) {
+ if (e.keyCode == keyDown) {
+ keyDownHandler();
+ }
+ return !elementHasScrolled;
+ }
+ );
+
+ if (settings.hideFocus) {
+ elem.css('outline', 'none');
+ if ('hideFocus' in container[0]) {
+ elem.attr('hideFocus', true);
+ }
+ } else {
+ elem.css('outline', '');
+ if ('hideFocus' in container[0]) {
+ elem.attr('hideFocus', false);
+ }
+ }
+
+ function keyDownHandler() {
+ var dX = horizontalDragPosition, dY = verticalDragPosition;
+ switch (keyDown) {
+ case 40: // down
+ jsp.scrollByY(settings.keyboardSpeed, false);
+ break;
+ case 38: // up
+ jsp.scrollByY(-settings.keyboardSpeed, false);
+ break;
+ case 34: // page down
+ case 32: // space
+ jsp.scrollByY(paneHeight * settings.scrollPagePercent, false);
+ break;
+ case 33: // page up
+ jsp.scrollByY(-paneHeight * settings.scrollPagePercent, false);
+ break;
+ case 39: // right
+ jsp.scrollByX(settings.keyboardSpeed, false);
+ break;
+ case 37: // left
+ jsp.scrollByX(-settings.keyboardSpeed, false);
+ break;
+ }
+
+ elementHasScrolled = dX != horizontalDragPosition || dY != verticalDragPosition;
+ return elementHasScrolled;
+ }
+ }
+
+ function removeKeyboardNav() {
+ elem.attr('tabindex', '-1')
+ .removeAttr('tabindex')
+ .unbind('keydown.jsp keypress.jsp');
+ }
+
+ function observeHash() {
+ if (location.hash && location.hash.length > 1) {
+ var e,
+ retryInt,
+ hash = escape(location.hash.substr(1)) // hash must be escaped to prevent XSS
+ ;
+ try {
+ e = $('#' + hash + ', a[name="' + hash + '"]');
+ } catch (err) {
+ return;
+ }
+
+ if (e.length && pane.find(hash)) {
+ // nasty workaround but it appears to take a little while before the hash has done its thing
+ // to the rendered page so we just wait until the container's scrollTop has been messed up.
+ if (container.scrollTop() === 0) {
+ retryInt = setInterval(
+ function () {
+ if (container.scrollTop() > 0) {
+ scrollToElement(e, true);
+ $(document).scrollTop(container.position().top);
+ clearInterval(retryInt);
+ }
+ },
+ 50
+ );
+ } else {
+ scrollToElement(e, true);
+ $(document).scrollTop(container.position().top);
+ }
+ }
+ }
+ }
+
+ function hijackInternalLinks() {
+ // only register the link handler once
+ if ($(document.body).data('jspHijack')) {
+ return;
+ }
+
+ // remember that the handler was bound
+ $(document.body).data('jspHijack', true);
+
+ // use live handler to also capture newly created links
+ $(document.body).delegate('a[href*=#]', 'click', function (event) {
+ // does the link point to the same page?
+ // this also takes care of cases with a <base>-Tag or Links not starting with the hash #
+ // e.g. <a href="index.html#test"> when the current url already is index.html
+ var href = this.href.substr(0, this.href.indexOf('#')),
+ locationHref = location.href,
+ hash,
+ element,
+ container,
+ jsp,
+ scrollTop,
+ elementTop;
+ if (location.href.indexOf('#') !== -1) {
+ locationHref = location.href.substr(0, location.href.indexOf('#'));
+ }
+ if (href !== locationHref) {
+ // the link points to another page
+ return;
+ }
+
+ // check if jScrollPane should handle this click event
+ hash = escape(this.href.substr(this.href.indexOf('#') + 1));
+
+ // find the element on the page
+ element;
+ try {
+ element = $('#' + hash + ', a[name="' + hash + '"]');
+ } catch (e) {
+ // hash is not a valid jQuery identifier
+ return;
+ }
+
+ if (!element.length) {
+ // this link does not point to an element on this page
+ return;
+ }
+
+ container = element.closest('.jspScrollable');
+ jsp = container.data('jsp');
+
+ // jsp might be another jsp instance than the one, that bound this event
+ // remember: this event is only bound once for all instances.
+ jsp.scrollToElement(element, true);
+
+ if (container[0].scrollIntoView) {
+ // also scroll to the top of the container (if it is not visible)
+ scrollTop = $(window).scrollTop();
+ elementTop = element.offset().top;
+ if (elementTop < scrollTop || elementTop > scrollTop + $(window).height()) {
+ container[0].scrollIntoView();
+ }
+ }
+
+ // jsp handled this event, prevent the browser default (scrolling :P)
+ event.preventDefault();
+ });
+ }
+
+ // Init touch on iPad, iPhone, iPod, Android
+ function initTouch() {
+ var startX,
+ startY,
+ touchStartX,
+ touchStartY,
+ moved,
+ moving = false;
+
+ container.unbind('touchstart.jsp touchmove.jsp touchend.jsp click.jsp-touchclick').bind(
+ 'touchstart.jsp',
+ function (e) {
+ var touch = e.originalEvent.touches[0];
+ startX = contentPositionX();
+ startY = contentPositionY();
+ touchStartX = touch.pageX;
+ touchStartY = touch.pageY;
+ moved = false;
+ moving = true;
+ }
+ ).bind(
+ 'touchmove.jsp',
+ function (ev) {
+ if (!moving) {
+ return;
+ }
+
+ var touchPos = ev.originalEvent.touches[0],
+ dX = horizontalDragPosition, dY = verticalDragPosition;
+
+ jsp.scrollTo(startX + touchStartX - touchPos.pageX, startY + touchStartY - touchPos.pageY);
+
+ moved = moved || Math.abs(touchStartX - touchPos.pageX) > 5 || Math.abs(touchStartY - touchPos.pageY) > 5;
+
+ // return true if there was no movement so rest of screen can scroll
+ return dX == horizontalDragPosition && dY == verticalDragPosition;
+ }
+ ).bind(
+ 'touchend.jsp',
+ function (e) {
+ moving = false;
+ /*if(moved) {
+ return false;
+ }*/
+ }
+ ).bind(
+ 'click.jsp-touchclick',
+ function (e) {
+ if (moved) {
+ moved = false;
+ return false;
+ }
+ }
+ );
+ }
+
+ function destroy() {
+ var currentY = contentPositionY(),
+ currentX = contentPositionX();
+ elem.removeClass('jspScrollable').unbind('.jsp');
+ elem.replaceWith(originalElement.append(pane.children()));
+ originalElement.scrollTop(currentY);
+ originalElement.scrollLeft(currentX);
+
+ // clear reinitialize timer if active
+ if (reinitialiseInterval) {
+ clearInterval(reinitialiseInterval);
+ }
+ }
+
+ // Public API
+ $.extend(
+ jsp,
+ {
+ // Reinitialises the scroll pane (if it's internal dimensions have changed since the last time it
+ // was initialised). The settings object which is passed in will override any settings from the
+ // previous time it was initialised - if you don't pass any settings then the ones from the previous
+ // initialisation will be used.
+ reinitialise: function (s) {
+ s = $.extend({}, settings, s);
+ initialise(s);
+ },
+ // Scrolls the specified element (a jQuery object, DOM node or jQuery selector string) into view so
+ // that it can be seen within the viewport. If stickToTop is true then the element will appear at
+ // the top of the viewport, if it is false then the viewport will scroll as little as possible to
+ // show the element. You can also specify if you want animation to occur. If you don't provide this
+ // argument then the animateScroll value from the settings object is used instead.
+ scrollToElement: function (ele, stickToTop, animate) {
+ scrollToElement(ele, stickToTop, animate);
+ },
+ // Scrolls the pane so that the specified co-ordinates within the content are at the top left
+ // of the viewport. animate is optional and if not passed then the value of animateScroll from
+ // the settings object this jScrollPane was initialised with is used.
+ scrollTo: function (destX, destY, animate) {
+ scrollToX(destX, animate);
+ scrollToY(destY, animate);
+ },
+ // Scrolls the pane so that the specified co-ordinate within the content is at the left of the
+ // viewport. animate is optional and if not passed then the value of animateScroll from the settings
+ // object this jScrollPane was initialised with is used.
+ scrollToX: function (destX, animate) {
+ scrollToX(destX, animate);
+ },
+ // Scrolls the pane so that the specified co-ordinate within the content is at the top of the
+ // viewport. animate is optional and if not passed then the value of animateScroll from the settings
+ // object this jScrollPane was initialised with is used.
+ scrollToY: function (destY, animate) {
+ scrollToY(destY, animate);
+ },
+ // Scrolls the pane to the specified percentage of its maximum horizontal scroll position. animate
+ // is optional and if not passed then the value of animateScroll from the settings object this
+ // jScrollPane was initialised with is used.
+ scrollToPercentX: function (destPercentX, animate) {
+ scrollToX(destPercentX * (contentWidth - paneWidth), animate);
+ },
+ // Scrolls the pane to the specified percentage of its maximum vertical scroll position. animate
+ // is optional and if not passed then the value of animateScroll from the settings object this
+ // jScrollPane was initialised with is used.
+ scrollToPercentY: function (destPercentY, animate) {
+ scrollToY(destPercentY * (contentHeight - paneHeight), animate);
+ },
+ // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then
+ // the value of animateScroll from the settings object this jScrollPane was initialised with is used.
+ scrollBy: function (deltaX, deltaY, animate) {
+ jsp.scrollByX(deltaX, animate);
+ jsp.scrollByY(deltaY, animate);
+ },
+ // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then
+ // the value of animateScroll from the settings object this jScrollPane was initialised with is used.
+ scrollByX: function (deltaX, animate) {
+ var destX = contentPositionX() + Math[deltaX < 0 ? 'floor' : 'ceil'](deltaX),
+ percentScrolled = destX / (contentWidth - paneWidth);
+ positionDragX(percentScrolled * dragMaxX, animate);
+ },
+ // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then
+ // the value of animateScroll from the settings object this jScrollPane was initialised with is used.
+ scrollByY: function (deltaY, animate) {
+ var destY = contentPositionY() + Math[deltaY < 0 ? 'floor' : 'ceil'](deltaY),
+ percentScrolled = destY / (contentHeight - paneHeight);
+ positionDragY(percentScrolled * dragMaxY, animate);
+ },
+ // Positions the horizontal drag at the specified x position (and updates the viewport to reflect
+ // this). animate is optional and if not passed then the value of animateScroll from the settings
+ // object this jScrollPane was initialised with is used.
+ positionDragX: function (x, animate) {
+ positionDragX(x, animate);
+ },
+ // Positions the vertical drag at the specified y position (and updates the viewport to reflect
+ // this). animate is optional and if not passed then the value of animateScroll from the settings
+ // object this jScrollPane was initialised with is used.
+ positionDragY: function (y, animate) {
+ positionDragY(y, animate);
+ },
+ // This method is called when jScrollPane is trying to animate to a new position. You can override
+ // it if you want to provide advanced animation functionality. It is passed the following arguments:
+ // * ele - the element whose position is being animated
+ // * prop - the property that is being animated
+ // * value - the value it's being animated to
+ // * stepCallback - a function that you must execute each time you update the value of the property
+ // You can use the default implementation (below) as a starting point for your own implementation.
+ animate: function (ele, prop, value, stepCallback) {
+ var params = {};
+ params[prop] = value;
+ ele.animate(
+ params,
+ {
+ 'duration': settings.animateDuration,
+ 'easing': settings.animateEase,
+ 'queue': false,
+ 'step': stepCallback
+ }
+ );
+ },
+ // Returns the current x position of the viewport with regards to the content pane.
+ getContentPositionX: function () {
+ return contentPositionX();
+ },
+ // Returns the current y position of the viewport with regards to the content pane.
+ getContentPositionY: function () {
+ return contentPositionY();
+ },
+ // Returns the width of the content within the scroll pane.
+ getContentWidth: function () {
+ return contentWidth;
+ },
+ // Returns the height of the content within the scroll pane.
+ getContentHeight: function () {
+ return contentHeight;
+ },
+ // Returns the horizontal position of the viewport within the pane content.
+ getPercentScrolledX: function () {
+ return contentPositionX() / (contentWidth - paneWidth);
+ },
+ // Returns the vertical position of the viewport within the pane content.
+ getPercentScrolledY: function () {
+ return contentPositionY() / (contentHeight - paneHeight);
+ },
+ // Returns whether or not this scrollpane has a horizontal scrollbar.
+ getIsScrollableH: function () {
+ return isScrollableH;
+ },
+ // Returns whether or not this scrollpane has a vertical scrollbar.
+ getIsScrollableV: function () {
+ return isScrollableV;
+ },
+ // Gets a reference to the content pane. It is important that you use this method if you want to
+ // edit the content of your jScrollPane as if you access the element directly then you may have some
+ // problems (as your original element has had additional elements for the scrollbars etc added into
+ // it).
+ getContentPane: function () {
+ return pane;
+ },
+ // Scrolls this jScrollPane down as far as it can currently scroll. If animate isn't passed then the
+ // animateScroll value from settings is used instead.
+ scrollToBottom: function (animate) {
+ positionDragY(dragMaxY, animate);
+ },
+ // Hijacks the links on the page which link to content inside the scrollpane. If you have changed
+ // the content of your page (e.g. via AJAX) and want to make sure any new anchor links to the
+ // contents of your scroll pane will work then call this function.
+ hijackInternalLinks: $.noop,
+ // Removes the jScrollPane and returns the page to the state it was in before jScrollPane was
+ // initialised.
+ destroy: function () {
+ destroy();
+ }
+ }
+ );
+
+ initialise(s);
+ }
+
+ // Pluginifying code...
+ settings = $.extend({}, $.fn.jScrollPane.defaults, settings);
+
+ // Apply default speed
+ $.each(['mouseWheelSpeed', 'arrowButtonSpeed', 'trackClickSpeed', 'keyboardSpeed'], function () {
+ settings[this] = settings[this] || settings.speed;
+ });
+
+ return this.each(
+ function () {
+ var elem = $(this), jspApi = elem.data('jsp');
+ if (jspApi) {
+ jspApi.reinitialise(settings);
+ } else {
+ $("script", elem).filter('[type="text/javascript"],:not([type])').remove();
+ jspApi = new JScrollPane(elem, settings);
+ elem.data('jsp', jspApi);
+ }
+ }
+ );
+ };
+
+ $.fn.jScrollPane.defaults = {
+ showArrows: false,
+ maintainPosition: true,
+ stickToBottom: false,
+ stickToRight: false,
+ clickOnTrack: true,
+ autoReinitialise: false,
+ autoReinitialiseDelay: 500,
+ verticalDragMinHeight: 0,
+ verticalDragMaxHeight: 99999,
+ horizontalDragMinWidth: 0,
+ horizontalDragMaxWidth: 99999,
+ contentWidth: undefined,
+ animateScroll: false,
+ animateDuration: 300,
+ animateEase: 'linear',
+ hijackInternalLinks: false,
+ verticalGutter: 4,
+ horizontalGutter: 4,
+ mouseWheelSpeed: 0,
+ arrowButtonSpeed: 0,
+ arrowRepeatFreq: 50,
+ arrowScrollOnHover: false,
+ trackClickSpeed: 0,
+ trackClickRepeatFreq: 70,
+ verticalArrowPositions: 'split',
+ horizontalArrowPositions: 'split',
+ enableKeyboardNavigation: true,
+ hideFocus: false,
+ keyboardSpeed: 0,
+ initialDelay: 300, // Delay before starting repeating
+ speed: 30, // Default speed when others falsey
+ scrollPagePercent: .8 // Percent of visible area scrolled when pageUp/Down or track area pressed
+ };
+
+})(jQuery, this);
diff --git a/plugins/SegmentEditor/templates/jquery.mousewheel.js b/plugins/SegmentEditor/templates/jquery.mousewheel.js
new file mode 100644
index 0000000000..d651ffce1b
--- /dev/null
+++ b/plugins/SegmentEditor/templates/jquery.mousewheel.js
@@ -0,0 +1,84 @@
+/*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net)
+ * Licensed under the MIT License (LICENSE.txt).
+ *
+ * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
+ * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
+ * Thanks to: Seamus Leahy for adding deltaX and deltaY
+ *
+ * Version: 3.0.6
+ *
+ * Requires: 1.2.2+
+ */
+
+(function ($) {
+
+ var types = ['DOMMouseScroll', 'mousewheel'];
+
+ if ($.event.fixHooks) {
+ for (var i = types.length; i;) {
+ $.event.fixHooks[ types[--i] ] = $.event.mouseHooks;
+ }
+ }
+
+ $.event.special.mousewheel = {
+ setup: function () {
+ if (this.addEventListener) {
+ for (var i = types.length; i;) {
+ this.addEventListener(types[--i], handler, false);
+ }
+ } else {
+ this.onmousewheel = handler;
+ }
+ },
+
+ teardown: function () {
+ if (this.removeEventListener) {
+ for (var i = types.length; i;) {
+ this.removeEventListener(types[--i], handler, false);
+ }
+ } else {
+ this.onmousewheel = null;
+ }
+ }
+ };
+
+ $.fn.extend({
+ mousewheel: function (fn) {
+ return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel");
+ },
+
+ unmousewheel: function (fn) {
+ return this.unbind("mousewheel", fn);
+ }
+ });
+
+
+ function handler(event) {
+ var orgEvent = event || window.event, args = [].slice.call(arguments, 1), delta = 0, returnValue = true, deltaX = 0, deltaY = 0;
+ event = $.event.fix(orgEvent);
+ event.type = "mousewheel";
+
+ // Old school scrollwheel delta
+ if (orgEvent.wheelDelta) { delta = orgEvent.wheelDelta / 120; }
+ if (orgEvent.detail) { delta = -orgEvent.detail / 3; }
+
+ // New school multidimensional scroll (touchpads) deltas
+ deltaY = delta;
+
+ // Gecko
+ if (orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS) {
+ deltaY = 0;
+ deltaX = -1 * delta;
+ }
+
+ // Webkit
+ if (orgEvent.wheelDeltaY !== undefined) { deltaY = orgEvent.wheelDeltaY / 120; }
+ if (orgEvent.wheelDeltaX !== undefined) { deltaX = -1 * orgEvent.wheelDeltaX / 120; }
+
+ // Add event and delta to the front of the arguments
+ args.unshift(event, delta, deltaX, deltaY);
+
+ return ($.event.dispatch || $.event.handle).apply(this, args);
+ }
+
+})(jQuery); \ No newline at end of file
diff --git a/plugins/SegmentEditor/templates/selector.tpl b/plugins/SegmentEditor/templates/selector.tpl
new file mode 100644
index 0000000000..1163cb839a
--- /dev/null
+++ b/plugins/SegmentEditor/templates/selector.tpl
@@ -0,0 +1,141 @@
+<div id="SegmentEditor" style="display:none;">
+ <div class="segmentationContainer listHtml">
+ <span class="segmentationTitle"><b>Add segment</b></span>
+ <ul class="submenu">
+ <li> Select a segment of visitors
+ <div class="segmentList">
+ <ul>
+ </ul>
+ </div>
+ </li>
+ </ul>
+ <a class="add_new_segment">Add new segment</a>
+ </div>
+
+ <div class="initial-state-rows">
+ <div class="segment-add-row initial">
+ <div>
+ <span>+ Drag &amp; Drop condition</span>
+ </div>
+ </div>
+ <div class="segment-and">AND</div>
+ <div class="segment-add-row initial">
+ <div>
+ <span>+ Drag &amp; Drop condition</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="segment-row-inputs">
+ <div class="segment-input metricListBlock">
+ <select title="Choose a segment" class="metricList">
+ {foreach from=$segmentsByCategory key=category item=segmentsInCategory}
+ <optgroup label="{$category}">
+ {foreach from=$segmentsInCategory item=segmentInCategory}
+ <option data-type="{$segmentInCategory.type}" value="{$segmentInCategory.segment}">{$segmentInCategory.name}</option>
+ {/foreach}
+ </optgroup>
+ {/foreach}
+ </select>
+ </div>
+ <div class="segment-input metricMatchBlock">
+ <select title="Matches">
+ <option value="==">Equals</option>
+ <option value="!=">Not Equals</option>
+ <option value="&lt;=">At most</option>
+ <option value="&gt;=">At least</option>
+ <option value="&lt;">Less</option>
+ <option value="&gt;">Greater</option>
+ <option value="=@">Contains</option>
+ <option value="!@">Does not contain</option>
+ </select>
+ </div>
+ <div class="segment-input metricValueBlock">
+ <input type="text" title="Value">
+ </div>
+ <div class="clear"></div>
+ </div>
+ <div class="segment-rows">
+ <div class="segment-row">
+ <a href="#" class="segment-close"></a>
+ <a href="#" class="segment-loading"></a>
+ </div>
+ </div>
+ <div class="segment-or">OR</div>
+ <div class="segment-add-or">
+ <div>
+ <a href="#"> + Add <span>OR</span> condition </a>
+ </div>
+ </div>
+ <div class="segment-and">AND</div>
+ <div class="segment-add-row">
+ <div>
+ <a href="#">+ Add <span>AND</span> condition </a>
+ </div>
+ </div>
+ <div style="position: absolute; z-index:999; width:1040px;" class="segment-element">
+ <div class="segment-nav">
+ <h4 class="visits"><span id="available_segments"><strong>
+ <select id="available_segments_select"></select>
+ </strong></span></h4>
+
+ <div class="scrollable">
+ <ul>
+ {foreach from=$segmentsByCategory key=category item=segmentsInCategory}
+ <li data="visit"><a class="metric_category" href="#">{$category}</a>
+ <ul style="display:none">
+ {foreach from=$segmentsInCategory item=segmentInCategory}
+ <li data-metric="{$segmentInCategory.segment}"><a class="ddmetric" href="#">{$segmentInCategory.name}</a></li>
+ {/foreach}
+ </ul>
+ </li>
+ {/foreach}
+ </ul>
+ </div>
+ <div class="custom_select_search">
+ <a href="#"></a>
+ <input type="text" aria-haspopup="true" aria-autocomplete="list" role="textbox" autocomplete="off" class="inp ui-autocomplete-input"
+ id="segmentSearch" value="Search" length="15">
+ </div>
+ </div>
+ <div class="segment-content">
+ {if $isSuperUser}
+ <div class="segment-top">
+ This segment is visible to: <span id="enabledAllUsers"><strong>
+ <select id="enabledAllUsers_select">
+ <option selected="" value="0">me</option>
+ <option value="1">All users</option>
+ </select>
+ </strong></span>
+
+ and displayed for <span id="visible_to_website"><strong>
+ <select id="visible_to_website_select">
+ <option selected="" value="{$idSite}">this website only</option>
+ <option value="0">all websites</option>
+ </select>
+ </strong></span>
+ </div>
+ {/if}
+ <h3>Name: <span>New segment</span> <a class="editSegmentName" href="#">edit</a></h3>
+ </div>
+ <div class="segment-footer">
+ <a class="delete" href="#">Delete</a>
+ <a class="close" href="#">Close</a>
+ <button class="saveAndApply">Save & Apply</button>
+ </div>
+ </div>
+</div>
+
+<span id="segmentEditorPanel">
+ <div id="segmentList"></div>
+</span>
+
+<div class="ui-confirm" id="confirm">
+ <h2>Are you sure you want to delete this segment?</h2>
+ <input role="yes" type="button" value="{'General_Yes'|translate}"/>
+ <input role="no" type="button" value="{'General_No'|translate}"/>
+</div>
+
+<script type="text/javascript">
+ var availableSegments = {$savedSegmentsJson};
+</script>