diff options
author | mattab <matthieu.aubry@gmail.com> | 2013-04-23 10:21:56 +0400 |
---|---|---|
committer | mattab <matthieu.aubry@gmail.com> | 2013-04-23 10:21:56 +0400 |
commit | 7933de115b3fc7e5dc9bc58b7e979cdefb89d16d (patch) | |
tree | cac674c41ed0cd1304de1d6ffa12614513549d6d /plugins/SegmentEditor | |
parent | 1eff94332f296e0fdaa8d34bfc81bf261f1006e9 (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')
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 Binary files differnew file mode 100644 index 0000000000..bc545850ad --- /dev/null +++ b/plugins/SegmentEditor/images/ajax-loader.gif diff --git a/plugins/SegmentEditor/images/bg-inverted-corners.png b/plugins/SegmentEditor/images/bg-inverted-corners.png Binary files differnew file mode 100644 index 0000000000..b4602e230c --- /dev/null +++ b/plugins/SegmentEditor/images/bg-inverted-corners.png diff --git a/plugins/SegmentEditor/images/bg-segment-search.png b/plugins/SegmentEditor/images/bg-segment-search.png Binary files differnew file mode 100644 index 0000000000..3a9a257eec --- /dev/null +++ b/plugins/SegmentEditor/images/bg-segment-search.png diff --git a/plugins/SegmentEditor/images/bg-select.png b/plugins/SegmentEditor/images/bg-select.png Binary files differnew file mode 100644 index 0000000000..a69f9a69c5 --- /dev/null +++ b/plugins/SegmentEditor/images/bg-select.png diff --git a/plugins/SegmentEditor/images/close.png b/plugins/SegmentEditor/images/close.png Binary files differnew file mode 100644 index 0000000000..f2a1380b19 --- /dev/null +++ b/plugins/SegmentEditor/images/close.png diff --git a/plugins/SegmentEditor/images/close_btn.png b/plugins/SegmentEditor/images/close_btn.png Binary files differnew file mode 100644 index 0000000000..6cf122e27a --- /dev/null +++ b/plugins/SegmentEditor/images/close_btn.png diff --git a/plugins/SegmentEditor/images/dashboard_h_bg_hover.png b/plugins/SegmentEditor/images/dashboard_h_bg_hover.png Binary files differnew file mode 100644 index 0000000000..37e4bd54e8 --- /dev/null +++ b/plugins/SegmentEditor/images/dashboard_h_bg_hover.png diff --git a/plugins/SegmentEditor/images/down_arrow.png b/plugins/SegmentEditor/images/down_arrow.png Binary files differnew file mode 100644 index 0000000000..a364892c7d --- /dev/null +++ b/plugins/SegmentEditor/images/down_arrow.png diff --git a/plugins/SegmentEditor/images/icon-users.png b/plugins/SegmentEditor/images/icon-users.png Binary files differnew file mode 100644 index 0000000000..d17d4501be --- /dev/null +++ b/plugins/SegmentEditor/images/icon-users.png diff --git a/plugins/SegmentEditor/images/reset_search.png b/plugins/SegmentEditor/images/reset_search.png Binary files differnew file mode 100644 index 0000000000..cb1d9e80a2 --- /dev/null +++ b/plugins/SegmentEditor/images/reset_search.png diff --git a/plugins/SegmentEditor/images/scroller.png b/plugins/SegmentEditor/images/scroller.png Binary files differnew file mode 100644 index 0000000000..ddcab93426 --- /dev/null +++ b/plugins/SegmentEditor/images/scroller.png diff --git a/plugins/SegmentEditor/images/search_btn.png b/plugins/SegmentEditor/images/search_btn.png Binary files differnew file mode 100644 index 0000000000..34abc4132a --- /dev/null +++ b/plugins/SegmentEditor/images/search_btn.png diff --git a/plugins/SegmentEditor/images/segment-close.png b/plugins/SegmentEditor/images/segment-close.png Binary files differnew file mode 100644 index 0000000000..266e0595a0 --- /dev/null +++ b/plugins/SegmentEditor/images/segment-close.png diff --git a/plugins/SegmentEditor/images/segment-move.png b/plugins/SegmentEditor/images/segment-move.png Binary files differnew file mode 100644 index 0000000000..25ee071db9 --- /dev/null +++ b/plugins/SegmentEditor/images/segment-move.png diff --git a/plugins/SegmentEditor/images/slide.png b/plugins/SegmentEditor/images/slide.png Binary files differnew file mode 100644 index 0000000000..657b9a6de5 --- /dev/null +++ b/plugins/SegmentEditor/images/slide.png diff --git a/plugins/SegmentEditor/images/up_arrow.png b/plugins/SegmentEditor/images/up_arrow.png Binary files differnew file mode 100644 index 0000000000..c7cb24c2b4 --- /dev/null +++ b/plugins/SegmentEditor/images/up_arrow.png 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 & Drop condition</span> + </div> + </div> + <div class="segment-and">AND</div> + <div class="segment-add-row initial"> + <div> + <span>+ Drag & 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="<=">At most</option> + <option value=">=">At least</option> + <option value="<">Less</option> + <option value=">">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> |