diff --git a/boot.php b/boot.php index 0637ff3fea..a070e103ea 100644 --- a/boot.php +++ b/boot.php @@ -33,7 +33,6 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Notify; -use Friendica\Model\Term; use Friendica\Util\BasePath; use Friendica\Util\DateTimeFormat; diff --git a/composer.json b/composer.json index 108ea8473d..20cbe46a77 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,6 @@ "bower-asset/base64": "^1.0", "bower-asset/chart-js": "^2.8", "bower-asset/dompurify": "^1.0", - "bower-asset/perfect-scrollbar": "^0.6", "bower-asset/vue": "^2.6", "npm-asset/es-jquery-sortable": "^0.9.13", "npm-asset/jquery": "^2.0", diff --git a/composer.lock b/composer.lock index 8834f3f701..c04c2e9b1f 100644 --- a/composer.lock +++ b/composer.lock @@ -237,37 +237,6 @@ ], "time": "2019-02-28T15:21:34+00:00" }, - { - "name": "bower-asset/perfect-scrollbar", - "version": "0.6.16", - "source": { - "type": "git", - "url": "https://github.com/utatti/perfect-scrollbar-bower.git", - "reference": "3049129e5dbb403295ce8507a461cdd0f200938c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utatti/perfect-scrollbar-bower/zipball/3049129e5dbb403295ce8507a461cdd0f200938c", - "reference": "3049129e5dbb403295ce8507a461cdd0f200938c", - "shasum": "" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": [ - "css/perfect-scrollbar.css", - "js/perfect-scrollbar.js" - ], - "bower-asset-ignore": [ - "**/.*", - "bower_components" - ] - }, - "license": [ - "MIT" - ], - "description": "Minimalistic but perfect custom scrollbar plugin", - "time": "2017-01-10T01:04:09+00:00" - }, { "name": "bower-asset/vue", "version": "v2.6.10", diff --git a/database.sql b/database.sql index a68f3d4e5a..df3894e8b4 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ -- Friendica 2020.06-dev (Red Hot Poker) --- DB_UPDATE_VERSION 1341 +-- DB_UPDATE_VERSION 1345 -- ------------------------------------------ @@ -666,7 +666,8 @@ CREATE TABLE IF NOT EXISTS `item` ( INDEX `uid_eventid` (`uid`,`event-id`), INDEX `icid` (`icid`), INDEX `iaid` (`iaid`), - INDEX `psid_wall` (`psid`,`wall`) + INDEX `psid_wall` (`psid`,`wall`), + INDEX `uri-id` (`uri-id`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Structure for all posts'; -- @@ -714,24 +715,6 @@ CREATE TABLE IF NOT EXISTS `item-content` ( INDEX `uri-id` (`uri-id`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Content for all posts'; --- --- TABLE item-delivery-data --- -CREATE TABLE IF NOT EXISTS `item-delivery-data` ( - `iid` int unsigned NOT NULL COMMENT 'Item id', - `postopts` text COMMENT 'External post connectors add their network name to this comma-separated string to identify that they should be delivered to these networks during delivery', - `inform` mediumtext COMMENT 'Additional receivers of the linked item', - `queue_count` mediumint NOT NULL DEFAULT 0 COMMENT 'Initial number of delivery recipients, used as item.delivery_queue_count', - `queue_done` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries, used as item.delivery_queue_done', - `queue_failed` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of unsuccessful deliveries, used as item.delivery_queue_failed', - `activitypub` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via ActivityPub', - `dfrn` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via DFRN', - `legacy_dfrn` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via legacy DFRN', - `diaspora` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via Diaspora', - `ostatus` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via OStatus', - PRIMARY KEY(`iid`) -) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Delivery data for items'; - -- -- TABLE item-uri -- @@ -1187,6 +1170,24 @@ CREATE TABLE IF NOT EXISTS `tag` ( INDEX `url` (`url`) ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='tags and mentions'; +-- +-- TABLE post-delivery-data +-- +CREATE TABLE IF NOT EXISTS `post-delivery-data` ( + `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', + `postopts` text COMMENT 'External post connectors add their network name to this comma-separated string to identify that they should be delivered to these networks during delivery', + `inform` mediumtext COMMENT 'Additional receivers of the linked item', + `queue_count` mediumint NOT NULL DEFAULT 0 COMMENT 'Initial number of delivery recipients, used as item.delivery_queue_count', + `queue_done` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries, used as item.delivery_queue_done', + `queue_failed` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of unsuccessful deliveries, used as item.delivery_queue_failed', + `activitypub` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via ActivityPub', + `dfrn` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via DFRN', + `legacy_dfrn` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via legacy DFRN', + `diaspora` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via Diaspora', + `ostatus` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via OStatus', + PRIMARY KEY(`uri-id`) +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Delivery data for items'; + -- -- TABLE post-tag -- @@ -1539,22 +1540,6 @@ CREATE VIEW `owner-view` AS SELECT INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self` INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid`; --- --- VIEW participation-view --- -DROP VIEW IF EXISTS `participation-view`; -CREATE VIEW `participation-view` AS SELECT - `participation`.`iid` AS `iid`, - `contact`.`id` AS `id`, - `contact`.`url` AS `url`, - `contact`.`name` AS `name`, - `contact`.`protocol` AS `protocol`, - CASE `contact`.`batch` WHEN '' THEN `fcontact`.`batch` ELSE `contact`.`batch` END AS `batch`, - CASE `fcontact`.`network` WHEN '' THEN `contact`.`network` ELSE `fcontact`.`network` END AS `network` - FROM `participation` - INNER JOIN `contact` ON `contact`.`id` = `participation`.`cid` AND NOT `contact`.`archive` - INNER JOIN `fcontact` ON `fcontact`.`id` = `participation`.`fid`; - -- -- VIEW pending-view -- @@ -1577,6 +1562,27 @@ CREATE VIEW `pending-view` AS SELECT INNER JOIN `contact` ON `register`.`uid` = `contact`.`uid` INNER JOIN `user` ON `register`.`uid` = `user`.`uid`; +-- +-- VIEW tag-search-view +-- +DROP VIEW IF EXISTS `tag-search-view`; +CREATE VIEW `tag-search-view` AS SELECT + `post-tag`.`uri-id` AS `uri-id`, + `item`.`id` AS `iid`, + `item`.`uri` AS `uri`, + `item`.`guid` AS `guid`, + `item`.`uid` AS `uid`, + `item`.`private` AS `private`, + `item`.`wall` AS `wall`, + `item`.`origin` AS `origin`, + `item`.`gravity` AS `gravity`, + `item`.`received` AS `received`, + `tag`.`name` AS `name` + FROM `post-tag` + INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` + INNER JOIN `item` ON `item`.`uri-id` = `post-tag`.`uri-id` + WHERE `post-tag`.`type` = 1; + -- -- VIEW workerqueue-view -- diff --git a/include/api.php b/include/api.php index 36b08f08f8..fdebdd48bc 100644 --- a/include/api.php +++ b/include/api.php @@ -41,7 +41,6 @@ use Friendica\Model\Item; use Friendica\Model\Mail; use Friendica\Model\Notify; use Friendica\Model\Photo; -use Friendica\Model\Term; use Friendica\Model\User; use Friendica\Model\UserItem; use Friendica\Network\FKOAuth1; @@ -1539,31 +1538,24 @@ function api_search($type) $params = ['order' => ['id' => true], 'limit' => [$start, $count]]; if (preg_match('/^#(\w+)$/', $searchTerm, $matches) === 1 && isset($matches[1])) { $searchTerm = $matches[1]; - $condition = ["`oid` > ? - AND (`uid` = 0 OR (`uid` = ? AND NOT `global`)) - AND `otype` = ? AND `type` = ? AND `term` = ?", - $since_id, local_user(), Term::OBJECT_TYPE_POST, Term::HASHTAG, $searchTerm]; - if ($max_id > 0) { - $condition[0] .= ' AND `oid` <= ?'; - $condition[] = $max_id; + $condition = ["`iid` > ? AND `name` = ? AND (NOT `private` OR (`private` AND `uid` = ?))", $since_id, $searchTerm, local_user()]; + $tags = DBA::select('tag-search-view', ['uri-id'], $condition); + $uriids = []; + while ($tag = DBA::fetch($tags)) { + $uriids[] = $tag['uri-id']; } - $terms = DBA::select('term', ['oid'], $condition, []); - $itemIds = []; - while ($term = DBA::fetch($terms)) { - $itemIds[] = $term['oid']; - } - DBA::close($terms); + DBA::close($tags); - if (empty($itemIds)) { + if (empty($uriids)) { return api_format_data('statuses', $type, $data); } - $preCondition = ['`id` IN (' . implode(', ', $itemIds) . ')']; + $condition = ['uri-id' => $uriids]; if ($exclude_replies) { - $preCondition[] = '`id` = `parent`'; + $condition['gravity'] = GRAVITY_PARENT; } - $condition = [implode(' AND ', $preCondition)]; + $params['group_by'] = ['uri-id']; } else { $condition = ["`id` > ? " . ($exclude_replies ? " AND `id` = `parent` " : ' ') . " @@ -2041,7 +2033,7 @@ function api_statuses_repeat($type) Logger::log('API: api_statuses_repeat: '.$id); - $fields = ['uri-id', 'body', 'title', 'attach', 'tag', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink']; + $fields = ['uri-id', 'body', 'title', 'attach', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink']; $item = Item::selectFirst($fields, ['id' => $id, 'private' => [Item::PUBLIC, Item::UNLISTED]]); if (DBA::isResult($item) && $item['body'] != "") { @@ -2059,7 +2051,6 @@ function api_statuses_repeat($type) $post .= "[/share]"; } $_REQUEST['body'] = $post; - $_REQUEST['tag'] = $item['tag']; $_REQUEST['attach'] = $item['attach']; $_REQUEST['profile_uid'] = api_user(); $_REQUEST['api_source'] = true; diff --git a/include/conversation.php b/include/conversation.php index 43eeb9e41c..a4fe9c00e3 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -22,7 +22,6 @@ use Friendica\App; use Friendica\Content\ContactSelector; use Friendica\Content\Feature; -use Friendica\Content\Pager; use Friendica\Content\Text\BBCode; use Friendica\Core\Hook; use Friendica\Core\Logger; @@ -34,7 +33,7 @@ use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\Profile; -use Friendica\Model\Term; +use Friendica\Model\Tag; use Friendica\Object\Post; use Friendica\Object\Thread; use Friendica\Protocol\Activity; @@ -527,7 +526,7 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o $profile_name = $item['author-link']; } - $tags = Term::populateTagsFromItem($item); + $tags = Tag::populateFromItem($item); $author = ['uid' => 0, 'id' => $item['author-id'], 'network' => $item['author-network'], 'url' => $item['author-link']]; diff --git a/include/enotify.php b/include/enotify.php index 2ae7a99157..ae2e2e7fef 100644 --- a/include/enotify.php +++ b/include/enotify.php @@ -107,12 +107,24 @@ function notification($params) $item_id = 0; } + if (isset($params['item']['uri-id'])) { + $uri_id = $params['item']['uri-id']; + } else { + $uri_id = 0; + } + if (isset($params['parent'])) { $parent_id = $params['parent']; } else { $parent_id = 0; } + if (isset($params['item']['parent-uri-id'])) { + $parent_uri_id = $params['item']['parent-uri-id']; + } else { + $parent_uri_id = 0; + } + $epreamble = ''; $preamble = ''; $subject = ''; @@ -452,17 +464,19 @@ function notification($params) if ($show_in_notification_page) { $notification = DI::notify()->insert([ - 'name' => $params['source_name'] ?? '', - 'name_cache' => substr(strip_tags(BBCode::convert($params['source_name'] ?? '')), 0, 255), - 'url' => $params['source_link'] ?? '', - 'photo' => $params['source_photo'] ?? '', - 'link' => $itemlink ?? '', - 'uid' => $params['uid'] ?? 0, - 'iid' => $item_id ?? 0, - 'parent' => $parent_id ?? 0, - 'type' => $params['type'] ?? '', - 'verb' => $params['verb'] ?? '', - 'otype' => $params['otype'] ?? '', + 'name' => $params['source_name'] ?? '', + 'name_cache' => substr(strip_tags(BBCode::convert($params['source_name'] ?? '')), 0, 255), + 'url' => $params['source_link'] ?? '', + 'photo' => $params['source_photo'] ?? '', + 'link' => $itemlink ?? '', + 'uid' => $params['uid'] ?? 0, + 'iid' => $item_id, + 'uri-id' => $uri_id, + 'parent' => $parent_id, + 'parent-uri-id' => $parent_uri_id, + 'type' => $params['type'] ?? '', + 'verb' => $params['verb'] ?? '', + 'otype' => $params['otype'] ?? '', ]); $notification->msg = Renderer::replaceMacros($epreamble, ['$itemlink' => $notification->link]); @@ -486,8 +500,9 @@ function notification($params) if (!DBA::exists('notify-threads', ['master-parent-item' => $params['parent'], 'receiver-uid' => $params['uid']])) { Logger::log("notify_id:" . intval($notify_id) . ", parent: " . intval($params['parent']) . "uid: " . intval($params['uid']), Logger::DEBUG); - $fields = ['notify-id' => $notify_id, 'master-parent-item' => $params['parent'], - 'receiver-uid' => $params['uid'], 'parent-item' => 0]; + $fields = ['notify-id' => $notify_id, 'master-parent-item' => $params['parent'], + 'master-parent-uri-id' => $parent_uri_id, + 'receiver-uid' => $params['uid'], 'parent-item' => 0]; DBA::insert('notify-threads', $fields); $additional_mail_header .= "Message-ID: <${id_for_parent}>\n"; @@ -574,7 +589,7 @@ function check_user_notification($itemid) { * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ function check_item_notification($itemid, $uid, $notification_type) { - $fields = ['id', 'mention', 'tag', 'parent', 'title', 'body', + $fields = ['id', 'uri-id', 'mention', 'parent', 'parent-uri-id', 'title', 'body', 'author-link', 'author-name', 'author-avatar', 'author-id', 'guid', 'parent-uri', 'uri', 'contact-id', 'network']; $condition = ['id' => $itemid, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'deleted' => false]; diff --git a/include/items.php b/include/items.php index 582fbb0933..38f4a58fba 100644 --- a/include/items.php +++ b/include/items.php @@ -141,28 +141,6 @@ function query_page_info($url, $photo = "", $keywords = false, $keyword_blacklis return $data; } -function add_page_keywords($url, $photo = "", $keywords = false, $keyword_blacklist = "") -{ - $data = query_page_info($url, $photo, $keywords, $keyword_blacklist); - if (empty($data["keywords"]) || !is_array($data["keywords"])) { - return ''; - } - - $tags = ""; - foreach ($data["keywords"] as $keyword) { - $hashtag = str_replace([" ", "+", "/", ".", "#", "'"], - ["", "", "", "", "", ""], $keyword); - - if ($tags != "") { - $tags .= ", "; - } - - $tags .= "#[url=" . DI::baseUrl() . "/search?tag=" . $hashtag . "]" . $hashtag . "[/url]"; - } - - return $tags; -} - function get_page_keywords($url, $photo = "", $keywords = false, $keyword_blacklist = "") { $data = query_page_info($url, $photo, $keywords, $keyword_blacklist); diff --git a/mod/item.php b/mod/item.php index 5785954bce..10ad3ff05b 100644 --- a/mod/item.php +++ b/mod/item.php @@ -29,7 +29,6 @@ */ use Friendica\App; -use Friendica\Content\Pager; use Friendica\Content\Text\BBCode; use Friendica\Core\Hook; use Friendica\Core\Logger; @@ -47,7 +46,6 @@ use Friendica\Model\Item; use Friendica\Model\Notify\Type; use Friendica\Model\Photo; use Friendica\Model\Tag; -use Friendica\Model\Term; use Friendica\Network\HTTPException; use Friendica\Object\EMail\ItemCCEMail; use Friendica\Protocol\Activity; @@ -102,7 +100,7 @@ function item_post(App $a) { $toplevel_item_id = intval($_REQUEST['parent'] ?? 0); $thr_parent_uri = trim($_REQUEST['parent_uri'] ?? ''); - $thread_parent_id = 0; + $thread_parent_uriid = 0; $thread_parent_contact = null; $toplevel_item = null; @@ -124,7 +122,7 @@ function item_post(App $a) { // if this isn't the top-level parent of the conversation, find it if (DBA::isResult($toplevel_item)) { // The URI and the contact is taken from the direct parent which needn't to be the top parent - $thread_parent_id = $toplevel_item['id']; + $thread_parent_uriid = $toplevel_item['uri-id']; $thr_parent_uri = $toplevel_item['uri']; $thread_parent_contact = Contact::getDetailsByURL($toplevel_item["author-link"]); @@ -377,13 +375,12 @@ function item_post(App $a) { } // Look for any tags and linkify them - $str_tags = ''; $inform = ''; $tags = BBCode::getTags($body); - if ($thread_parent_id && !\Friendica\Content\Feature::isEnabled($uid, 'explicit_mentions')) { - $tags = item_add_implicit_mentions($tags, $thread_parent_contact, $thread_parent_id); + if ($thread_parent_uriid && !\Friendica\Content\Feature::isEnabled($uid, 'explicit_mentions')) { + $tags = item_add_implicit_mentions($tags, $thread_parent_contact, $thread_parent_uriid); } $tagged = []; @@ -396,7 +393,7 @@ function item_post(App $a) { foreach ($tags as $tag) { $tag_type = substr($tag, 0, 1); - if ($tag_type == Term::TAG_CHARACTER[Term::HASHTAG]) { + if ($tag_type == Tag::TAG_CHARACTER[Tag::HASHTAG]) { continue; } @@ -416,14 +413,14 @@ function item_post(App $a) { continue; } - $success = handle_tag($body, $inform, $str_tags, local_user() ? local_user() : $profile_uid, $tag, $network); + $success = handle_tag($body, $inform, local_user() ? local_user() : $profile_uid, $tag, $network); if ($success['replaced']) { $tagged[] = $tag; } // When the forum is private or the forum is addressed with a "!" make the post private - if (is_array($success['contact']) && (!empty($success['contact']['prv']) || ($tag_type == Term::TAG_CHARACTER[Term::EXCLUSIVE_MENTION]))) { + if (is_array($success['contact']) && (!empty($success['contact']['prv']) || ($tag_type == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]))) { $private_forum = $success['contact']['prv']; - $only_to_forum = ($tag_type == Term::TAG_CHARACTER[Term::EXCLUSIVE_MENTION]); + $only_to_forum = ($tag_type == Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]); $private_id = $success['contact']['id']; $forum_contact = $success['contact']; } elseif (is_array($success['contact']) && !empty($success['contact']['forum']) && @@ -600,7 +597,6 @@ function item_post(App $a) { $datarray['app'] = $app; $datarray['location'] = $location; $datarray['coord'] = $coord; - $datarray['tag'] = $str_tags; $datarray['file'] = $categories; $datarray['inform'] = $inform; $datarray['verb'] = $verb; @@ -697,7 +693,6 @@ function item_post(App $a) { $fields = [ 'title' => $datarray['title'], 'body' => $datarray['body'], - 'tag' => $datarray['tag'], 'attach' => $datarray['attach'], 'file' => $datarray['file'], 'rendered-html' => $datarray['rendered-html'], @@ -892,7 +887,6 @@ function item_content(App $a) * @param App $a * @param string $body the text to replace the tag in * @param string $inform a comma-seperated string containing everybody to inform - * @param string $str_tags string to add the tag to * @param integer $profile_uid * @param string $tag the tag to replace * @param string $network The network of the post @@ -901,24 +895,15 @@ function item_content(App $a) * @throws ImagickException * @throws HTTPException\InternalServerErrorException */ -function handle_tag(&$body, &$inform, &$str_tags, $profile_uid, $tag, $network = "") +function handle_tag(&$body, &$inform, $profile_uid, $tag, $network = "") { $replaced = false; - $r = null; //is it a person tag? - if (Term::isType($tag, Term::MENTION, Term::IMPLICIT_MENTION, Term::EXCLUSIVE_MENTION)) { + if (Tag::isType($tag, Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION)) { $tag_type = substr($tag, 0, 1); //is it already replaced? if (strpos($tag, '[url=')) { - //append tag to str_tags - if (!stristr($str_tags, $tag)) { - if (strlen($str_tags)) { - $str_tags .= ','; - } - $str_tags .= $tag; - } - // Checking for the alias that is used for OStatus $pattern = "/[@!]\[url\=(.*?)\](.*?)\[\/url\]/ism"; if (preg_match($pattern, $tag, $matches)) { @@ -926,14 +911,6 @@ function handle_tag(&$body, &$inform, &$str_tags, $profile_uid, $tag, $network = if ($data["alias"] != "") { $newtag = '@[url=' . $data["alias"] . ']' . $data["nick"] . '[/url]'; - - if (!stripos($str_tags, '[url=' . $data["alias"] . ']')) { - if (strlen($str_tags)) { - $str_tags .= ','; - } - - $str_tags .= $newtag; - } } } @@ -1007,7 +984,6 @@ function handle_tag(&$body, &$inform, &$str_tags, $profile_uid, $tag, $network = } $profile = $contact["url"]; - $alias = $contact["alias"]; $newname = ($contact["name"] ?? '') ?: $contact["nick"]; } @@ -1018,39 +994,18 @@ function handle_tag(&$body, &$inform, &$str_tags, $profile_uid, $tag, $network = $profile = str_replace(',', '%2c', $profile); $newtag = $tag_type.'[url=' . $profile . ']' . $newname . '[/url]'; $body = str_replace($tag_type . $name, $newtag, $body); - // append tag to str_tags - if (!stristr($str_tags, $newtag)) { - if (strlen($str_tags)) { - $str_tags .= ','; - } - $str_tags .= $newtag; - } - - /* - * Status.Net seems to require the numeric ID URL in a mention if the person isn't - * subscribed to you. But the nickname URL is OK if they are. Grrr. We'll tag both. - */ - if (!empty($alias)) { - $newtag = '@[url=' . $alias . ']' . $newname . '[/url]'; - if (!stripos($str_tags, '[url=' . $alias . ']')) { - if (strlen($str_tags)) { - $str_tags .= ','; - } - $str_tags .= $newtag; - } - } } } return ['replaced' => $replaced, 'contact' => $contact]; } -function item_add_implicit_mentions(array $tags, array $thread_parent_contact, $thread_parent_id) +function item_add_implicit_mentions(array $tags, array $thread_parent_contact, $thread_parent_uriid) { if (DI::config()->get('system', 'disable_implicit_mentions')) { // Add a tag if the parent contact is from ActivityPub or OStatus (This will notify them) if (in_array($thread_parent_contact['network'], [Protocol::OSTATUS, Protocol::ACTIVITYPUB])) { - $contact = Term::TAG_CHARACTER[Term::MENTION] . '[url=' . $thread_parent_contact['url'] . ']' . $thread_parent_contact['nick'] . '[/url]'; + $contact = Tag::TAG_CHARACTER[Tag::MENTION] . '[url=' . $thread_parent_contact['url'] . ']' . $thread_parent_contact['nick'] . '[/url]'; if (!stripos(implode($tags), '[url=' . $thread_parent_contact['url'] . ']')) { $tags[] = $contact; } @@ -1060,15 +1015,15 @@ function item_add_implicit_mentions(array $tags, array $thread_parent_contact, $ $thread_parent_contact['url'] => $thread_parent_contact['nick'] ]; - $parent_terms = Term::tagArrayFromItemId($thread_parent_id, [Term::MENTION, Term::IMPLICIT_MENTION]); + $parent_terms = Tag::getByURIId($thread_parent_uriid, [Tag::MENTION, Tag::IMPLICIT_MENTION]); foreach ($parent_terms as $parent_term) { - $implicit_mentions[$parent_term['url']] = $parent_term['term']; + $implicit_mentions[$parent_term['url']] = $parent_term['name']; } foreach ($implicit_mentions as $url => $label) { if ($url != \Friendica\Model\Profile::getMyURL() && !stripos(implode($tags), '[url=' . $url . ']')) { - $tags[] = Term::TAG_CHARACTER[Term::IMPLICIT_MENTION] . '[url=' . $url . ']' . $label . '[/url]'; + $tags[] = Tag::TAG_CHARACTER[Tag::IMPLICIT_MENTION] . '[url=' . $url . ']' . $label . '[/url]'; } } } diff --git a/mod/network.php b/mod/network.php index 6c02c4f843..9ec8c95b92 100644 --- a/mod/network.php +++ b/mod/network.php @@ -787,14 +787,13 @@ function networkThreadedView(App $a, $update, $parent) } $items = DBA::p("SELECT `item`.`parent-uri` AS `uri`, 0 AS `item_id`, `item`.$ordering AS `order_date`, `author`.`url` AS `author-link` FROM `item` - STRAIGHT_JOIN (SELECT `oid` FROM `term` WHERE `term` IN - (SELECT SUBSTR(`term`, 2) FROM `search` WHERE `uid` = ? AND `term` LIKE '#%') AND `otype` = ? AND `type` = ? AND `uid` = 0) AS `term` - ON `item`.`id` = `term`.`oid` + STRAIGHT_JOIN (SELECT `uri-id` FROM `tag-search-view` WHERE `name` IN + (SELECT SUBSTR(`term`, 2) FROM `search` WHERE `uid` = ? AND `term` LIKE '#%') AND `uid` = 0) AS `tag-search` + ON `item`.`uri-id` = `tag-search`.`uri-id` STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `item`.`author-id` WHERE `item`.`uid` = 0 AND `item`.$ordering < ? AND `item`.$ordering > ? AND `item`.`gravity` = ? AND NOT `author`.`hidden` AND NOT `author`.`blocked`" . $sql_tag_nets, - local_user(), Term::OBJECT_TYPE_POST, Term::HASHTAG, - $top_limit, $bottom_limit, GRAVITY_PARENT); + local_user(), $top_limit, $bottom_limit, GRAVITY_PARENT); $data = DBA::toArray($items); diff --git a/mod/tagger.php b/mod/tagger.php index b3ba472eab..86a6ff69f3 100644 --- a/mod/tagger.php +++ b/mod/tagger.php @@ -29,7 +29,6 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Item; use Friendica\Model\Tag; -use Friendica\Model\Term; use Friendica\Protocol\Activity; use Friendica\Util\Strings; use Friendica\Util\XML; @@ -170,50 +169,8 @@ EOT; Item::update(['visible' => true], ['id' => $item['id']]); } - $term_objtype = ($item['resource-id'] ? Term::OBJECT_TYPE_PHOTO : Term::OBJECT_TYPE_POST); - Tag::store($item['uri-id'], Tag::HASHTAG, $term); - $t = q("SELECT count(tid) as tcount FROM term WHERE oid=%d AND term='%s'", - intval($item['id']), - DBA::escape($term) - ); - - if (!$blocktags && $t[0]['tcount'] == 0) { - q("INSERT INTO term (oid, otype, type, term, url, uid) VALUE (%d, %d, %d, '%s', '%s', %d)", - intval($item['id']), - $term_objtype, - Term::HASHTAG, - DBA::escape($term), - '', - intval($owner_uid) - ); - } - - // if the original post is on this site, update it. - $original_item = Item::selectFirst(['tag', 'id', 'uid'], ['origin' => true, 'uri' => $item['uri']]); - if (DBA::isResult($original_item)) { - $x = q("SELECT `blocktags` FROM `user` WHERE `uid`=%d LIMIT 1", - intval($original_item['uid']) - ); - $t = q("SELECT COUNT(`tid`) AS `tcount` FROM `term` WHERE `oid`=%d AND `term`='%s'", - intval($original_item['id']), - DBA::escape($term) - ); - - if (DBA::isResult($x) && !$x[0]['blocktags'] && $t[0]['tcount'] == 0){ - q("INSERT INTO term (`oid`, `otype`, `type`, `term`, `url`, `uid`) VALUE (%d, %d, %d, '%s', '%s', %d)", - intval($original_item['id']), - $term_objtype, - Term::HASHTAG, - DBA::escape($term), - '', - intval($owner_uid) - ); - } - } - - $arr['id'] = $post_id; Hook::callAll('post_local_end', $arr); diff --git a/mod/tagrm.php b/mod/tagrm.php index 51000c9854..7e8ae8524b 100644 --- a/mod/tagrm.php +++ b/mod/tagrm.php @@ -25,7 +25,6 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Item; use Friendica\Model\Tag; -use Friendica\Model\Term; use Friendica\Util\Strings; function tagrm_post(App $a) @@ -58,35 +57,24 @@ function tagrm_post(App $a) * @param $tags array * @throws Exception */ -function update_tags($item_id, $tags){ - if (empty($item_id) || empty($tags)){ +function update_tags($item_id, $tags) +{ + if (empty($item_id) || empty($tags)) { return; } - $item = Item::selectFirst(['tag', 'uri-id'], ['id' => $item_id, 'uid' => local_user()]); + $item = Item::selectFirst(['uri-id'], ['id' => $item_id, 'uid' => local_user()]); if (!DBA::isResult($item)) { return; } - $old_tags = explode(',', $item['tag']); - foreach ($tags as $new_tag) { if (preg_match_all('/([#@!])\[url\=([^\[\]]*)\]([^\[\]]*)\[\/url\]/ism', $new_tag, $results, PREG_SET_ORDER)) { foreach ($results as $tag) { Tag::removeByHash($item['uri-id'], $tag[1], $tag[3], $tag[2]); } } - - foreach ($old_tags as $index => $old_tag) { - if (strcmp($old_tag, $new_tag) == 0) { - unset($old_tags[$index]); - break; - } - } } - - $tag_str = implode(',', $old_tags); - Term::insertFromTagFieldByItemId($item_id, $tag_str); } function tagrm_content(App $a) diff --git a/src/Content/Widget/TagCloud.php b/src/Content/Widget/TagCloud.php index 5bdf7d8346..109940a126 100644 --- a/src/Content/Widget/TagCloud.php +++ b/src/Content/Widget/TagCloud.php @@ -25,7 +25,7 @@ use Friendica\Core\Renderer; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Item; -use Friendica\Model\Term; +use Friendica\Model\Tag; /** * TagCloud widget @@ -46,7 +46,7 @@ class TagCloud * @return string HTML formatted output. * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function getHTML($uid, $count = 0, $owner_id = 0, $flags = '', $type = Term::HASHTAG) + public static function getHTML($uid, $count = 0, $owner_id = 0, $flags = '', $type = Tag::HASHTAG) { $o = ''; $r = self::tagadelic($uid, $count, $owner_id, $flags, $type); @@ -85,7 +85,7 @@ class TagCloud * @return array Alphabetical sorted array of used tags of an user. * @throws \Exception */ - private static function tagadelic($uid, $count = 0, $owner_id = 0, $flags = '', $type = Term::HASHTAG) + private static function tagadelic($uid, $count = 0, $owner_id = 0, $flags = '', $type = Tag::HASHTAG) { $sql_options = Item::getPermissionsSQLByUserId($uid); $limit = $count ? sprintf('LIMIT %d', intval($count)) : ''; @@ -101,16 +101,13 @@ class TagCloud } // Fetch tags - $tag_stmt = DBA::p("SELECT `term`, COUNT(`term`) AS `total` FROM `term` - LEFT JOIN `item` ON `term`.`oid` = `item`.`id` - WHERE `term`.`uid` = ? AND `term`.`type` = ? - AND `term`.`otype` = ? + $tag_stmt = DBA::p("SELECT `name`, COUNT(`name`) AS `total` FROM `tag-search-view` + LEFT JOIN `item` ON `tag-search-view`.`uri-id` = `item`.`uri-id` + WHERE `tag-search-view`.`uid` = ? AND `item`.`visible` AND NOT `item`.`deleted` AND NOT `item`.`moderated` $sql_options - GROUP BY `term` ORDER BY `total` DESC $limit", - $uid, - $type, - Term::OBJECT_TYPE_POST + GROUP BY `name` ORDER BY `total` DESC $limit", + $uid ); if (!DBA::isResult($tag_stmt)) { return []; @@ -139,7 +136,7 @@ class TagCloud } foreach ($arr as $rr) { - $tags[$x][0] = $rr['term']; + $tags[$x][0] = $rr['name']; $tags[$x][1] = log($rr['total']); $tags[$x][2] = 0; $min = min($min, $tags[$x][1]); diff --git a/src/Content/Widget/TrendingTags.php b/src/Content/Widget/TrendingTags.php index 9f935e6de6..9c24d1549c 100644 --- a/src/Content/Widget/TrendingTags.php +++ b/src/Content/Widget/TrendingTags.php @@ -23,7 +23,7 @@ namespace Friendica\Content\Widget; use Friendica\Core\Renderer; use Friendica\DI; -use Friendica\Model\Term; +use Friendica\Model\Tag; /** * Trending tags aside widget for the community pages, handles both local and global scopes @@ -41,9 +41,9 @@ class TrendingTags public static function getHTML($content = 'global', int $period = 24) { if ($content == 'local') { - $tags = Term::getLocalTrendingHashtags($period, 20); + $tags = Tag::getLocalTrendingHashtags($period, 20); } else { - $tags = Term::getGlobalTrendingHashtags($period, 20); + $tags = Tag::getGlobalTrendingHashtags($period, 20); } $tpl = Renderer::getMarkupTemplate('widget/trending_tags.tpl'); diff --git a/src/Database/PostUpdate.php b/src/Database/PostUpdate.php index cd7e8b9462..bb3b11160b 100644 --- a/src/Database/PostUpdate.php +++ b/src/Database/PostUpdate.php @@ -28,7 +28,9 @@ use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\ItemURI; use Friendica\Model\PermissionSet; +use Friendica\Model\Tag; use Friendica\Model\UserItem; +use Friendica\Util\Strings; /** * These database-intensive post update routines are meant to be executed in the background by the cronjob. @@ -64,6 +66,15 @@ class PostUpdate if (!self::update1329()) { return false; } + if (!self::update1341()) { + return false; + } + if (!self::update1342()) { + return false; + } + if (!self::update1345()) { + return false; + } return true; } @@ -533,4 +544,181 @@ class PostUpdate return false; } + + /** + * Fill the "tag" table with tags and mentions from the body + * + * @return bool "true" when the job is done + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + private static function update1341() + { + // Was the script completed? + if (DI::config()->get('system', 'post_update_version') >= 1341) { + return true; + } + + $id = DI::config()->get('system', 'post_update_version_1341_id', 0); + + Logger::info('Start', ['item' => $id]); + + $rows = 0; + + $items = DBA::p("SELECT `uri-id`,`body` FROM `item-content` WHERE + (`body` LIKE ? OR `body` LIKE ? OR `body` LIKE ?) AND `uri-id` >= ? + ORDER BY `uri-id` LIMIT 100000", '%#%', '%@%', '%!%', $id); + + if (DBA::errorNo() != 0) { + Logger::error('Database error', ['no' => DBA::errorNo(), 'message' => DBA::errorMessage()]); + return false; + } + + while ($item = DBA::fetch($items)) { + Tag::storeFromBody($item['uri-id'], $item['body'], '#!@', false); + $id = $item['uri-id']; + ++$rows; + if ($rows % 1000 == 0) { + DI::config()->set('system', 'post_update_version_1341_id', $id); + } + } + DBA::close($items); + + DI::config()->set('system', 'post_update_version_1341_id', $id); + + Logger::info('Processed', ['rows' => $rows, 'last' => $id]); + + // When there are less than 1,000 items processed this means that we reached the end + // The other entries will then be processed with the regular functionality + if ($rows < 1000) { + DI::config()->set('system', 'post_update_version', 1341); + Logger::info('Done'); + return true; + } + + return false; + } + + /** + * Fill the "tag" table with tags and mentions from the "term" table + * + * @return bool "true" when the job is done + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + private static function update1342() + { + // Was the script completed? + if (DI::config()->get('system', 'post_update_version') >= 1342) { + return true; + } + + $id = DI::config()->get('system', 'post_update_version_1342_id', 0); + + Logger::info('Start', ['item' => $id]); + + $rows = 0; + + $terms = DBA::p("SELECT `term`.`tid`, `item`.`uri-id`, `term`.`type`, `term`.`term`, `term`.`url`, `item-content`.`body` + FROM `term` + INNER JOIN `item` ON `item`.`id` = `term`.`oid` + INNER JOIN `item-content` ON `item-content`.`uri-id` = `item`.`uri-id` + WHERE term.type IN (?, ?, ?, ?) AND `tid` >= ? ORDER BY `tid` LIMIT 100000", + Tag::HASHTAG, Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION, $id); + + if (DBA::errorNo() != 0) { + Logger::error('Database error', ['no' => DBA::errorNo(), 'message' => DBA::errorMessage()]); + return false; + } + + while ($term = DBA::fetch($terms)) { + if (($term['type'] == Tag::MENTION) && !empty($term['url']) && !strstr($term['body'], $term['url'])) { + $condition = ['nurl' => Strings::normaliseLink($term['url']), 'uid' => 0, 'deleted' => false]; + $contact = DBA::selectFirst('contact', ['url', 'alias'], $condition, ['order' => ['id']]); + if (!DBA::isResult($contact)) { + $ssl_url = str_replace('http://', 'https://', $term['url']); + $condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $term['url'], Strings::normaliseLink($term['url']), $ssl_url, 0]; + $contact = DBA::selectFirst('contact', ['url', 'alias'], $condition, ['order' => ['id']]); + } + + if (DBA::isResult($contact) && (!strstr($term['body'], $contact['url']) && (empty($contact['alias']) || !strstr($term['body'], $contact['alias'])))) { + $term['type'] = Tag::IMPLICIT_MENTION; + } + } + + Tag::store($term['uri-id'], $term['type'], $term['term'], $term['url'], false); + + $id = $term['tid']; + ++$rows; + if ($rows % 1000 == 0) { + DI::config()->set('system', 'post_update_version_1342_id', $id); + } + } + DBA::close($terms); + + DI::config()->set('system', 'post_update_version_1342_id', $id); + + Logger::info('Processed', ['rows' => $rows, 'last' => $id]); + + // When there are less than 1,000 items processed this means that we reached the end + // The other entries will then be processed with the regular functionality + if ($rows < 1000) { + DI::config()->set('system', 'post_update_version', 1342); + Logger::info('Done'); + return true; + } + + return false; + } + + /** + * Fill the "post-delivery-data" table with data from the "item-delivery-data" table + * + * @return bool "true" when the job is done + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + private static function update1345() + { + // Was the script completed? + if (DI::config()->get('system', 'post_update_version') >= 1345) { + return true; + } + + $id = DI::config()->get('system', 'post_update_version_1345_id', 0); + + Logger::info('Start', ['item' => $id]); + + $rows = 0; + + $deliveries = DBA::p("SELECT `uri-id`, `iid`, `item-delivery-data`.`postopts`, `item-delivery-data`.`inform`, + `queue_count`, `queue_done`, `activitypub`, `dfrn`, `diaspora`, `ostatus`, `legacy_dfrn`, `queue_failed` + FROM `item-delivery-data` + INNER JOIN `item` ON `item`.`id` = `item-delivery-data`.`iid` + WHERE `iid` >= ? ORDER BY `iid` LIMIT 10000", $id); + + if (DBA::errorNo() != 0) { + Logger::error('Database error', ['no' => DBA::errorNo(), 'message' => DBA::errorMessage()]); + return false; + } + + while ($delivery = DBA::fetch($deliveries)) { + $id = $delivery['iid']; + unset($delivery['iid']); + DBA::insert('post-delivery-data', $delivery); + ++$rows; + } + DBA::close($deliveries); + + DI::config()->set('system', 'post_update_version_1345_id', $id); + + Logger::info('Processed', ['rows' => $rows, 'last' => $id]); + + // When there are less than 100 items processed this means that we reached the end + // The other entries will then be processed with the regular functionality + if ($rows < 100) { + DI::config()->set('system', 'post_update_version', 1345); + Logger::info('Done'); + return true; + } + + return false; + } } diff --git a/src/Model/Item.php b/src/Model/Item.php index 97c96f6748..a4f45e3f45 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -61,7 +61,7 @@ class Item // Field list that is used to display the items const DISPLAY_FIELDLIST = [ - 'uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid', 'network', 'gravity', + 'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', 'network', 'gravity', 'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink', 'wall', 'private', 'starred', 'origin', 'title', 'body', 'file', 'attach', 'language', 'content-warning', 'location', 'coord', 'app', 'rendered-hash', 'rendered-html', 'object', @@ -77,10 +77,10 @@ class Item ]; // Field list that is used to deliver items via the protocols - const DELIVER_FIELDLIST = ['uid', 'id', 'parent', 'uri', 'thr-parent', 'parent-uri', 'guid', + const DELIVER_FIELDLIST = ['uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', 'parent-guid', 'created', 'edited', 'verb', 'object-type', 'object', 'target', 'private', 'title', 'body', 'location', 'coord', 'app', - 'attach', 'tag', 'deleted', 'extid', 'post-type', + 'attach', 'deleted', 'extid', 'post-type', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'author-id', 'author-link', 'owner-link', 'contact-uid', 'signed_text', 'signature', 'signer', 'network']; @@ -98,7 +98,7 @@ class Item 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'contact-id', 'type', 'wall', 'gravity', 'extid', 'icid', 'iaid', 'psid', 'created', 'edited', 'commented', 'received', 'changed', 'verb', - 'postopts', 'plink', 'resource-id', 'event-id', 'tag', 'attach', 'inform', + 'postopts', 'plink', 'resource-id', 'event-id', 'attach', 'inform', 'file', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'network', @@ -283,7 +283,7 @@ class Item // Fetch data from the item-content table whenever there is content there if (self::isLegacyMode()) { - $legacy_fields = array_merge(ItemDeliveryData::LEGACY_FIELD_LIST, self::MIXED_CONTENT_FIELDLIST); + $legacy_fields = array_merge(Post\DeliveryData::LEGACY_FIELD_LIST, self::MIXED_CONTENT_FIELDLIST); foreach ($legacy_fields as $field) { if (empty($row[$field]) && !empty($row['internal-item-' . $field])) { $row[$field] = $row['internal-item-' . $field]; @@ -317,11 +317,6 @@ class Item } if (!array_key_exists('verb', $row) || in_array($row['verb'], ['', Activity::POST, Activity::SHARE])) { - // Build the tag string out of the term entries - if (array_key_exists('tag', $row) && empty($row['tag'])) { - $row['tag'] = Term::tagTextFromItemId($row['internal-iid']); - } - // Build the file string out of the term entries if (array_key_exists('file', $row) && empty($row['file'])) { $row['file'] = Term::fileTextFromItemId($row['internal-iid']); @@ -673,7 +668,7 @@ class Item 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'contact-id', 'owner-id', 'author-id', 'type', 'wall', 'gravity', 'extid', 'created', 'edited', 'commented', 'received', 'changed', 'psid', - 'resource-id', 'event-id', 'tag', 'attach', 'post-type', 'file', + 'resource-id', 'event-id', 'attach', 'post-type', 'file', 'private', 'pubmail', 'moderated', 'visible', 'starred', 'bookmark', 'unseen', 'deleted', 'origin', 'forum_mode', 'mention', 'global', 'id' => 'item_id', 'network', 'icid', 'iaid', 'id' => 'internal-iid', @@ -687,7 +682,7 @@ class Item $fields['item-content'] = array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST); - $fields['item-delivery-data'] = array_merge(ItemDeliveryData::LEGACY_FIELD_LIST, ItemDeliveryData::FIELD_LIST); + $fields['post-delivery-data'] = array_merge(Post\DeliveryData::LEGACY_FIELD_LIST, Post\DeliveryData::FIELD_LIST); $fields['permissionset'] = ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']; @@ -809,8 +804,8 @@ class Item $joins .= " LEFT JOIN `item-content` ON `item-content`.`uri-id` = `item`.`uri-id`"; } - if (strpos($sql_commands, "`item-delivery-data`.") !== false) { - $joins .= " LEFT JOIN `item-delivery-data` ON `item-delivery-data`.`iid` = `item`.`id`"; + if (strpos($sql_commands, "`post-delivery-data`.") !== false) { + $joins .= " LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `item`.`uri-id` AND `item`.`origin`"; } if (strpos($sql_commands, "`permissionset`.") !== false) { @@ -850,7 +845,7 @@ class Item $selected[] = 'internal-user-ignored'; } - $legacy_fields = array_merge(ItemDeliveryData::LEGACY_FIELD_LIST, self::MIXED_CONTENT_FIELDLIST); + $legacy_fields = array_merge(Post\DeliveryData::LEGACY_FIELD_LIST, self::MIXED_CONTENT_FIELDLIST); $selection = []; foreach ($fields as $table => $table_fields) { @@ -922,7 +917,7 @@ class Item // We cannot simply expand the condition to check for origin entries // The condition needn't to be a simple array but could be a complex condition. // And we have to execute this query before the update to ensure to fetch the same data. - $items = DBA::select('item', ['id', 'origin', 'uri', 'uri-id', 'iaid', 'icid', 'tag', 'file'], $condition); + $items = DBA::select('item', ['id', 'origin', 'uri', 'uri-id', 'iaid', 'icid', 'file'], $condition); $content_fields = []; foreach (array_merge(self::CONTENT_FIELDLIST, self::MIXED_CONTENT_FIELDLIST) as $field) { @@ -936,7 +931,7 @@ class Item } } - $delivery_data = ItemDeliveryData::extractFields($fields); + $delivery_data = Post\DeliveryData::extractFields($fields); $clear_fields = ['bookmark', 'type', 'author-name', 'author-avatar', 'author-link', 'owner-name', 'owner-avatar', 'owner-link', 'postopts', 'inform']; foreach ($clear_fields as $field) { @@ -945,13 +940,6 @@ class Item } } - if (array_key_exists('tag', $fields)) { - $tags = $fields['tag']; - $fields['tag'] = null; - } else { - $tags = null; - } - if (array_key_exists('file', $fields)) { $files = $fields['file']; $fields['file'] = null; @@ -1024,13 +1012,6 @@ class Item } } - if (!is_null($tags)) { - Term::insertFromTagFieldByItemId($item['id'], $tags); - if (!empty($item['tag'])) { - DBA::update('item', ['tag' => ''], ['id' => $item['id']]); - } - } - if (!is_null($files)) { Term::insertFromFileFieldByItemId($item['id'], $files); if (!empty($item['file'])) { @@ -1038,7 +1019,7 @@ class Item } } - ItemDeliveryData::update($item['id'], $delivery_data); + Post\DeliveryData::update($item['uri-id'], $delivery_data); self::updateThread($item['id']); @@ -1118,7 +1099,7 @@ class Item { Logger::info('Mark item for deletion by id', ['id' => $item_id, 'callstack' => System::callstack()]); // locate item to be deleted - $fields = ['id', 'uri', 'uid', 'parent', 'parent-uri', 'origin', + $fields = ['id', 'uri', 'uri-id', 'uid', 'parent', 'parent-uri', 'origin', 'deleted', 'file', 'resource-id', 'event-id', 'attach', 'verb', 'object-type', 'object', 'target', 'contact-id', 'icid', 'iaid', 'psid']; @@ -1184,9 +1165,6 @@ class Item } } - // Delete tags that had been attached to other items - self::deleteTagsFromItem($item); - // Delete notifications DBA::delete('notify', ['iid' => $item['id'], 'uid' => $item['uid']]); @@ -1194,7 +1172,6 @@ class Item $item_fields = ['deleted' => true, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()]; DBA::update('item', $item_fields, ['id' => $item['id']]); - Term::insertFromTagFieldByItemId($item['id'], ''); Term::insertFromFileFieldByItemId($item['id'], ''); self::deleteThread($item['id'], $item['parent-uri']); @@ -1202,7 +1179,7 @@ class Item self::markForDeletion(['uri' => $item['uri'], 'uid' => 0, 'deleted' => false], $priority); } - ItemDeliveryData::delete($item['id']); + Post\DeliveryData::delete($item['uri-id']); // We don't delete the item-activity here, since we need some of the data for ActivityPub @@ -1243,43 +1220,6 @@ class Item return true; } - private static function deleteTagsFromItem($item) - { - if (($item["verb"] != Activity::TAG) || ($item["object-type"] != Activity\ObjectType::TAGTERM)) { - return; - } - - $xo = XML::parseString($item["object"]); - $xt = XML::parseString($item["target"]); - - if ($xt->type != Activity\ObjectType::NOTE) { - return; - } - - $i = self::selectFirst(['id', 'contact-id', 'tag'], ['uri' => $xt->id, 'uid' => $item['uid']]); - if (!DBA::isResult($i)) { - return; - } - - // For tags, the owner cannot remove the tag on the author's copy of the post. - $owner_remove = ($item["contact-id"] == $i["contact-id"]); - $author_copy = $item["origin"]; - - if (($owner_remove && $author_copy) || !$owner_remove) { - return; - } - - $tags = explode(',', $i["tag"]); - $newtags = []; - if (count($tags)) { - foreach ($tags as $tag) { - if (trim($tag) !== trim($xo->body)) { - $newtags[] = trim($tag); - } - } - } - self::update(['tag' => implode(',', $newtags)], ['id' => $i["id"]]); - } private static function guid($item, $notify) { @@ -1547,7 +1487,6 @@ class Item $item['deny_gid'] = trim($item['deny_gid'] ?? ''); $item['private'] = intval($item['private'] ?? self::PUBLIC); $item['body'] = trim($item['body'] ?? ''); - $item['tag'] = trim($item['tag'] ?? ''); $item['attach'] = trim($item['attach'] ?? ''); $item['app'] = trim($item['app'] ?? ''); $item['origin'] = intval($item['origin'] ?? 0); @@ -1674,6 +1613,11 @@ class Item // Check for hashtags in the body and repair or add hashtag links self::setHashtags($item); + // Store tags from the body if this hadn't been handled previously in the protocol classes + if (!Tag::existsForPost($item['uri-id'])) { + Tag::storeFromBody($item['uri-id'], $item['body']); + } + $item['thr-parent'] = $item['parent-uri']; $notify_type = Delivery::POST; @@ -1865,13 +1809,6 @@ class Item Logger::log('' . print_r($item,true), Logger::DATA); - if (array_key_exists('tag', $item)) { - $tags = $item['tag']; - unset($item['tag']); - } else { - $tags = ''; - } - if (array_key_exists('file', $item)) { $files = $item['file']; unset($item['file']); @@ -1898,7 +1835,7 @@ class Item self::insertContent($item); } - $delivery_data = ItemDeliveryData::extractFields($item); + $delivery_data = Post\DeliveryData::extractFields($item); unset($item['postopts']); unset($item['inform']); @@ -2002,7 +1939,7 @@ class Item } if (!empty($item['origin']) || !empty($item['wall']) || !empty($delivery_data['postopts']) || !empty($delivery_data['inform'])) { - ItemDeliveryData::insert($current_post, $delivery_data); + Post\DeliveryData::insert($item['uri-id'], $delivery_data); } DBA::commit(); @@ -2011,10 +1948,6 @@ class Item * Due to deadlock issues with the "term" table we are doing these steps after the commit. * This is not perfect - but a workable solution until we found the reason for the problem. */ - if (!empty($tags)) { - Term::insertFromTagFieldByItemId($current_post, $tags); - } - if (!empty($files)) { Term::insertFromFileFieldByItemId($current_post, $files); } @@ -2629,9 +2562,6 @@ class Item if (DI::config()->get('system', 'local_tags')) { $item["body"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", "#[url=".DI::baseUrl()."/search?tag=$2]$2[/url]", $item["body"]); - - $item["tag"] = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", - "#[url=".DI::baseUrl()."/search?tag=$2]$2[/url]", $item["tag"]); } // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls @@ -2663,13 +2593,6 @@ class Item $newtag = '#[url=' . DI::baseUrl() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]'; $item["body"] = str_replace($tag, $newtag, $item["body"]); - - if (!stristr($item["tag"], "/search?tag=" . $basetag . "]" . $basetag . "[/url]")) { - if (strlen($item["tag"])) { - $item["tag"] = ',' . $item["tag"]; - } - $item["tag"] = $newtag . $item["tag"]; - } } // Convert back the masked hashtags @@ -3027,30 +2950,6 @@ class Item return $recipients; } - public static function getFeedTags($item) - { - $ret = []; - $matches = false; - $cnt = preg_match_all('|\#\[url\=(.*?)\](.*?)\[\/url\]|', $item['tag'], $matches); - if ($cnt) { - for ($x = 0; $x < $cnt; $x ++) { - if ($matches[1][$x]) { - $ret[$matches[2][$x]] = ['#', $matches[1][$x], $matches[2][$x]]; - } - } - } - $matches = false; - $cnt = preg_match_all('|\@\[url\=(.*?)\](.*?)\[\/url\]|', $item['tag'], $matches); - if ($cnt) { - for ($x = 0; $x < $cnt; $x ++) { - if ($matches[1][$x]) { - $ret[] = ['@', $matches[1][$x], $matches[2][$x]]; - } - } - } - return $ret; - } - public static function expire($uid, $days, $network = "", $force = false) { if (!$uid || ($days < 1)) { @@ -3558,7 +3457,7 @@ class Item return $ev; } - $tags = Term::populateTagsFromItem($item); + $tags = Tag::populateFromItem($item); $item['tags'] = $tags['tags']; $item['hashtags'] = $tags['hashtags']; diff --git a/src/Model/ItemDeliveryData.php b/src/Model/Post/DeliveryData.php similarity index 63% rename from src/Model/ItemDeliveryData.php rename to src/Model/Post/DeliveryData.php index a26c80b1cb..0feb38281b 100644 --- a/src/Model/ItemDeliveryData.php +++ b/src/Model/Post/DeliveryData.php @@ -19,12 +19,12 @@ * */ -namespace Friendica\Model; +namespace Friendica\Model\Post; use Friendica\Database\DBA; use \BadMethodCallException; -class ItemDeliveryData +class DeliveryData { const LEGACY_FIELD_LIST = [ // Legacy fields moved from item table @@ -55,7 +55,7 @@ class ItemDeliveryData public static function extractFields(array &$fields) { $delivery_data = []; - foreach (array_merge(ItemDeliveryData::FIELD_LIST, ItemDeliveryData::LEGACY_FIELD_LIST) as $key => $field) { + foreach (array_merge(self::FIELD_LIST, self::LEGACY_FIELD_LIST) as $key => $field) { if (is_int($key) && isset($fields[$field])) { // Legacy field moved from item table $delivery_data[$field] = $fields[$field]; @@ -71,16 +71,16 @@ class ItemDeliveryData } /** - * Increments the queue_done for the given item ID. + * Increments the queue_done for the given URI ID. * * Avoids racing condition between multiple delivery threads. * - * @param integer $item_id + * @param integer $uri_id * @param integer $protocol * @return bool * @throws \Exception */ - public static function incrementQueueDone($item_id, $protocol = 0) + public static function incrementQueueDone(int $uri_id, int $protocol = 0) { $sql = ''; @@ -102,69 +102,69 @@ class ItemDeliveryData break; } - return DBA::e('UPDATE `item-delivery-data` SET `queue_done` = `queue_done` + 1' . $sql . ' WHERE `iid` = ?', $item_id); + return DBA::e('UPDATE `post-delivery-data` SET `queue_done` = `queue_done` + 1' . $sql . ' WHERE `uri-id` = ?', $uri_id); } /** - * Increments the queue_failed for the given item ID. + * Increments the queue_failed for the given URI ID. * * Avoids racing condition between multiple delivery threads. * - * @param integer $item_id + * @param integer $uri_id * @return bool * @throws \Exception */ - public static function incrementQueueFailed($item_id) + public static function incrementQueueFailed(int $uri_id) { - return DBA::e('UPDATE `item-delivery-data` SET `queue_failed` = `queue_failed` + 1 WHERE `iid` = ?', $item_id); + return DBA::e('UPDATE `post-delivery-data` SET `queue_failed` = `queue_failed` + 1 WHERE `uri-id` = ?', $uri_id); } /** - * Increments the queue_count for the given item ID. + * Increments the queue_count for the given URI ID. * - * @param integer $item_id + * @param integer $uri_id * @param integer $increment * @return bool * @throws \Exception */ - public static function incrementQueueCount(int $item_id, int $increment = 1) + public static function incrementQueueCount(int $uri_id, int $increment = 1) { - return DBA::e('UPDATE `item-delivery-data` SET `queue_count` = `queue_count` + ? WHERE `iid` = ?', $increment, $item_id); + return DBA::e('UPDATE `post-delivery-data` SET `queue_count` = `queue_count` + ? WHERE `uri-id` = ?', $increment, $uri_id); } /** - * Insert a new item delivery data entry + * Insert a new URI delivery data entry * - * @param integer $item_id + * @param integer $uri_id * @param array $fields * @return bool * @throws \Exception */ - public static function insert($item_id, array $fields) + public static function insert(int $uri_id, array $fields) { - if (empty($item_id)) { - throw new BadMethodCallException('Empty item_id'); + if (empty($uri_id)) { + throw new BadMethodCallException('Empty URI_id'); } - $fields['iid'] = $item_id; + $fields['uri-id'] = $uri_id; - return DBA::insert('item-delivery-data', $fields); + return DBA::insert('post-delivery-data', $fields); } /** - * Update/Insert item delivery data + * Update/Insert URI delivery data * * If you want to update queue_done, please use incrementQueueDone instead. * - * @param integer $item_id + * @param integer $uri_id * @param array $fields * @return bool * @throws \Exception */ - public static function update($item_id, array $fields) + public static function update(int $uri_id, array $fields) { - if (empty($item_id)) { - throw new BadMethodCallException('Empty item_id'); + if (empty($uri_id)) { + throw new BadMethodCallException('Empty URI_id'); } if (empty($fields)) { @@ -172,22 +172,22 @@ class ItemDeliveryData return true; } - return DBA::update('item-delivery-data', $fields, ['iid' => $item_id], true); + return DBA::update('post-delivery-data', $fields, ['uri-id' => $uri_id], true); } /** - * Delete item delivery data + * Delete URI delivery data * - * @param integer $item_id + * @param integer $uri_id * @return bool * @throws \Exception */ - public static function delete($item_id) + public static function delete(int $uri_id) { - if (empty($item_id)) { - throw new BadMethodCallException('Empty item_id'); + if (empty($uri_id)) { + throw new BadMethodCallException('Empty URI_id'); } - return DBA::delete('item-delivery-data', ['iid' => $item_id]); + return DBA::delete('post-delivery-data', ['uri-id' => $uri_id]); } } diff --git a/src/Model/Tag.php b/src/Model/Tag.php index cf453f2431..0a06dcc250 100644 --- a/src/Model/Tag.php +++ b/src/Model/Tag.php @@ -22,9 +22,11 @@ namespace Friendica\Model; use Friendica\Content\Text\BBCode; +use Friendica\Core\Cache\Duration; use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\Database\DBA; +use Friendica\DI; use Friendica\Util\Strings; /** @@ -38,8 +40,6 @@ class Tag const UNKNOWN = 0; const HASHTAG = 1; const MENTION = 2; - const CATEGORY = 3; - const FILE = 5; /** * An implicit mention is a mention in a comment body that is redundant with the threading information. */ @@ -67,7 +67,19 @@ class Tag */ public static function store(int $uriid, int $type, string $name, string $url = '', $probing = true) { - $name = trim($name, "\x00..\x20\xFF#!@"); + if ($type == self::HASHTAG) { + // Remove some common "garbarge" from tags + $name = trim($name, "\x00..\x20\xFF#!@,;.:'/?!^°$%".'"'); + + $tags = explode(self::TAG_CHARACTER[self::HASHTAG], $name); + if (count($tags) > 1) { + foreach ($tags as $tag) { + self::store($uriid, $type, $tag, $url, $probing); + } + return; + } + } + if (empty($name)) { return; } @@ -75,7 +87,7 @@ class Tag $cid = 0; $tagid = 0; - if (in_array($type, [Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION])) { + if (in_array($type, [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION])) { if (empty($url)) { // No mention without a contact url return; @@ -114,7 +126,7 @@ class Tag if (empty($cid)) { $fields = ['name' => substr($name, 0, 96), 'url' => '']; - if (($type != Tag::HASHTAG) && !empty($url) && ($url != $name)) { + if (($type != self::HASHTAG) && !empty($url) && ($url != $name)) { $fields['url'] = strtolower($url); } @@ -134,9 +146,9 @@ class Tag $fields = ['uri-id' => $uriid, 'type' => $type, 'tid' => $tagid, 'cid' => $cid]; - if (in_array($type, [Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION])) { + if (in_array($type, [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION])) { $condition = $fields; - $condition['type'] = [Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION]; + $condition['type'] = [self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION]; if (DBA::exists('post-tag', $condition)) { Logger::info('Tag already exists', $fields); return; @@ -221,6 +233,17 @@ class Tag } } + /** + * Checks for stored hashtags and mentions for the given post + * + * @param integer $uriid + * @return bool + */ + public static function existsForPost(int $uriid) + { + return DBA::exists('post-tag', ['uri-id' => $uriid, 'type' => [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION, self::EXCLUSIVE_MENTION]]); + } + /** * Remove tag/mention * @@ -282,6 +305,176 @@ class Tag } else { return self::UNKNOWN; } - } + + /** + * Retrieves the terms from the provided type(s) associated with the provided item ID. + * + * @param int $item_id + * @param int|array $type + * @return array + * @throws \Exception + */ + public static function getByURIId(int $uri_id, array $type = [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION, self::EXCLUSIVE_MENTION]) + { + $condition = ['uri-id' => $uri_id, 'type' => $type]; + return DBA::selectToArray('tag-view', ['type', 'name', 'url'], $condition); + } + + /** + * Sorts an item's tags into mentions, hashtags and other tags. Generate personalized URLs by user and modify the + * provided item's body with them. + * + * @param array $item + * @return array + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function populateFromItem(&$item) + { + $return = [ + 'tags' => [], + 'hashtags' => [], + 'mentions' => [], + 'implicit_mentions' => [], + ]; + + $searchpath = DI::baseUrl() . "/search?tag="; + + $taglist = DBA::select('tag-view', ['type', 'name', 'url'], + ['uri-id' => $item['uri-id'], 'type' => [self::HASHTAG, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION]]); + while ($tag = DBA::fetch($taglist)) { + if ($tag['url'] == '') { + $tag['url'] = $searchpath . rawurlencode($tag['name']); + } + + $orig_tag = $tag['url']; + + $prefix = self::TAG_CHARACTER[$tag['type']]; + switch($tag['type']) { + case self::HASHTAG: + if ($orig_tag != $tag['url']) { + $item['body'] = str_replace($orig_tag, $tag['url'], $item['body']); + } + + $return['hashtags'][] = $prefix . '' . htmlspecialchars($tag['name']) . ''; + $return['tags'][] = $prefix . '' . htmlspecialchars($tag['name']) . ''; + break; + case self::MENTION: + case self::EXCLUSIVE_MENTION: + $tag['url'] = Contact::magicLink($tag['url']); + $return['mentions'][] = $prefix . '' . htmlspecialchars($tag['name']) . ''; + $return['tags'][] = $prefix . '' . htmlspecialchars($tag['name']) . ''; + break; + case self::IMPLICIT_MENTION: + $return['implicit_mentions'][] = $prefix . $tag['name']; + break; + } + } + DBA::close($taglist); + + return $return; + } + + /** + * Search posts for given tag + * + * @param string $search + * @param integer $uid + * @param integer $start + * @param integer $limit + * @return array with URI-ID + */ + public static function getURIIdListByTag(string $search, int $uid = 0, int $start = 0, int $limit = 100) + { + $condition = ["`name` = ? AND (NOT `private` OR (`private` AND `uid` = ?))", $search, $uid]; + $params = [ + 'order' => ['uri-id' => true], + 'group_by' => ['uri-id'], + 'limit' => [$start, $limit] + ]; + + $tags = DBA::select('tag-search-view', ['uri-id'], $condition, $params); + + $uriids = []; + while ($tag = DBA::fetch($tags)) { + $uriids[] = $tag['uri-id']; + } + DBA::close($tags); + + return $uriids; + } + + /** + * Returns a list of the most frequent global hashtags over the given period + * + * @param int $period Period in hours to consider posts + * @return array + * @throws \Exception + */ + public static function getGlobalTrendingHashtags(int $period, $limit = 10) + { + $tags = DI::cache()->get('global_trending_tags'); + + if (empty($tags)) { + $tagsStmt = DBA::p("SELECT `name` AS `term`, COUNT(*) AS `score` + FROM `tag-search-view` + WHERE `private` = ? AND `received` > DATE_SUB(NOW(), INTERVAL ? HOUR) + GROUP BY `term` ORDER BY `score` DESC LIMIT ?", + Item::PUBLIC, $period, $limit); + + if (DBA::isResult($tagsStmt)) { + $tags = DBA::toArray($tagsStmt); + DI::cache()->set('global_trending_tags', $tags, Duration::HOUR); + } + } + + return $tags ?: []; + } + + /** + * Returns a list of the most frequent local hashtags over the given period + * + * @param int $period Period in hours to consider posts + * @return array + * @throws \Exception + */ + public static function getLocalTrendingHashtags(int $period, $limit = 10) + { + $tags = DI::cache()->get('local_trending_tags'); + + if (empty($tags)) { + $tagsStmt = DBA::p("SELECT `name` AS `term`, COUNT(*) AS `score` + FROM `tag-search-view` + WHERE `private` = ? AND `wall` AND `origin` AND `received` > DATE_SUB(NOW(), INTERVAL ? HOUR) + GROUP BY `term` ORDER BY `score` DESC LIMIT ?", + Item::PUBLIC, $period, $limit); + + if (DBA::isResult($tagsStmt)) { + $tags = DBA::toArray($tagsStmt); + DI::cache()->set('local_trending_tags', $tags, Duration::HOUR); + } + } + + return $tags ?: []; + } + + /** + * Check if the provided tag is of one of the provided term types. + * + * @param string $tag + * @param int ...$types + * @return bool + */ + public static function isType($tag, ...$types) + { + $tag_chars = []; + foreach ($types as $type) { + if (array_key_exists($type, self::TAG_CHARACTER)) { + $tag_chars[] = self::TAG_CHARACTER[$type]; + } + } + + return Strings::startsWith($tag, $tag_chars); + } } diff --git a/src/Model/Term.php b/src/Model/Term.php index 13639f7700..ea9ddc1919 100644 --- a/src/Model/Term.php +++ b/src/Model/Term.php @@ -21,11 +21,7 @@ namespace Friendica\Model; -use Friendica\Core\Cache\Duration; -use Friendica\Core\Logger; use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Util\Strings; /** * Class Term @@ -37,155 +33,10 @@ use Friendica\Util\Strings; class Term { const UNKNOWN = 0; - const HASHTAG = 1; - const MENTION = 2; const CATEGORY = 3; const FILE = 5; - /** - * An implicit mention is a mention in a comment body that is redundant with the threading information. - */ - const IMPLICIT_MENTION = 8; - /** - * An exclusive mention transfers the ownership of the post to the target account, usually a forum. - */ - const EXCLUSIVE_MENTION = 9; - - const TAG_CHARACTER = [ - self::HASHTAG => '#', - self::MENTION => '@', - self::IMPLICIT_MENTION => '%', - self::EXCLUSIVE_MENTION => '!', - ]; const OBJECT_TYPE_POST = 1; - const OBJECT_TYPE_PHOTO = 2; - - /** - * Returns a list of the most frequent global hashtags over the given period - * - * @param int $period Period in hours to consider posts - * @return array - * @throws \Exception - */ - public static function getGlobalTrendingHashtags(int $period, $limit = 10) - { - $tags = DI::cache()->get('global_trending_tags'); - - if (!$tags) { - $tagsStmt = DBA::p("SELECT t.`term`, COUNT(*) AS `score` - FROM `term` t - JOIN `item` i ON i.`id` = t.`oid` AND i.`uid` = t.`uid` - JOIN `thread` ON `thread`.`iid` = i.`id` - WHERE `thread`.`visible` - AND NOT `thread`.`deleted` - AND NOT `thread`.`moderated` - AND `thread`.`private` = ? - AND t.`uid` = 0 - AND t.`otype` = ? - AND t.`type` = ? - AND t.`term` != '' - AND i.`received` > DATE_SUB(NOW(), INTERVAL ? HOUR) - GROUP BY `term` - ORDER BY `score` DESC - LIMIT ?", - Item::PUBLIC, - Term::OBJECT_TYPE_POST, - Term::HASHTAG, - $period, - $limit - ); - - if (DBA::isResult($tagsStmt)) { - $tags = DBA::toArray($tagsStmt); - DI::cache()->set('global_trending_tags', $tags, Duration::HOUR); - } - } - - return $tags ?: []; - } - - /** - * Returns a list of the most frequent local hashtags over the given period - * - * @param int $period Period in hours to consider posts - * @return array - * @throws \Exception - */ - public static function getLocalTrendingHashtags(int $period, $limit = 10) - { - $tags = DI::cache()->get('local_trending_tags'); - - if (!$tags) { - $tagsStmt = DBA::p("SELECT t.`term`, COUNT(*) AS `score` - FROM `term` t - JOIN `item` i ON i.`id` = t.`oid` AND i.`uid` = t.`uid` - JOIN `thread` ON `thread`.`iid` = i.`id` - WHERE `thread`.`visible` - AND NOT `thread`.`deleted` - AND NOT `thread`.`moderated` - AND `thread`.`private` = ? - AND `thread`.`wall` - AND `thread`.`origin` - AND t.`otype` = ? - AND t.`type` = ? - AND t.`term` != '' - AND i.`received` > DATE_SUB(NOW(), INTERVAL ? HOUR) - GROUP BY `term` - ORDER BY `score` DESC - LIMIT ?", - Item::PUBLIC, - Term::OBJECT_TYPE_POST, - Term::HASHTAG, - $period, - $limit - ); - - if (DBA::isResult($tagsStmt)) { - $tags = DBA::toArray($tagsStmt); - DI::cache()->set('local_trending_tags', $tags, Duration::HOUR); - } - } - - return $tags ?: []; - } - - /** - * Generates the legacy item.tag field comma-separated BBCode string from an item ID. - * Includes only hashtags, implicit and explicit mentions. - * - * @param int $item_id - * @return string - * @throws \Exception - */ - public static function tagTextFromItemId($item_id) - { - $tag_list = []; - $tags = self::tagArrayFromItemId($item_id, [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]); - foreach ($tags as $tag) { - $tag_list[] = self::TAG_CHARACTER[$tag['type']] . '[url=' . $tag['url'] . ']' . $tag['term'] . '[/url]'; - } - - return implode(',', $tag_list); - } - - /** - * Retrieves the terms from the provided type(s) associated with the provided item ID. - * - * @param int $item_id - * @param int|array $type - * @return array - * @throws \Exception - */ - public static function tagArrayFromItemId($item_id, $type = [self::HASHTAG, self::MENTION]) - { - $condition = ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => $type]; - $tags = DBA::select('term', ['type', 'term', 'url'], $condition); - if (!DBA::isResult($tags)) { - return []; - } - - return DBA::toArray($tags); - } /** * Generates the legacy item.file field string from an item ID. @@ -198,7 +49,9 @@ class Term public static function fileTextFromItemId($item_id) { $file_text = ''; - $tags = self::tagArrayFromItemId($item_id, [self::FILE, self::CATEGORY]); + + $condition = ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => [self::FILE, self::CATEGORY]]; + $tags = DBA::selectToArray('term', ['type', 'term', 'url'], $condition); foreach ($tags as $tag) { if ($tag['type'] == self::CATEGORY) { $file_text .= '<' . $tag['term'] . '>'; @@ -210,170 +63,6 @@ class Term return $file_text; } - /** - * Inserts new terms for the provided item ID based on the legacy item.tag field BBCode content. - * Deletes all previous tag terms for the same item ID. - * Sets both the item.mention and thread.mentions field flags if a mention concerning the item UID is found. - * - * @param int $item_id - * @param string $tag_str - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function insertFromTagFieldByItemId($item_id, $tag_str) - { - $profile_base = DI::baseUrl(); - $profile_data = parse_url($profile_base); - $profile_path = $profile_data['path'] ?? ''; - $profile_base_friendica = $profile_data['host'] . $profile_path . '/profile/'; - $profile_base_diaspora = $profile_data['host'] . $profile_path . '/u/'; - - $fields = ['guid', 'uid', 'id', 'edited', 'deleted', 'created', 'received', 'title', 'body', 'parent']; - $item = Item::selectFirst($fields, ['id' => $item_id]); - if (!DBA::isResult($item)) { - return; - } - - $item['tag'] = $tag_str; - - // Clean up all tags - self::deleteByItemId($item_id); - - if ($item['deleted']) { - return; - } - - $taglist = explode(',', $item['tag']); - - $tags_string = ''; - foreach ($taglist as $tag) { - if (Strings::startsWith($tag, self::TAG_CHARACTER)) { - $tags_string .= ' ' . trim($tag); - } else { - $tags_string .= ' #' . trim($tag); - } - } - - $data = ' ' . $item['title'] . ' ' . $item['body'] . ' ' . $tags_string . ' '; - - // ignore anything in a code block - $data = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $data); - - $tags = []; - - $pattern = '/\W\#([^\[].*?)[\s\'".,:;\?!\[\]\/]/ism'; - if (preg_match_all($pattern, $data, $matches)) { - foreach ($matches[1] as $match) { - $tags['#' . $match] = ''; - } - } - - $pattern = '/\W([\#@!%])\[url\=(.*?)\](.*?)\[\/url\]/ism'; - if (preg_match_all($pattern, $data, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - - if (in_array($match[1], [ - self::TAG_CHARACTER[self::MENTION], - self::TAG_CHARACTER[self::IMPLICIT_MENTION], - self::TAG_CHARACTER[self::EXCLUSIVE_MENTION] - ])) { - $contact = Contact::getDetailsByURL($match[2], 0); - if (!empty($contact['addr'])) { - $match[3] = $contact['addr']; - } - - if (!empty($contact['url'])) { - $match[2] = $contact['url']; - } - } - - $tags[$match[2]] = $match[1] . trim($match[3], ',.:;[]/\"?!'); - } - } - - foreach ($tags as $link => $tag) { - if (self::isType($tag, self::HASHTAG)) { - // try to ignore #039 or #1 or anything like that - if (ctype_digit(substr(trim($tag), 1))) { - continue; - } - - // try to ignore html hex escapes, e.g. #x2317 - if ((substr(trim($tag), 1, 1) == 'x' || substr(trim($tag), 1, 1) == 'X') && ctype_digit(substr(trim($tag), 2))) { - continue; - } - - $type = self::HASHTAG; - $term = substr($tag, 1); - $link = ''; - } elseif (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION)) { - if (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION)) { - $type = self::MENTION; - } else { - $type = self::IMPLICIT_MENTION; - } - - $contact = Contact::getDetailsByURL($link, 0); - if (!empty($contact['name'])) { - $term = $contact['name']; - } else { - $term = substr($tag, 1); - } - } else { // This shouldn't happen - $type = self::HASHTAG; - $term = $tag; - $link = ''; - - Logger::notice('Unknown term type', ['tag' => $tag]); - } - - if (DBA::exists('term', ['uid' => $item['uid'], 'otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'term' => $term, 'type' => $type])) { - continue; - } - - if (empty($term)) { - continue; - } - - if ($item['uid'] == 0) { - $global = true; - DBA::update('term', ['global' => true], ['otype' => self::OBJECT_TYPE_POST, 'guid' => $item['guid']]); - } else { - $global = DBA::exists('term', ['uid' => 0, 'otype' => self::OBJECT_TYPE_POST, 'guid' => $item['guid']]); - } - - DBA::insert('term', [ - 'uid' => $item['uid'], - 'oid' => $item_id, - 'otype' => self::OBJECT_TYPE_POST, - 'type' => $type, - 'term' => substr($term, 0, 255), - 'url' => $link, - 'guid' => $item['guid'], - 'created' => $item['created'], - 'received' => $item['received'], - 'global' => $global - ]); - - // Search for mentions - if (self::isType($tag, self::MENTION, self::EXCLUSIVE_MENTION) - && ( - strpos($link, $profile_base_friendica) !== false - || strpos($link, $profile_base_diaspora) !== false - ) - ) { - $users_stmt = DBA::p("SELECT `uid` FROM `contact` WHERE self AND (`url` = ? OR `nurl` = ?)", $link, $link); - $users = DBA::toArray($users_stmt); - foreach ($users AS $user) { - if ($user['uid'] == $item['uid']) { - /// @todo This function is called from Item::update - so we mustn't call that function here - DBA::update('item', ['mention' => true], ['id' => $item_id]); - DBA::update('thread', ['mention' => true], ['iid' => $item['parent']]); - } - } - } - } - } - /** * Inserts new terms for the provided item ID based on the legacy item.file field BBCode content. * Deletes all previous file terms for the same item ID. @@ -423,98 +112,4 @@ class Term } } } - - /** - * Sorts an item's tags into mentions, hashtags and other tags. Generate personalized URLs by user and modify the - * provided item's body with them. - * - * @param array $item - * @return array - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function populateTagsFromItem(&$item) - { - $return = [ - 'tags' => [], - 'hashtags' => [], - 'mentions' => [], - 'implicit_mentions' => [], - ]; - - $searchpath = DI::baseUrl() . "/search?tag="; - - $taglist = DBA::select( - 'term', - ['type', 'term', 'url'], - ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item['id'], 'type' => [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]], - ['order' => ['tid']] - ); - while ($tag = DBA::fetch($taglist)) { - if ($tag['url'] == '') { - $tag['url'] = $searchpath . rawurlencode($tag['term']); - } - - $orig_tag = $tag['url']; - - $prefix = self::TAG_CHARACTER[$tag['type']]; - switch($tag['type']) { - case self::HASHTAG: - if ($orig_tag != $tag['url']) { - $item['body'] = str_replace($orig_tag, $tag['url'], $item['body']); - } - - $return['hashtags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; - $return['tags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; - break; - case self::MENTION: - $tag['url'] = Contact::magicLink($tag['url']); - $return['mentions'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; - $return['tags'][] = $prefix . '' . htmlspecialchars($tag['term']) . ''; - break; - case self::IMPLICIT_MENTION: - $return['implicit_mentions'][] = $prefix . $tag['term']; - break; - } - } - DBA::close($taglist); - - return $return; - } - - /** - * Delete tags of the specific type(s) from an item - * - * @param int $item_id - * @param int|array $type - * @throws \Exception - */ - public static function deleteByItemId($item_id, $type = [self::HASHTAG, self::MENTION, self::IMPLICIT_MENTION]) - { - if (empty($item_id)) { - return; - } - - // Clean up all tags - DBA::delete('term', ['otype' => self::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => $type]); - } - - /** - * Check if the provided tag is of one of the provided term types. - * - * @param string $tag - * @param int ...$types - * @return bool - */ - public static function isType($tag, ...$types) - { - $tag_chars = []; - foreach ($types as $type) { - if (array_key_exists($type, self::TAG_CHARACTER)) { - $tag_chars[] = self::TAG_CHARACTER[$type]; - } - } - - return Strings::startsWith($tag, $tag_chars); - } } diff --git a/src/Model/UserItem.php b/src/Model/UserItem.php index 72db1005d1..89dbafed82 100644 --- a/src/Model/UserItem.php +++ b/src/Model/UserItem.php @@ -26,7 +26,7 @@ use Friendica\Core\Hook; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Util\Strings; -use Friendica\Model\Term; +use Friendica\Model\Tag; class UserItem { @@ -50,7 +50,7 @@ class UserItem */ public static function setNotification(int $iid) { - $fields = ['id', 'uid', 'body', 'parent', 'gravity', 'tag', 'contact-id', 'thr-parent', 'parent-uri', 'author-id']; + $fields = ['id', 'uri-id', 'uid', 'body', 'parent', 'gravity', 'tag', 'contact-id', 'thr-parent', 'parent-uri', 'author-id']; $item = Item::selectFirst($fields, ['id' => $iid, 'origin' => false]); if (!DBA::isResult($item)) { return; @@ -207,7 +207,7 @@ class UserItem } // Or the contact is a mentioned forum - $tags = DBA::select('term', ['url'], ['otype' => Term::OBJECT_TYPE_POST, 'oid' => $item['id'], 'type' => Term::MENTION, 'uid' => $uid]); + $tags = DBA::select('tag-view', ['url'], ['uri-id' => $item['uri-id'], 'type' => [Tag::MENTION, Tag::EXCLUSIVE_MENTION]]); while ($tag = DBA::fetch($tags)) { $condition = ['nurl' => Strings::normaliseLink($tag['url']), 'uid' => $uid, 'notify_new_posts' => true, 'contact-type' => Contact::TYPE_COMMUNITY]; if (DBA::exists('contact', $condition)) { @@ -227,9 +227,10 @@ class UserItem */ private static function checkImplicitMention(array $item, array $profiles) { - foreach ($profiles AS $profile) { - if (strpos($item['tag'], '=' . $profile.']') || strpos($item['body'], '=' . $profile . ']')) { - if (strpos($item['body'], $profile) === false) { + $mentions = Tag::getByURIId($item['uri-id'], [Tag::IMPLICIT_MENTION]); + foreach ($mentions as $mention) { + foreach ($profiles as $profile) { + if (Strings::compareLink($profile, $mention['url'])) { return true; } } @@ -246,9 +247,10 @@ class UserItem */ private static function checkExplicitMention(array $item, array $profiles) { - foreach ($profiles AS $profile) { - if (strpos($item['tag'], '=' . $profile.']') || strpos($item['body'], '=' . $profile . ']')) { - if (!(strpos($item['body'], $profile) === false)) { + $mentions = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]); + foreach ($mentions as $mention) { + foreach ($profiles as $profile) { + if (Strings::compareLink($profile, $mention['url'])) { return true; } } diff --git a/src/Module/Admin/Item/Source.php b/src/Module/Admin/Item/Source.php index b8aaff99b8..e35eafd2f2 100644 --- a/src/Module/Admin/Item/Source.php +++ b/src/Module/Admin/Item/Source.php @@ -48,14 +48,14 @@ class Source extends BaseAdmin $item_id = ''; $terms = []; if (!empty($guid)) { - $item = Model\Item::selectFirst(['id', 'guid', 'uri'], ['guid' => $guid]); + $item = Model\Item::selectFirst(['id', 'uri-id', 'guid', 'uri'], ['guid' => $guid]); $conversation = Model\Conversation::getByItemUri($item['uri']); $item_id = $item['id']; $item_uri = $item['uri']; $source = $conversation['source']; - $terms = Model\Term::tagArrayFromItemId($item['id'], [Model\Term::HASHTAG, Model\Term::MENTION, Model\Term::IMPLICIT_MENTION]); + $terms = Model\Tag::getByURIId($item['uri-id'], [Model\Tag::HASHTAG, Model\Tag::MENTION, Model\Tag::IMPLICIT_MENTION]); } $tpl = Renderer::getMarkupTemplate('admin/item/source.tpl'); diff --git a/src/Module/Hashtag.php b/src/Module/Hashtag.php index 06c6374e34..365e77572c 100644 --- a/src/Module/Hashtag.php +++ b/src/Module/Hashtag.php @@ -25,14 +25,12 @@ use Friendica\BaseModule; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Util\Strings; -use Friendica\Model\Term; /** * Hashtag module. */ class Hashtag extends BaseModule { - public static function content(array $parameters = []) { $result = []; @@ -42,12 +40,9 @@ class Hashtag extends BaseModule System::jsonExit($result); } - $taglist = DBA::p("SELECT DISTINCT(`term`) FROM `term` WHERE `term` LIKE ? AND `type` = ? ORDER BY `term`", - $t . '%', - intval(Term::HASHTAG) - ); + $taglist = DBA::select('tag', ['name'], ["`name` LIKE ?", $t . "%"], ['order' => ['name'], 'limit' => 100]); while ($tag = DBA::fetch($taglist)) { - $result[] = ['text' => $tag['term']]; + $result[] = ['text' => $tag['name']]; } DBA::close($taglist); diff --git a/src/Module/Profile/Profile.php b/src/Module/Profile/Profile.php index 6c7f4f14e5..c187281d33 100644 --- a/src/Module/Profile/Profile.php +++ b/src/Module/Profile/Profile.php @@ -35,7 +35,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Profile as ProfileModel; -use Friendica\Model\Term; +use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Module\BaseProfile; use Friendica\Module\Security\Login; @@ -184,7 +184,7 @@ class Profile extends BaseProfile foreach (explode(',', $a->profile['pub_keywords']) as $tag_label) { $tags[] = [ 'url' => '/search?tag=' . $tag_label, - 'label' => Term::TAG_CHARACTER[Term::HASHTAG] . $tag_label, + 'label' => Tag::TAG_CHARACTER[Tag::HASHTAG] . $tag_label, ]; } diff --git a/src/Module/Profile/Status.php b/src/Module/Profile/Status.php index 8b6734e5cd..685ab4e076 100644 --- a/src/Module/Profile/Status.php +++ b/src/Module/Profile/Status.php @@ -147,8 +147,8 @@ class Status extends BaseProfile } if (!empty($hashtags)) { - $sql_post_table .= sprintf("INNER JOIN (SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d ORDER BY `tid` DESC) AS `term` ON `item`.`id` = `term`.`oid` ", - DBA::escape(Strings::protectSprintf($hashtags)), intval(Term::OBJECT_TYPE_POST), intval(Term::HASHTAG), intval($a->profile['uid'])); + $sql_post_table .= sprintf("INNER JOIN (SELECT `uri-id` FROM `tag-search-view` WHERE `name` = '%s' AND `uid` = %d ORDER BY `uri-id` DESC) AS `tag-search` ON `item`.`uri-id` = `tag-search`.`uri-id` ", + DBA::escape(Strings::protectSprintf($hashtags)), intval($a->profile['uid'])); } if (!empty($datequery)) { diff --git a/src/Module/Search/Index.php b/src/Module/Search/Index.php index 44407623e9..27074fa82a 100644 --- a/src/Module/Search/Index.php +++ b/src/Module/Search/Index.php @@ -33,7 +33,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Item; -use Friendica\Model\Term; +use Friendica\Model\Tag; use Friendica\Module\BaseSearch; use Friendica\Network\HTTPException; use Friendica\Util\Strings; @@ -149,28 +149,11 @@ class Index extends BaseSearch if ($tag) { Logger::info('Start tag search.', ['q' => $search]); + $uriids = Tag::getURIIdListByTag($search, local_user(), $pager->getStart(), $pager->getItemsPerPage()); - $condition = [ - "(`uid` = 0 OR (`uid` = ? AND NOT `global`)) - AND `otype` = ? AND `type` = ? AND `term` = ?", - local_user(), Term::OBJECT_TYPE_POST, Term::HASHTAG, $search - ]; - $params = [ - 'order' => ['received' => true], - 'limit' => [$pager->getStart(), $pager->getItemsPerPage()] - ]; - $terms = DBA::select('term', ['oid'], $condition, $params); - - $itemids = []; - while ($term = DBA::fetch($terms)) { - $itemids[] = $term['oid']; - } - - DBA::close($terms); - - if (!empty($itemids)) { - $params = ['order' => ['id' => true]]; - $items = Item::selectForUser(local_user(), [], ['id' => $itemids], $params); + if (!empty($uriids)) { + $params = ['order' => ['id' => true], 'group_by' => ['uri-id']]; + $items = Item::selectForUser(local_user(), [], ['uri-id' => $uriids], $params); $r = Item::inArray($items); } else { $r = []; diff --git a/src/Object/Post.php b/src/Object/Post.php index 76cf6b0367..8488df000f 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -33,7 +33,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Item; -use Friendica\Model\Term; +use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Protocol\Activity; use Friendica\Util\Crypto; @@ -390,7 +390,7 @@ class Post $buttons["like"] = false; } - $tags = Term::populateTagsFromItem($item); + $tags = Tag::populateFromItem($item); $ago = Temporal::getRelativeDate($item['created']); $ago_received = Temporal::getRelativeDate($item['received']); @@ -860,7 +860,7 @@ class Post return ''; } - $item = Item::selectFirst(['author-addr'], ['id' => $this->getId()]); + $item = Item::selectFirst(['author-addr', 'uri-id'], ['id' => $this->getId()]); if (!DBA::isResult($item) || empty($item['author-addr'])) { // Should not happen return ''; @@ -872,7 +872,7 @@ class Post $text = ''; } - $terms = Term::tagArrayFromItemId($this->getId(), [Term::MENTION, Term::IMPLICIT_MENTION]); + $terms = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]); foreach ($terms as $term) { $profile = Contact::getDetailsByURL($term['url']); if (!empty($profile['addr']) && ((($profile['contact-type'] ?? '') ?: Contact::TYPE_UNKNOWN) != Contact::TYPE_COMMUNITY) && diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index 7860223432..479d8d55c7 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -35,7 +35,6 @@ use Friendica\Model\Item; use Friendica\Model\ItemURI; use Friendica\Model\Mail; use Friendica\Model\Tag; -use Friendica\Model\Term; use Friendica\Model\User; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; @@ -388,7 +387,7 @@ class Processor if (empty($activity['directmessage']) && ($item['thr-parent'] != $item['uri']) && ($item['gravity'] == GRAVITY_COMMENT)) { $item_private = !in_array(0, $activity['item_receiver']); - $parent = Item::selectFirst(['id', 'private', 'author-link', 'alias'], ['uri' => $item['thr-parent']]); + $parent = Item::selectFirst(['id', 'uri-id', 'private', 'author-link', 'alias'], ['uri' => $item['thr-parent']]); if (!DBA::isResult($parent)) { Logger::warning('Unknown parent item.', ['uri' => $item['thr-parent']]); return false; @@ -1016,7 +1015,7 @@ class Processor return []; } - $parent_terms = Term::tagArrayFromItemId($parent['id'], [Term::MENTION, Term::IMPLICIT_MENTION]); + $parent_terms = Tag::getByURIId($parent['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]); $parent_author = Contact::getDetailsByURL($parent['author-link'], 0); @@ -1084,8 +1083,8 @@ class Processor foreach ($activity_tags as $index => $tag) { if (in_array($tag['href'], $potential_mentions)) { $activity_tags[$index]['name'] = preg_replace( - '/' . preg_quote(Term::TAG_CHARACTER[Term::MENTION], '/') . '/', - Term::TAG_CHARACTER[Term::IMPLICIT_MENTION], + '/' . preg_quote(Tag::TAG_CHARACTER[Tag::MENTION], '/') . '/', + Tag::TAG_CHARACTER[Tag::IMPLICIT_MENTION], $activity_tags[$index]['name'], 1 ); diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 8928b9435a..16b7b039a9 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -34,9 +34,10 @@ use Friendica\Model\APContact; use Friendica\Model\Contact; use Friendica\Model\Conversation; use Friendica\Model\Item; +use Friendica\Model\ItemURI; use Friendica\Model\Profile; use Friendica\Model\Photo; -use Friendica\Model\Term; +use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; @@ -407,7 +408,7 @@ class Transmitter $actor_profile = APContact::getByURL($item['author-link']); } - $terms = Term::tagArrayFromItemId($item['id'], [Term::MENTION, Term::IMPLICIT_MENTION]); + $terms = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]); if ($item['private'] != Item::PRIVATE) { // Directly mention the original author upon a quoted reshare. @@ -701,6 +702,8 @@ class Transmitter return []; } + $mail['uri-id'] = ItemURI::insert(['uri' => $mail['uri'], 'guid' => $mail['guid']]); + $reply = DBA::selectFirst('mail', ['uri'], ['parent-uri' => $mail['parent-uri'], 'reply' => false]); // Making the post more compatible for Mastodon by: @@ -1009,12 +1012,12 @@ class Transmitter { $tags = []; - $terms = Term::tagArrayFromItemId($item['id'], [Term::HASHTAG, Term::MENTION, Term::IMPLICIT_MENTION]); + $terms = Tag::getByURIId($item['uri-id'], [Tag::HASHTAG, Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]); foreach ($terms as $term) { - if ($term['type'] == Term::HASHTAG) { - $url = DI::baseUrl() . '/search?tag=' . urlencode($term['term']); - $tags[] = ['type' => 'Hashtag', 'href' => $url, 'name' => '#' . $term['term']]; - } elseif ($term['type'] == Term::MENTION || $term['type'] == Term::IMPLICIT_MENTION) { + if ($term['type'] == Tag::HASHTAG) { + $url = DI::baseUrl() . '/search?tag=' . urlencode($term['name']); + $tags[] = ['type' => 'Hashtag', 'href' => $url, 'name' => '#' . $term['name']]; + } else { $contact = Contact::getDetailsByURL($term['url']); if (!empty($contact['addr'])) { $mention = '@' . $contact['addr']; @@ -1213,15 +1216,14 @@ class Transmitter /** * Returns if the post contains sensitive content ("nsfw") * - * @param integer $item_id + * @param integer $uri_id * * @return boolean * @throws \Exception */ - private static function isSensitive($item_id) + private static function isSensitive($uri_id) { - $condition = ['otype' => Term::OBJECT_TYPE_POST, 'oid' => $item_id, 'type' => Term::HASHTAG, 'term' => 'nsfw']; - return DBA::exists('term', $condition); + return DBA::exists('tag-view', ['uri-id' => $uri_id, 'name' => 'nsfw']); } /** @@ -1303,7 +1305,7 @@ class Transmitter $data['url'] = $item['plink']; $data['attributedTo'] = $item['author-link']; - $data['sensitive'] = self::isSensitive($item['id']); + $data['sensitive'] = self::isSensitive($item['uri-id']); $data['context'] = self::fetchContextURLForItem($item); if (!empty($item['title'])) { diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index 4e5aac37d9..5060a7d563 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -1080,21 +1080,15 @@ class DFRN $entry->appendChild($actarg); } - $tags = Item::getFeedTags($item); + $tags = Tag::getByURIId($item['uri-id']); - /// @TODO Combine this with similar below if() block? if (count($tags)) { - foreach ($tags as $t) { - if (($type != 'html') || ($t[0] != "@")) { - XML::addElement($doc, $entry, "category", "", ["scheme" => "X-DFRN:".$t[0].":".$t[1], "term" => $t[2]]); + foreach ($tags as $tag) { + if (($type != 'html') || ($tag['type'] == Tag::HASHTAG)) { + XML::addElement($doc, $entry, "category", "", ["scheme" => "X-DFRN:" . Tag::TAG_CHARACTER[$tag['type']] . ":" . $tag['url'], "term" => $tag['name']]); } - } - } - - if (count($tags)) { - foreach ($tags as $t) { - if ($t[0] == "@") { - $mentioned[$t[1]] = $t[1]; + if ($tag['type'] != Tag::HASHTAG) { + $mentioned[$tag['url']] = $tag['url']; } } } @@ -2238,11 +2232,6 @@ class DFRN // extract tag, if not duplicate, add to parent item if ($xo->content) { Tag::store($item_tag['uri-id'], Tag::HASHTAG, $xo->content); - - if (!stristr($item_tag["tag"], trim($xo->content))) { - $tag = $item_tag["tag"] . (strlen($item_tag["tag"]) ? ',' : '') . '#[url=' . $xo->id . ']'. $xo->content . '[/url]'; - Item::update(['tag' => $tag], ['id' => $item_tag["id"]]); - } } } } @@ -2440,17 +2429,7 @@ class DFRN if (($term != "") && ($scheme != "")) { $parts = explode(":", $scheme); if ((count($parts) >= 4) && (array_shift($parts) == "X-DFRN")) { - $termhash = array_shift($parts); $termurl = implode(":", $parts); - - if (!empty($item["tag"])) { - $item["tag"] .= ","; - } else { - $item["tag"] = ""; - } - - $item["tag"] .= $termhash . "[url=" . $termurl . "]" . $term . "[/url]"; - Tag::store($item['uri-id'], Tag::IMPLICIT_MENTION, $term, $termurl); } } diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 6282295011..708f5335a5 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -36,11 +36,9 @@ use Friendica\Model\Conversation; use Friendica\Model\GContact; use Friendica\Model\Item; use Friendica\Model\ItemURI; -use Friendica\Model\ItemDeliveryData; use Friendica\Model\Mail; -use Friendica\Model\Profile; +use Friendica\Model\Post; use Friendica\Model\Tag; -use Friendica\Model\Term; use Friendica\Model\User; use Friendica\Network\Probe; use Friendica\Util\Crypto; @@ -114,7 +112,7 @@ class Diaspora if (DI::config()->get("system", "relay_directly", false)) { // We distribute our stuff based on the parent to ensure that the thread will be complete - $parent = Item::selectFirst(['parent'], ['id' => $item_id]); + $parent = Item::selectFirst(['uri-id'], ['id' => $item_id]); if (!DBA::isResult($parent)) { return; } @@ -127,11 +125,10 @@ class Diaspora DBA::close($servers); // All tags of the current post - $condition = ['otype' => Term::OBJECT_TYPE_POST, 'type' => Term::HASHTAG, 'oid' => $parent['parent']]; - $tags = DBA::select('term', ['term'], $condition); + $tags = DBA::select('tag-view', ['name'], ['uri-id' => $parent['uri-id'], 'type' => Tag::HASHTAG]); $taglist = []; while ($tag = DBA::fetch($tags)) { - $taglist[] = $tag['term']; + $taglist[] = $tag['name']; } DBA::close($tags); @@ -255,19 +252,25 @@ class Diaspora * One of the parameters is a contact array. * This is done to avoid duplicates. * - * @param integer $thread The id of the thread - * @param array $contacts The previously fetched contacts + * @param array $parent The parent post + * @param array $contacts The previously fetched contacts * * @return array of relay servers * @throws \Exception */ - public static function participantsForThread($thread, array $contacts) + public static function participantsForThread(array $parent, array $contacts) { - $participation = DBA::select('participation-view', [], ['iid' => $thread]); + if (!in_array($parent['private'], [Item::PUBLIC, Item::UNLISTED])) { + return $contacts; + } - while ($contact = DBA::fetch($participation)) { - if (empty($contact['protocol'])) { - $contact['protocol'] = $contact['network']; + $items = Item::select(['author-id'], ['parent' => $parent['id']], ['group_by' => ['author-id']]); + while ($item = DBA::fetch($items)) { + $contact = DBA::selectFirst('contact', ['id', 'url', 'name', 'protocol', 'batch', 'network'], + ['id' => $item['author-id']]); + if (!DBA::isResult($contact)) { + // Shouldn't happen + continue; } $exists = false; @@ -278,11 +281,11 @@ class Diaspora } if (!$exists) { + Logger::info('Add participant to receiver list', ['item' => $parent['guid'], 'participant' => $contact['url']]); $contacts[] = $contact; } } - - DBA::close($participation); + DBA::close($items); return $contacts; } @@ -2254,18 +2257,32 @@ class Diaspora * @param array $importer Array of the importer user * @param object $data The message object * - * @return bool always true + * @return bool success * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ private static function receiveParticipation(array $importer, $data) { $author = strtolower(Strings::escapeTags(XML::unescape($data->author))); + $guid = Strings::escapeTags(XML::unescape($data->guid)); $parent_guid = Strings::escapeTags(XML::unescape($data->parent_guid)); - $contact_id = Contact::getIdForURL($author); - if (!$contact_id) { - Logger::log('Contact not found: '.$author); + $contact = self::allowedContactByHandle($importer, $author, true); + if (!$contact) { + return false; + } + + if (self::messageExists($importer["uid"], $guid)) { + return true; + } + + $parent_item = self::parentItem($importer["uid"], $parent_guid, $author, $contact); + if (!$parent_item) { + return false; + } + + if (!in_array($parent_item['private'], [Item::PUBLIC, Item::UNLISTED])) { + Logger::info('Item is not public, participation is ignored', ['parent_guid' => $parent_guid, 'guid' => $guid, 'author' => $author]); return false; } @@ -2275,36 +2292,48 @@ class Diaspora return false; } - $item = Item::selectFirst(['id'], ['guid' => $parent_guid, 'origin' => true, 'private' => [Item::PUBLIC, Item::UNLISTED]]); - if (!DBA::isResult($item)) { - Logger::log('Item not found, no origin or private: '.$parent_guid); - return false; - } + $author_contact = self::authorContactByUrl($contact, $person, $importer["uid"]); - $author_parts = explode('@', $author); - if (isset($author_parts[1])) { - $server = $author_parts[1]; - } else { - // Should never happen - $server = $author; - } + // Store participation + $datarray = []; - Logger::log('Received participation for ID: '.$item['id'].' - Contact: '.$contact_id.' - Server: '.$server, Logger::DEBUG); + $datarray["protocol"] = Conversation::PARCEL_DIASPORA; - if (!DBA::exists('participation', ['iid' => $item['id'], 'server' => $server])) { - DBA::insert('participation', ['iid' => $item['id'], 'cid' => $contact_id, 'fid' => $person['id'], 'server' => $server]); - } + $datarray["uid"] = $importer["uid"]; + $datarray["contact-id"] = $author_contact["cid"]; + $datarray["network"] = $author_contact["network"]; + + $datarray["owner-link"] = $datarray["author-link"] = $person["url"]; + $datarray["owner-id"] = $datarray["author-id"] = Contact::getIdForURL($person["url"], 0); + + $datarray["guid"] = $guid; + $datarray["uri"] = self::getUriFromGuid($author, $guid); + + $datarray["verb"] = Activity::FOLLOW; + $datarray["gravity"] = GRAVITY_ACTIVITY; + $datarray["parent-uri"] = $parent_item["uri"]; + + $datarray["object-type"] = Activity\ObjectType::NOTE; + + $datarray["body"] = Activity::FOLLOW; + + // Diaspora doesn't provide a date for a participation + $datarray["changed"] = $datarray["created"] = $datarray["edited"] = DateTimeFormat::utcNow(); + + $message_id = Item::insert($datarray); + + Logger::info('Participation stored', ['id' => $message_id, 'guid' => $guid, 'parent_guid' => $parent_guid, 'author' => $author]); // Send all existing comments and likes to the requesting server - $comments = Item::select(['id', 'parent', 'verb', 'self'], ['parent' => $item['id']]); + $comments = Item::select(['id', 'uri-id', 'parent'], ['parent' => $parent_item['id']]); while ($comment = Item::fetch($comments)) { if ($comment['id'] == $comment['parent']) { continue; } - Logger::info('Deliver participation', ['item' => $comment['id'], 'contact' => $contact_id]); - if (Worker::add(PRIORITY_HIGH, 'Delivery', Delivery::POST, $comment['id'], $contact_id)) { - ItemDeliveryData::incrementQueueCount($comment['id'], 1); + Logger::info('Deliver participation', ['item' => $comment['id'], 'contact' => $author_contact["cid"]]); + if (Worker::add(PRIORITY_HIGH, 'Delivery', Delivery::POST, $comment['id'], $author_contact["cid"])) { + Post\DeliveryData::incrementQueueCount($comment['uri-id'], 1); } } DBA::close($comments); @@ -2585,7 +2614,7 @@ class Diaspora } // Do we already have this item? - $fields = ['body', 'title', 'attach', 'tag', 'app', 'created', 'object-type', 'uri', 'guid', + $fields = ['body', 'title', 'attach', 'app', 'created', 'object-type', 'uri', 'guid', 'author-name', 'author-link', 'author-avatar']; $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => [Item::PUBLIC, Item::UNLISTED]]; $item = Item::selectFirst($fields, $condition); @@ -2629,7 +2658,7 @@ class Diaspora } if ($stored) { - $fields = ['body', 'title', 'attach', 'tag', 'app', 'created', 'object-type', 'uri', 'guid', + $fields = ['body', 'title', 'attach', 'app', 'created', 'object-type', 'uri', 'guid', 'author-name', 'author-link', 'author-avatar']; $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => [Item::PUBLIC, Item::UNLISTED]]; $item = Item::selectFirst($fields, $condition); @@ -2775,7 +2804,6 @@ class Diaspora Tag::storeFromBody($datarray['uri-id'], $datarray["body"]); - $datarray["tag"] = $original_item["tag"]; $datarray["attach"] = $original_item["attach"]; $datarray["app"] = $original_item["app"]; diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index 14a3c28ab9..baf439dc02 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -385,18 +385,10 @@ class Feed { $item["attach"] .= '[attach]href="' . $href . '" length="' . $length . '" type="' . $type . '"[/attach]'; } - $tags = ''; $taglist = []; $categories = $xpath->query("category", $entry); foreach ($categories AS $category) { - $hashtag = $category->nodeValue; - if ($tags != '') { - $tags .= ', '; - } - - $taglink = "#[url=" . DI::baseUrl() . "/search?tag=" . $hashtag . "]" . $hashtag . "[/url]"; - $tags .= $taglink; - $taglist[] = $hashtag; + $taglist[] = $category->nodeValue; } $body = trim(XML::getFirstNodeValue($xpath, 'atom:content/text()', $entry)); @@ -477,7 +469,6 @@ class Feed { // We always strip the title since it will be added in the page information $item["title"] = ""; $item["body"] = $item["body"] . add_page_info($item["plink"], false, $preview, ($contact["fetch_further_information"] == 2), $contact["ffi_keyword_blacklist"]); - $item["tag"] = add_page_keywords($item["plink"], $preview, ($contact["fetch_further_information"] == 2), $contact["ffi_keyword_blacklist"]); $taglist = get_page_keywords($item["plink"], $preview, ($contact["fetch_further_information"] == 2), $contact["ffi_keyword_blacklist"]); $item["object-type"] = Activity\ObjectType::BOOKMARK; unset($item["attach"]); @@ -487,14 +478,10 @@ class Feed { } if (!empty($contact["fetch_further_information"]) && ($contact["fetch_further_information"] == 3)) { - if (!empty($tags)) { - $item["tag"] = $tags; - } else { - // @todo $preview is never set in this case, is it intended? - @MrPetovan 2018-02-13 - $item["tag"] = add_page_keywords($item["plink"], $preview, true, $contact["ffi_keyword_blacklist"]); + if (empty($taglist)) { $taglist = get_page_keywords($item["plink"], $preview, true, $contact["ffi_keyword_blacklist"]); } - $item["body"] .= "\n" . $item['tag']; + $item["body"] .= "\n" . self::tagToString($taglist); } else { $taglist = []; } @@ -540,6 +527,27 @@ class Feed { return ["header" => $author, "items" => $items]; } + /** + * Convert a tag array to a tag string + * + * @param array $tags + * @return string tag string + */ + private static function tagToString(array $tags) + { + $tagstr = ''; + + foreach ($tags as $tag) { + if ($tagstr != "") { + $tagstr .= ", "; + } + + $tagstr .= "#[url=" . DI::baseUrl() . "/search?tag=" . urlencode($tag) . "]" . $tag . "[/url]"; + } + + return $tagstr; + } + private static function titleIsBody($title, $body) { $title = strip_tags($title); diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php index ccec6d9348..cff50f08e0 100644 --- a/src/Protocol/OStatus.php +++ b/src/Protocol/OStatus.php @@ -2081,14 +2081,9 @@ class OStatus XML::addElement($doc, $entry, "ostatus:conversation", $conversation_uri, $attributes); } - $tags = item::getFeedTags($item); - - if (count($tags)) { - foreach ($tags as $t) { - if ($t[0] == "@") { - $mentioned[$t[1]] = $t[1]; - } - } + // uri-id isn't present for follow entry pseudo-items + foreach (Tag::getByURIId($item['uri-id'] ?? 0) as $tag) { + $mentioned[$tag['url']] = $tag['url']; } // Make sure that mentions are accepted (GNU Social has problems with mixing HTTP and HTTPS) @@ -2138,9 +2133,9 @@ class OStatus } if (count($tags)) { - foreach ($tags as $t) { - if ($t[0] != "@") { - XML::addElement($doc, $entry, "category", "", ["term" => $t[2]]); + foreach ($tags as $tag) { + if ($tag['type'] == Tag::HASHTAG) { + XML::addElement($doc, $entry, "category", "", ["term" => $tag['name']]); } } } diff --git a/src/Worker/APDelivery.php b/src/Worker/APDelivery.php index 60752896fc..609bb88886 100644 --- a/src/Worker/APDelivery.php +++ b/src/Worker/APDelivery.php @@ -23,7 +23,8 @@ namespace Friendica\Worker; use Friendica\Core\Logger; use Friendica\Core\Worker; -use Friendica\Model\ItemDeliveryData; +use Friendica\Model\Item; +use Friendica\Model\Post; use Friendica\Protocol\ActivityPub; use Friendica\Util\HTTPSignature; @@ -67,10 +68,13 @@ class APDelivery } } + // This should never fail and is temporariy (until the move to ) + $item = Item::selectFirst(['uri-id'], ['id' => $target_id]); + if (!$success && !Worker::defer() && in_array($cmd, [Delivery::POST])) { - ItemDeliveryData::incrementQueueFailed($target_id); + Post\DeliveryData::incrementQueueFailed($item['uri-id']); } elseif ($success && in_array($cmd, [Delivery::POST])) { - ItemDeliveryData::incrementQueueDone($target_id, ItemDeliveryData::ACTIVITYPUB); + Post\DeliveryData::incrementQueueDone($item['uri-id'], Post\DeliveryData::ACTIVITYPUB); } } } diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index 94a0f4902b..0a865cb3a7 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -58,14 +58,14 @@ class Delivery if ($cmd == self::MAIL) { $target_item = DBA::selectFirst('mail', [], ['id' => $target_id]); if (!DBA::isResult($target_item)) { - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } $uid = $target_item['uid']; } elseif ($cmd == self::SUGGESTION) { $target_item = DBA::selectFirst('fsuggest', [], ['id' => $target_id]); if (!DBA::isResult($target_item)) { - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } $uid = $target_item['uid']; @@ -75,7 +75,7 @@ class Delivery } else { $item = Model\Item::selectFirst(['parent'], ['id' => $target_id]); if (!DBA::isResult($item) || empty($item['parent'])) { - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } $parent_id = intval($item['parent']); @@ -97,13 +97,13 @@ class Delivery if (empty($target_item)) { Logger::log('Item ' . $target_id . "wasn't found. Quitting here."); - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } if (empty($parent)) { Logger::log('Parent ' . $parent_id . ' for item ' . $target_id . "wasn't found. Quitting here."); - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } @@ -113,7 +113,7 @@ class Delivery $uid = $target_item['uid']; } else { Logger::log('Only public users for item ' . $target_id, Logger::DEBUG); - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } @@ -127,7 +127,7 @@ class Delivery if (!empty($contact_id) && Model\Contact::isArchived($contact_id)) { Logger::info('Contact is archived', ['id' => $contact_id, 'cmd' => $cmd, 'item' => $target_item['id']]); - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } @@ -187,7 +187,7 @@ class Delivery $owner = Model\User::getOwnerDataById($uid); if (!DBA::isResult($owner)) { - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } @@ -196,12 +196,12 @@ class Delivery ['id' => $contact_id, 'blocked' => false, 'pending' => false, 'self' => false] ); if (!DBA::isResult($contact)) { - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } if (Network::isUrlBlocked($contact['url'])) { - self::setFailedQueue($cmd, $target_id); + self::setFailedQueue($cmd, $target_item); return; } @@ -242,16 +242,16 @@ class Delivery /** * Increased the "failed" counter in the item delivery data * - * @param string $cmd Command - * @param integer $id Item id + * @param string $cmd Command + * @param array $item Item array */ - private static function setFailedQueue(string $cmd, int $id) + private static function setFailedQueue(string $cmd, array $item) { if (!in_array($cmd, [Delivery::POST, Delivery::POKE])) { return; } - Model\ItemDeliveryData::incrementQueueFailed($id); + Model\Post\DeliveryData::incrementQueueFailed($item['uri-id'] ?? $item['id']); } /** @@ -335,13 +335,13 @@ class Delivery DFRN::import($atom, $target_importer); if (in_array($cmd, [Delivery::POST, Delivery::POKE])) { - Model\ItemDeliveryData::incrementQueueDone($target_item['id'], Model\ItemDeliveryData::DFRN); + Model\Post\DeliveryData::incrementQueueDone($target_item['uri-id'], Model\Post\DeliveryData::DFRN); } return; } - $protocol = Model\ItemDeliveryData::DFRN; + $protocol = Model\Post\DeliveryData::DFRN; // We don't have a relationship with contacts on a public post. // Se we transmit with the new method and via Diaspora as a fallback @@ -357,9 +357,9 @@ class Delivery if (in_array($cmd, [Delivery::POST, Delivery::POKE])) { if (($deliver_status >= 200) && ($deliver_status <= 299)) { - Model\ItemDeliveryData::incrementQueueDone($target_item['id'], $protocol); + Model\Post\DeliveryData::incrementQueueDone($target_item['uri-id'], $protocol); } else { - Model\ItemDeliveryData::incrementQueueFailed($target_item['id']); + Model\Post\DeliveryData::incrementQueueFailed($target_item['uri-id']); } } return; @@ -376,11 +376,11 @@ class Delivery if ($deliver_status < 200) { // Legacy DFRN $deliver_status = DFRN::deliver($owner, $contact, $atom); - $protocol = Model\ItemDeliveryData::LEGACY_DFRN; + $protocol = Model\Post\DeliveryData::LEGACY_DFRN; } } else { $deliver_status = DFRN::deliver($owner, $contact, $atom); - $protocol = Model\ItemDeliveryData::LEGACY_DFRN; + $protocol = Model\Post\DeliveryData::LEGACY_DFRN; } Logger::info('DFRN Delivery', ['cmd' => $cmd, 'url' => $contact['url'], 'guid' => ($target_item['guid'] ?? '') ?: $target_item['id'], 'return' => $deliver_status]); @@ -390,7 +390,7 @@ class Delivery Model\Contact::unmarkForArchival($contact); if (in_array($cmd, [Delivery::POST, Delivery::POKE])) { - Model\ItemDeliveryData::incrementQueueDone($target_item['id'], $protocol); + Model\Post\DeliveryData::incrementQueueDone($target_item['uri-id'], $protocol); } } else { // The message could not be delivered. We mark the contact as "dead" @@ -398,7 +398,7 @@ class Delivery Logger::info('Delivery failed: defer message', ['id' => ($target_item['guid'] ?? '') ?: $target_item['id']]); if (!Worker::defer() && in_array($cmd, [Delivery::POST, Delivery::POKE])) { - Model\ItemDeliveryData::incrementQueueFailed($target_item['id']); + Model\Post\DeliveryData::incrementQueueFailed($target_item['uri-id']); } } } @@ -475,7 +475,7 @@ class Delivery Model\Contact::unmarkForArchival($contact); if (in_array($cmd, [Delivery::POST, Delivery::POKE])) { - Model\ItemDeliveryData::incrementQueueDone($target_item['id'], Model\ItemDeliveryData::DIASPORA); + Model\Post\DeliveryData::incrementQueueDone($target_item['uri-id'], Model\Post\DeliveryData::DIASPORA); } } else { // The message could not be delivered. We mark the contact as "dead" @@ -490,10 +490,10 @@ class Delivery Logger::info('Delivery failed: defer message', ['id' => ($target_item['guid'] ?? '') ?: $target_item['id']]); // defer message for redelivery if (!Worker::defer() && in_array($cmd, [Delivery::POST, Delivery::POKE])) { - Model\ItemDeliveryData::incrementQueueFailed($target_item['id']); + Model\Post\DeliveryData::incrementQueueFailed($target_item['uri-id']); } } elseif (in_array($cmd, [Delivery::POST, Delivery::POKE])) { - Model\ItemDeliveryData::incrementQueueFailed($target_item['id']); + Model\Post\DeliveryData::incrementQueueFailed($target_item['uri-id']); } } } @@ -603,7 +603,7 @@ class Delivery Email::send($addr, $subject, $headers, $target_item); - Model\ItemDeliveryData::incrementQueueDone($target_item['id'], Model\ItemDeliveryData::MAIL); + Model\Post\DeliveryData::incrementQueueDone($target_item['uri-id'], Model\Post\DeliveryData::MAIL); Logger::info('Delivered via mail', ['guid' => $target_item['guid'], 'to' => $addr, 'subject' => $subject]); } diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index 35a228fce1..b93b1dd74b 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -32,7 +32,7 @@ use Friendica\Model\Contact; use Friendica\Model\Conversation; use Friendica\Model\Group; use Friendica\Model\Item; -use Friendica\Model\ItemDeliveryData; +use Friendica\Model\Post; use Friendica\Model\PushSubscriber; use Friendica\Model\User; use Friendica\Network\Probe; @@ -441,7 +441,7 @@ class Notifier // Fetch the participation list // The function will ensure that there are no duplicates - $relay_list = Diaspora::participantsForThread($target_id, $relay_list); + $relay_list = Diaspora::participantsForThread($parent, $relay_list); // Add the relay to the list, avoid duplicates. // Don't send community posts to the relay. Forum posts via the Diaspora protocol are looking ugly. @@ -573,7 +573,7 @@ class Notifier /// @TODO Redeliver/queue these items on failure, though there is no contact record $delivery_queue_count++; Salmon::slapper($owner, $url, $slap); - ItemDeliveryData::incrementQueueDone($target_id, ItemDeliveryData::OSTATUS); + Post\DeliveryData::incrementQueueDone($target_item['uri-id'], Post\DeliveryData::OSTATUS); } } @@ -595,11 +595,11 @@ class Notifier // Workaround for pure connector posts if (in_array($cmd, [Delivery::POST, Delivery::POKE])) { if ($delivery_queue_count == 0) { - ItemDeliveryData::incrementQueueDone($target_item['id']); + Post\DeliveryData::incrementQueueDone($target_item['uri-id']); $delivery_queue_count = 1; } - ItemDeliveryData::incrementQueueCount($target_item['id'], $delivery_queue_count); + Post\DeliveryData::incrementQueueCount($target_item['uri-id'], $delivery_queue_count); } } diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 7ba2c38a36..9cc03aa5d0 100755 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -51,7 +51,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1341); + define('DB_UPDATE_VERSION', 1345); } return [ @@ -754,6 +754,7 @@ return [ "icid" => ["icid"], "iaid" => ["iaid"], "psid_wall" => ["psid", "wall"], + "uri-id" => ["uri-id"], ] ], "item-activity" => [ @@ -803,25 +804,6 @@ return [ "uri-id" => ["uri-id"] ] ], - "item-delivery-data" => [ - "comment" => "Delivery data for items", - "fields" => [ - "iid" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "relation" => ["item" => "id"], "comment" => "Item id"], - "postopts" => ["type" => "text", "comment" => "External post connectors add their network name to this comma-separated string to identify that they should be delivered to these networks during delivery"], - "inform" => ["type" => "mediumtext", "comment" => "Additional receivers of the linked item"], - "queue_count" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Initial number of delivery recipients, used as item.delivery_queue_count"], - "queue_done" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries, used as item.delivery_queue_done"], - "queue_failed" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of unsuccessful deliveries, used as item.delivery_queue_failed"], - "activitypub" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via ActivityPub"], - "dfrn" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via DFRN"], - "legacy_dfrn" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via legacy DFRN"], - "diaspora" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via Diaspora"], - "ostatus" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via OStatus"], - ], - "indexes" => [ - "PRIMARY" => ["iid"], - ] - ], "item-uri" => [ "comment" => "URI and GUID for items", "fields" => [ @@ -926,6 +908,8 @@ return [ "link" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], "iid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["item" => "id"], "comment" => "item.id"], "parent" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["item" => "id"], "comment" => ""], + "uri-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Item-uri id of the related post"], + "parent-uri-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Item-uri id of the parent of the related post"], "seen" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "verb" => ["type" => "varchar(100)", "not null" => "1", "default" => "", "comment" => ""], "otype" => ["type" => "varchar(10)", "not null" => "1", "default" => "", "comment" => ""], @@ -944,8 +928,8 @@ return [ "fields" => [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], "notify-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["notify" => "id"], "comment" => ""], - "master-parent-item" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["item" => "id"], - "comment" => ""], + "master-parent-item" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "relation" => ["item" => "id"], "comment" => ""], + "master-parent-uri-id" => ["type" => "int unsigned", "relation" => ["item-uri" => "id"], "comment" => "Item-uri id of the parent of the related post"], "parent-item" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "comment" => ""], "receiver-uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "relation" => ["user" => "uid"], "comment" => "User id"], @@ -1305,6 +1289,25 @@ return [ "url" => ["url"] ] ], + "post-delivery-data" => [ + "comment" => "Delivery data for items", + "fields" => [ + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "relation" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "postopts" => ["type" => "text", "comment" => "External post connectors add their network name to this comma-separated string to identify that they should be delivered to these networks during delivery"], + "inform" => ["type" => "mediumtext", "comment" => "Additional receivers of the linked item"], + "queue_count" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Initial number of delivery recipients, used as item.delivery_queue_count"], + "queue_done" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries, used as item.delivery_queue_done"], + "queue_failed" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of unsuccessful deliveries, used as item.delivery_queue_failed"], + "activitypub" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via ActivityPub"], + "dfrn" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via DFRN"], + "legacy_dfrn" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via legacy DFRN"], + "diaspora" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via Diaspora"], + "ostatus" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via OStatus"], + ], + "indexes" => [ + "PRIMARY" => ["uri-id"], + ] + ], "post-tag" => [ "comment" => "post relation to tags", "fields" => [ diff --git a/static/dbview.config.php b/static/dbview.config.php index d6ea944a44..13408d27a9 100755 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -186,20 +186,6 @@ return [ INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self` INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid`" ], - "participation-view" => [ - "fields" => [ - "iid" => ["participation", "iid"], - "id" => ["contact", "id"], - "url" => ["contact", "url"], - "name" => ["contact", "name"], - "protocol" => ["contact", "protocol"], - "batch" => "CASE `contact`.`batch` WHEN '' THEN `fcontact`.`batch` ELSE `contact`.`batch` END", - "network" => "CASE `fcontact`.`network` WHEN '' THEN `contact`.`network` ELSE `fcontact`.`network` END", - ], - "query" => "FROM `participation` - INNER JOIN `contact` ON `contact`.`id` = `participation`.`cid` AND NOT `contact`.`archive` - INNER JOIN `fcontact` ON `fcontact`.`id` = `participation`.`fid`" - ], "pending-view" => [ "fields" => [ "id" => ["register", "id"], @@ -220,6 +206,25 @@ return [ INNER JOIN `contact` ON `register`.`uid` = `contact`.`uid` INNER JOIN `user` ON `register`.`uid` = `user`.`uid`" ], + "tag-search-view" => [ + "fields" => [ + "uri-id" => ["post-tag", "uri-id"], + "iid" => ["item", "id"], + "uri" => ["item", "uri"], + "guid" => ["item", "guid"], + "uid" => ["item", "uid"], + "private" => ["item", "private"], + "wall" => ["item", "wall"], + "origin" => ["item", "origin"], + "gravity" => ["item", "gravity"], + "received" => ["item", "received"], + "name" => ["tag", "name"], + ], + "query" => "FROM `post-tag` + INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` + INNER JOIN `item` ON `item`.`uri-id` = `post-tag`.`uri-id` + WHERE `post-tag`.`type` = 1" + ], "workerqueue-view" => [ "fields" => [ "pid" => ["process", "pid"], diff --git a/tests/datasets/api.fixture.php b/tests/datasets/api.fixture.php index c9c16a33fc..fb11ae3476 100644 --- a/tests/datasets/api.fixture.php +++ b/tests/datasets/api.fixture.php @@ -27,7 +27,7 @@ return [ 'photo', 'workerqueue', 'mail', - 'item-delivery-data', + 'post-delivery-data', // Base test config to avoid notice messages 'config' => [ [ @@ -103,6 +103,7 @@ return [ 'item' => [ [ 'id' => 1, + 'uri-id' => 1, 'visible' => 1, 'contact-id' => 42, 'author-id' => 42, @@ -123,6 +124,7 @@ return [ ], [ 'id' => 2, + 'uri-id' => 2, 'visible' => 1, 'contact-id' => 42, 'author-id' => 42, @@ -140,6 +142,7 @@ return [ [ 'id' => 3, + 'uri-id' => 3, 'visible' => 1, 'contact-id' => 43, 'author-id' => 43, @@ -156,6 +159,7 @@ return [ ], [ 'id' => 4, + 'uri-id' => 4, 'visible' => 1, 'contact-id' => 44, 'author-id' => 44, @@ -173,6 +177,7 @@ return [ [ 'id' => 5, + 'uri-id' => 5, 'visible' => 1, 'contact-id' => 42, 'author-id' => 42, @@ -193,6 +198,7 @@ return [ ], [ 'id' => 6, + 'uri-id' => 6, 'visible' => 1, 'contact-id' => 44, 'author-id' => 44, diff --git a/view/js/perfect-scrollbar/LICENSE b/view/js/perfect-scrollbar/LICENSE new file mode 100644 index 0000000000..8df97031a8 --- /dev/null +++ b/view/js/perfect-scrollbar/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Hyunje Alex Jun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/view/js/perfect-scrollbar/README.md b/view/js/perfect-scrollbar/README.md new file mode 100644 index 0000000000..9b445dbf83 --- /dev/null +++ b/view/js/perfect-scrollbar/README.md @@ -0,0 +1,5 @@ +# Bower Package of perfect-scrollbar + +This is the [Bower](https://bower.io/) package of perfect-scrollbar. + +For details and usage please read more on [https://github.com/noraesae/perfect-scrollbar](https://github.com/noraesae/perfect-scrollbar). diff --git a/view/js/perfect-scrollbar/bower.json b/view/js/perfect-scrollbar/bower.json new file mode 100644 index 0000000000..423477446f --- /dev/null +++ b/view/js/perfect-scrollbar/bower.json @@ -0,0 +1,18 @@ +{ + "name": "perfect-scrollbar", + "version": "0.6.16", + "homepage": "http://noraesae.github.io/perfect-scrollbar/", + "authors": [ + "Hyunje Jun " + ], + "description": "Minimalistic but perfect custom scrollbar plugin", + "main": [ + "css/perfect-scrollbar.css", + "js/perfect-scrollbar.js" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "bower_components" + ] +} diff --git a/view/js/perfect-scrollbar/css/perfect-scrollbar.css b/view/js/perfect-scrollbar/css/perfect-scrollbar.css new file mode 100644 index 0000000000..6f03551ade --- /dev/null +++ b/view/js/perfect-scrollbar/css/perfect-scrollbar.css @@ -0,0 +1,113 @@ +/* perfect-scrollbar v0.6.16 */ +.ps-container { + -ms-touch-action: auto; + touch-action: auto; + overflow: hidden !important; + -ms-overflow-style: none; } + @supports (-ms-overflow-style: none) { + .ps-container { + overflow: auto !important; } } + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + .ps-container { + overflow: auto !important; } } + .ps-container.ps-active-x > .ps-scrollbar-x-rail, + .ps-container.ps-active-y > .ps-scrollbar-y-rail { + display: block; + background-color: transparent; } + .ps-container.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail { + background-color: #eee; + opacity: 0.9; } + .ps-container.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail > .ps-scrollbar-x { + background-color: #999; + height: 11px; } + .ps-container.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail { + background-color: #eee; + opacity: 0.9; } + .ps-container.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail > .ps-scrollbar-y { + background-color: #999; + width: 11px; } + .ps-container > .ps-scrollbar-x-rail { + display: none; + position: absolute; + /* please don't change 'position' */ + opacity: 0; + -webkit-transition: background-color .2s linear, opacity .2s linear; + -o-transition: background-color .2s linear, opacity .2s linear; + -moz-transition: background-color .2s linear, opacity .2s linear; + transition: background-color .2s linear, opacity .2s linear; + bottom: 0px; + /* there must be 'bottom' for ps-scrollbar-x-rail */ + height: 15px; } + .ps-container > .ps-scrollbar-x-rail > .ps-scrollbar-x { + position: absolute; + /* please don't change 'position' */ + background-color: #aaa; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out; + transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out; + -o-transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out; + -moz-transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out; + transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out; + transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -webkit-border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out; + bottom: 2px; + /* there must be 'bottom' for ps-scrollbar-x */ + height: 6px; } + .ps-container > .ps-scrollbar-x-rail:hover > .ps-scrollbar-x, .ps-container > .ps-scrollbar-x-rail:active > .ps-scrollbar-x { + height: 11px; } + .ps-container > .ps-scrollbar-y-rail { + display: none; + position: absolute; + /* please don't change 'position' */ + opacity: 0; + -webkit-transition: background-color .2s linear, opacity .2s linear; + -o-transition: background-color .2s linear, opacity .2s linear; + -moz-transition: background-color .2s linear, opacity .2s linear; + transition: background-color .2s linear, opacity .2s linear; + right: 0; + /* there must be 'right' for ps-scrollbar-y-rail */ + width: 15px; } + .ps-container > .ps-scrollbar-y-rail > .ps-scrollbar-y { + position: absolute; + /* please don't change 'position' */ + background-color: #aaa; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out; + transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out; + -o-transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out; + -moz-transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out; + transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out; + transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -webkit-border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out; + right: 2px; + /* there must be 'right' for ps-scrollbar-y */ + width: 6px; } + .ps-container > .ps-scrollbar-y-rail:hover > .ps-scrollbar-y, .ps-container > .ps-scrollbar-y-rail:active > .ps-scrollbar-y { + width: 11px; } + .ps-container:hover.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail { + background-color: #eee; + opacity: 0.9; } + .ps-container:hover.ps-in-scrolling.ps-x > .ps-scrollbar-x-rail > .ps-scrollbar-x { + background-color: #999; + height: 11px; } + .ps-container:hover.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail { + background-color: #eee; + opacity: 0.9; } + .ps-container:hover.ps-in-scrolling.ps-y > .ps-scrollbar-y-rail > .ps-scrollbar-y { + background-color: #999; + width: 11px; } + .ps-container:hover > .ps-scrollbar-x-rail, + .ps-container:hover > .ps-scrollbar-y-rail { + opacity: 0.6; } + .ps-container:hover > .ps-scrollbar-x-rail:hover { + background-color: #eee; + opacity: 0.9; } + .ps-container:hover > .ps-scrollbar-x-rail:hover > .ps-scrollbar-x { + background-color: #999; } + .ps-container:hover > .ps-scrollbar-y-rail:hover { + background-color: #eee; + opacity: 0.9; } + .ps-container:hover > .ps-scrollbar-y-rail:hover > .ps-scrollbar-y { + background-color: #999; } diff --git a/view/js/perfect-scrollbar/css/perfect-scrollbar.min.css b/view/js/perfect-scrollbar/css/perfect-scrollbar.min.css new file mode 100644 index 0000000000..a0ad63ca51 --- /dev/null +++ b/view/js/perfect-scrollbar/css/perfect-scrollbar.min.css @@ -0,0 +1,2 @@ +/* perfect-scrollbar v0.6.16 */ +.ps-container{-ms-touch-action:auto;touch-action:auto;overflow:hidden !important;-ms-overflow-style:none}@supports (-ms-overflow-style: none){.ps-container{overflow:auto !important}}@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none){.ps-container{overflow:auto !important}}.ps-container.ps-active-x>.ps-scrollbar-x-rail,.ps-container.ps-active-y>.ps-scrollbar-y-rail{display:block;background-color:transparent}.ps-container.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail{background-color:#eee;opacity:.9}.ps-container.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail>.ps-scrollbar-x{background-color:#999;height:11px}.ps-container.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail{background-color:#eee;opacity:.9}.ps-container.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail>.ps-scrollbar-y{background-color:#999;width:11px}.ps-container>.ps-scrollbar-x-rail{display:none;position:absolute;opacity:0;-webkit-transition:background-color .2s linear, opacity .2s linear;-o-transition:background-color .2s linear, opacity .2s linear;-moz-transition:background-color .2s linear, opacity .2s linear;transition:background-color .2s linear, opacity .2s linear;bottom:0px;height:15px}.ps-container>.ps-scrollbar-x-rail>.ps-scrollbar-x{position:absolute;background-color:#aaa;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out;-o-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out;-moz-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -webkit-border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out;bottom:2px;height:6px}.ps-container>.ps-scrollbar-x-rail:hover>.ps-scrollbar-x,.ps-container>.ps-scrollbar-x-rail:active>.ps-scrollbar-x{height:11px}.ps-container>.ps-scrollbar-y-rail{display:none;position:absolute;opacity:0;-webkit-transition:background-color .2s linear, opacity .2s linear;-o-transition:background-color .2s linear, opacity .2s linear;-moz-transition:background-color .2s linear, opacity .2s linear;transition:background-color .2s linear, opacity .2s linear;right:0;width:15px}.ps-container>.ps-scrollbar-y-rail>.ps-scrollbar-y{position:absolute;background-color:#aaa;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, -webkit-border-radius .2s ease-in-out;-o-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out;-moz-transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out;transition:background-color .2s linear, height .2s linear, width .2s ease-in-out, border-radius .2s ease-in-out, -webkit-border-radius .2s ease-in-out, -moz-border-radius .2s ease-in-out;right:2px;width:6px}.ps-container>.ps-scrollbar-y-rail:hover>.ps-scrollbar-y,.ps-container>.ps-scrollbar-y-rail:active>.ps-scrollbar-y{width:11px}.ps-container:hover.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail{background-color:#eee;opacity:.9}.ps-container:hover.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail>.ps-scrollbar-x{background-color:#999;height:11px}.ps-container:hover.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail{background-color:#eee;opacity:.9}.ps-container:hover.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail>.ps-scrollbar-y{background-color:#999;width:11px}.ps-container:hover>.ps-scrollbar-x-rail,.ps-container:hover>.ps-scrollbar-y-rail{opacity:.6}.ps-container:hover>.ps-scrollbar-x-rail:hover{background-color:#eee;opacity:.9}.ps-container:hover>.ps-scrollbar-x-rail:hover>.ps-scrollbar-x{background-color:#999}.ps-container:hover>.ps-scrollbar-y-rail:hover{background-color:#eee;opacity:.9}.ps-container:hover>.ps-scrollbar-y-rail:hover>.ps-scrollbar-y{background-color:#999} diff --git a/view/js/perfect-scrollbar/js/perfect-scrollbar.jquery.js b/view/js/perfect-scrollbar/js/perfect-scrollbar.jquery.js new file mode 100644 index 0000000000..5ae830f124 --- /dev/null +++ b/view/js/perfect-scrollbar/js/perfect-scrollbar.jquery.js @@ -0,0 +1,1577 @@ +/* perfect-scrollbar v0.6.16 */ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0) { + classes.splice(idx, 1); + } + element.className = classes.join(' '); +} + +exports.add = function (element, className) { + if (element.classList) { + element.classList.add(className); + } else { + oldAdd(element, className); + } +}; + +exports.remove = function (element, className) { + if (element.classList) { + element.classList.remove(className); + } else { + oldRemove(element, className); + } +}; + +exports.list = function (element) { + if (element.classList) { + return Array.prototype.slice.apply(element.classList); + } else { + return element.className.split(' '); + } +}; + +},{}],3:[function(require,module,exports){ +'use strict'; + +var DOM = {}; + +DOM.e = function (tagName, className) { + var element = document.createElement(tagName); + element.className = className; + return element; +}; + +DOM.appendTo = function (child, parent) { + parent.appendChild(child); + return child; +}; + +function cssGet(element, styleName) { + return window.getComputedStyle(element)[styleName]; +} + +function cssSet(element, styleName, styleValue) { + if (typeof styleValue === 'number') { + styleValue = styleValue.toString() + 'px'; + } + element.style[styleName] = styleValue; + return element; +} + +function cssMultiSet(element, obj) { + for (var key in obj) { + var val = obj[key]; + if (typeof val === 'number') { + val = val.toString() + 'px'; + } + element.style[key] = val; + } + return element; +} + +DOM.css = function (element, styleNameOrObject, styleValue) { + if (typeof styleNameOrObject === 'object') { + // multiple set with object + return cssMultiSet(element, styleNameOrObject); + } else { + if (typeof styleValue === 'undefined') { + return cssGet(element, styleNameOrObject); + } else { + return cssSet(element, styleNameOrObject, styleValue); + } + } +}; + +DOM.matches = function (element, query) { + if (typeof element.matches !== 'undefined') { + return element.matches(query); + } else { + if (typeof element.matchesSelector !== 'undefined') { + return element.matchesSelector(query); + } else if (typeof element.webkitMatchesSelector !== 'undefined') { + return element.webkitMatchesSelector(query); + } else if (typeof element.mozMatchesSelector !== 'undefined') { + return element.mozMatchesSelector(query); + } else if (typeof element.msMatchesSelector !== 'undefined') { + return element.msMatchesSelector(query); + } + } +}; + +DOM.remove = function (element) { + if (typeof element.remove !== 'undefined') { + element.remove(); + } else { + if (element.parentNode) { + element.parentNode.removeChild(element); + } + } +}; + +DOM.queryChildren = function (element, selector) { + return Array.prototype.filter.call(element.childNodes, function (child) { + return DOM.matches(child, selector); + }); +}; + +module.exports = DOM; + +},{}],4:[function(require,module,exports){ +'use strict'; + +var EventElement = function (element) { + this.element = element; + this.events = {}; +}; + +EventElement.prototype.bind = function (eventName, handler) { + if (typeof this.events[eventName] === 'undefined') { + this.events[eventName] = []; + } + this.events[eventName].push(handler); + this.element.addEventListener(eventName, handler, false); +}; + +EventElement.prototype.unbind = function (eventName, handler) { + var isHandlerProvided = (typeof handler !== 'undefined'); + this.events[eventName] = this.events[eventName].filter(function (hdlr) { + if (isHandlerProvided && hdlr !== handler) { + return true; + } + this.element.removeEventListener(eventName, hdlr, false); + return false; + }, this); +}; + +EventElement.prototype.unbindAll = function () { + for (var name in this.events) { + this.unbind(name); + } +}; + +var EventManager = function () { + this.eventElements = []; +}; + +EventManager.prototype.eventElement = function (element) { + var ee = this.eventElements.filter(function (eventElement) { + return eventElement.element === element; + })[0]; + if (typeof ee === 'undefined') { + ee = new EventElement(element); + this.eventElements.push(ee); + } + return ee; +}; + +EventManager.prototype.bind = function (element, eventName, handler) { + this.eventElement(element).bind(eventName, handler); +}; + +EventManager.prototype.unbind = function (element, eventName, handler) { + this.eventElement(element).unbind(eventName, handler); +}; + +EventManager.prototype.unbindAll = function () { + for (var i = 0; i < this.eventElements.length; i++) { + this.eventElements[i].unbindAll(); + } +}; + +EventManager.prototype.once = function (element, eventName, handler) { + var ee = this.eventElement(element); + var onceHandler = function (e) { + ee.unbind(eventName, onceHandler); + handler(e); + }; + ee.bind(eventName, onceHandler); +}; + +module.exports = EventManager; + +},{}],5:[function(require,module,exports){ +'use strict'; + +module.exports = (function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return function () { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + }; +})(); + +},{}],6:[function(require,module,exports){ +'use strict'; + +var cls = require('./class'); +var dom = require('./dom'); + +var toInt = exports.toInt = function (x) { + return parseInt(x, 10) || 0; +}; + +var clone = exports.clone = function (obj) { + if (!obj) { + return null; + } else if (obj.constructor === Array) { + return obj.map(clone); + } else if (typeof obj === 'object') { + var result = {}; + for (var key in obj) { + result[key] = clone(obj[key]); + } + return result; + } else { + return obj; + } +}; + +exports.extend = function (original, source) { + var result = clone(original); + for (var key in source) { + result[key] = clone(source[key]); + } + return result; +}; + +exports.isEditable = function (el) { + return dom.matches(el, "input,[contenteditable]") || + dom.matches(el, "select,[contenteditable]") || + dom.matches(el, "textarea,[contenteditable]") || + dom.matches(el, "button,[contenteditable]"); +}; + +exports.removePsClasses = function (element) { + var clsList = cls.list(element); + for (var i = 0; i < clsList.length; i++) { + var className = clsList[i]; + if (className.indexOf('ps-') === 0) { + cls.remove(element, className); + } + } +}; + +exports.outerWidth = function (element) { + return toInt(dom.css(element, 'width')) + + toInt(dom.css(element, 'paddingLeft')) + + toInt(dom.css(element, 'paddingRight')) + + toInt(dom.css(element, 'borderLeftWidth')) + + toInt(dom.css(element, 'borderRightWidth')); +}; + +exports.startScrolling = function (element, axis) { + cls.add(element, 'ps-in-scrolling'); + if (typeof axis !== 'undefined') { + cls.add(element, 'ps-' + axis); + } else { + cls.add(element, 'ps-x'); + cls.add(element, 'ps-y'); + } +}; + +exports.stopScrolling = function (element, axis) { + cls.remove(element, 'ps-in-scrolling'); + if (typeof axis !== 'undefined') { + cls.remove(element, 'ps-' + axis); + } else { + cls.remove(element, 'ps-x'); + cls.remove(element, 'ps-y'); + } +}; + +exports.env = { + isWebKit: 'WebkitAppearance' in document.documentElement.style, + supportsTouch: (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch), + supportsIePointer: window.navigator.msMaxTouchPoints !== null +}; + +},{"./class":2,"./dom":3}],7:[function(require,module,exports){ +'use strict'; + +var destroy = require('./plugin/destroy'); +var initialize = require('./plugin/initialize'); +var update = require('./plugin/update'); + +module.exports = { + initialize: initialize, + update: update, + destroy: destroy +}; + +},{"./plugin/destroy":9,"./plugin/initialize":17,"./plugin/update":21}],8:[function(require,module,exports){ +'use strict'; + +module.exports = { + handlers: ['click-rail', 'drag-scrollbar', 'keyboard', 'wheel', 'touch'], + maxScrollbarLength: null, + minScrollbarLength: null, + scrollXMarginOffset: 0, + scrollYMarginOffset: 0, + suppressScrollX: false, + suppressScrollY: false, + swipePropagation: true, + useBothWheelAxes: false, + wheelPropagation: false, + wheelSpeed: 1, + theme: 'default' +}; + +},{}],9:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var dom = require('../lib/dom'); +var instances = require('./instances'); + +module.exports = function (element) { + var i = instances.get(element); + + if (!i) { + return; + } + + i.event.unbindAll(); + dom.remove(i.scrollbarX); + dom.remove(i.scrollbarY); + dom.remove(i.scrollbarXRail); + dom.remove(i.scrollbarYRail); + _.removePsClasses(element); + + instances.remove(element); +}; + +},{"../lib/dom":3,"../lib/helper":6,"./instances":18}],10:[function(require,module,exports){ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindClickRailHandler(element, i) { + function pageOffset(el) { + return el.getBoundingClientRect(); + } + var stopPropagation = function (e) { e.stopPropagation(); }; + + i.event.bind(i.scrollbarY, 'click', stopPropagation); + i.event.bind(i.scrollbarYRail, 'click', function (e) { + var positionTop = e.pageY - window.pageYOffset - pageOffset(i.scrollbarYRail).top; + var direction = positionTop > i.scrollbarYTop ? 1 : -1; + + updateScroll(element, 'top', element.scrollTop + direction * i.containerHeight); + updateGeometry(element); + + e.stopPropagation(); + }); + + i.event.bind(i.scrollbarX, 'click', stopPropagation); + i.event.bind(i.scrollbarXRail, 'click', function (e) { + var positionLeft = e.pageX - window.pageXOffset - pageOffset(i.scrollbarXRail).left; + var direction = positionLeft > i.scrollbarXLeft ? 1 : -1; + + updateScroll(element, 'left', element.scrollLeft + direction * i.containerWidth); + updateGeometry(element); + + e.stopPropagation(); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindClickRailHandler(element, i); +}; + +},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],11:[function(require,module,exports){ +'use strict'; + +var _ = require('../../lib/helper'); +var dom = require('../../lib/dom'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindMouseScrollXHandler(element, i) { + var currentLeft = null; + var currentPageX = null; + + function updateScrollLeft(deltaX) { + var newLeft = currentLeft + (deltaX * i.railXRatio); + var maxLeft = Math.max(0, i.scrollbarXRail.getBoundingClientRect().left) + (i.railXRatio * (i.railXWidth - i.scrollbarXWidth)); + + if (newLeft < 0) { + i.scrollbarXLeft = 0; + } else if (newLeft > maxLeft) { + i.scrollbarXLeft = maxLeft; + } else { + i.scrollbarXLeft = newLeft; + } + + var scrollLeft = _.toInt(i.scrollbarXLeft * (i.contentWidth - i.containerWidth) / (i.containerWidth - (i.railXRatio * i.scrollbarXWidth))) - i.negativeScrollAdjustment; + updateScroll(element, 'left', scrollLeft); + } + + var mouseMoveHandler = function (e) { + updateScrollLeft(e.pageX - currentPageX); + updateGeometry(element); + e.stopPropagation(); + e.preventDefault(); + }; + + var mouseUpHandler = function () { + _.stopScrolling(element, 'x'); + i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler); + }; + + i.event.bind(i.scrollbarX, 'mousedown', function (e) { + currentPageX = e.pageX; + currentLeft = _.toInt(dom.css(i.scrollbarX, 'left')) * i.railXRatio; + _.startScrolling(element, 'x'); + + i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler); + i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler); + + e.stopPropagation(); + e.preventDefault(); + }); +} + +function bindMouseScrollYHandler(element, i) { + var currentTop = null; + var currentPageY = null; + + function updateScrollTop(deltaY) { + var newTop = currentTop + (deltaY * i.railYRatio); + var maxTop = Math.max(0, i.scrollbarYRail.getBoundingClientRect().top) + (i.railYRatio * (i.railYHeight - i.scrollbarYHeight)); + + if (newTop < 0) { + i.scrollbarYTop = 0; + } else if (newTop > maxTop) { + i.scrollbarYTop = maxTop; + } else { + i.scrollbarYTop = newTop; + } + + var scrollTop = _.toInt(i.scrollbarYTop * (i.contentHeight - i.containerHeight) / (i.containerHeight - (i.railYRatio * i.scrollbarYHeight))); + updateScroll(element, 'top', scrollTop); + } + + var mouseMoveHandler = function (e) { + updateScrollTop(e.pageY - currentPageY); + updateGeometry(element); + e.stopPropagation(); + e.preventDefault(); + }; + + var mouseUpHandler = function () { + _.stopScrolling(element, 'y'); + i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler); + }; + + i.event.bind(i.scrollbarY, 'mousedown', function (e) { + currentPageY = e.pageY; + currentTop = _.toInt(dom.css(i.scrollbarY, 'top')) * i.railYRatio; + _.startScrolling(element, 'y'); + + i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler); + i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler); + + e.stopPropagation(); + e.preventDefault(); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindMouseScrollXHandler(element, i); + bindMouseScrollYHandler(element, i); +}; + +},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],12:[function(require,module,exports){ +'use strict'; + +var _ = require('../../lib/helper'); +var dom = require('../../lib/dom'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindKeyboardHandler(element, i) { + var hovered = false; + i.event.bind(element, 'mouseenter', function () { + hovered = true; + }); + i.event.bind(element, 'mouseleave', function () { + hovered = false; + }); + + var shouldPrevent = false; + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + if (deltaX === 0) { + if (!i.scrollbarYActive) { + return false; + } + if ((scrollTop === 0 && deltaY > 0) || (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)) { + return !i.settings.wheelPropagation; + } + } + + var scrollLeft = element.scrollLeft; + if (deltaY === 0) { + if (!i.scrollbarXActive) { + return false; + } + if ((scrollLeft === 0 && deltaX < 0) || (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)) { + return !i.settings.wheelPropagation; + } + } + return true; + } + + i.event.bind(i.ownerDocument, 'keydown', function (e) { + if ((e.isDefaultPrevented && e.isDefaultPrevented()) || e.defaultPrevented) { + return; + } + + var focused = dom.matches(i.scrollbarX, ':focus') || + dom.matches(i.scrollbarY, ':focus'); + + if (!hovered && !focused) { + return; + } + + var activeElement = document.activeElement ? document.activeElement : i.ownerDocument.activeElement; + if (activeElement) { + if (activeElement.tagName === 'IFRAME') { + activeElement = activeElement.contentDocument.activeElement; + } else { + // go deeper if element is a webcomponent + while (activeElement.shadowRoot) { + activeElement = activeElement.shadowRoot.activeElement; + } + } + if (_.isEditable(activeElement)) { + return; + } + } + + var deltaX = 0; + var deltaY = 0; + + switch (e.which) { + case 37: // left + if (e.metaKey) { + deltaX = -i.contentWidth; + } else if (e.altKey) { + deltaX = -i.containerWidth; + } else { + deltaX = -30; + } + break; + case 38: // up + if (e.metaKey) { + deltaY = i.contentHeight; + } else if (e.altKey) { + deltaY = i.containerHeight; + } else { + deltaY = 30; + } + break; + case 39: // right + if (e.metaKey) { + deltaX = i.contentWidth; + } else if (e.altKey) { + deltaX = i.containerWidth; + } else { + deltaX = 30; + } + break; + case 40: // down + if (e.metaKey) { + deltaY = -i.contentHeight; + } else if (e.altKey) { + deltaY = -i.containerHeight; + } else { + deltaY = -30; + } + break; + case 33: // page up + deltaY = 90; + break; + case 32: // space bar + if (e.shiftKey) { + deltaY = 90; + } else { + deltaY = -90; + } + break; + case 34: // page down + deltaY = -90; + break; + case 35: // end + if (e.ctrlKey) { + deltaY = -i.contentHeight; + } else { + deltaY = -i.containerHeight; + } + break; + case 36: // home + if (e.ctrlKey) { + deltaY = element.scrollTop; + } else { + deltaY = i.containerHeight; + } + break; + default: + return; + } + + updateScroll(element, 'top', element.scrollTop - deltaY); + updateScroll(element, 'left', element.scrollLeft + deltaX); + updateGeometry(element); + + shouldPrevent = shouldPreventDefault(deltaX, deltaY); + if (shouldPrevent) { + e.preventDefault(); + } + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindKeyboardHandler(element, i); +}; + +},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],13:[function(require,module,exports){ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindMouseWheelHandler(element, i) { + var shouldPrevent = false; + + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + if (deltaX === 0) { + if (!i.scrollbarYActive) { + return false; + } + if ((scrollTop === 0 && deltaY > 0) || (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)) { + return !i.settings.wheelPropagation; + } + } + + var scrollLeft = element.scrollLeft; + if (deltaY === 0) { + if (!i.scrollbarXActive) { + return false; + } + if ((scrollLeft === 0 && deltaX < 0) || (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)) { + return !i.settings.wheelPropagation; + } + } + return true; + } + + function getDeltaFromEvent(e) { + var deltaX = e.deltaX; + var deltaY = -1 * e.deltaY; + + if (typeof deltaX === "undefined" || typeof deltaY === "undefined") { + // OS X Safari + deltaX = -1 * e.wheelDeltaX / 6; + deltaY = e.wheelDeltaY / 6; + } + + if (e.deltaMode && e.deltaMode === 1) { + // Firefox in deltaMode 1: Line scrolling + deltaX *= 10; + deltaY *= 10; + } + + if (deltaX !== deltaX && deltaY !== deltaY/* NaN checks */) { + // IE in some mouse drivers + deltaX = 0; + deltaY = e.wheelDelta; + } + + if (e.shiftKey) { + // reverse axis with shift key + return [-deltaY, -deltaX]; + } + return [deltaX, deltaY]; + } + + function shouldBeConsumedByChild(deltaX, deltaY) { + var child = element.querySelector('textarea:hover, select[multiple]:hover, .ps-child:hover'); + if (child) { + if (!window.getComputedStyle(child).overflow.match(/(scroll|auto)/)) { + // if not scrollable + return false; + } + + var maxScrollTop = child.scrollHeight - child.clientHeight; + if (maxScrollTop > 0) { + if (!(child.scrollTop === 0 && deltaY > 0) && !(child.scrollTop === maxScrollTop && deltaY < 0)) { + return true; + } + } + var maxScrollLeft = child.scrollLeft - child.clientWidth; + if (maxScrollLeft > 0) { + if (!(child.scrollLeft === 0 && deltaX < 0) && !(child.scrollLeft === maxScrollLeft && deltaX > 0)) { + return true; + } + } + } + return false; + } + + function mousewheelHandler(e) { + var delta = getDeltaFromEvent(e); + + var deltaX = delta[0]; + var deltaY = delta[1]; + + if (shouldBeConsumedByChild(deltaX, deltaY)) { + return; + } + + shouldPrevent = false; + if (!i.settings.useBothWheelAxes) { + // deltaX will only be used for horizontal scrolling and deltaY will + // only be used for vertical scrolling - this is the default + updateScroll(element, 'top', element.scrollTop - (deltaY * i.settings.wheelSpeed)); + updateScroll(element, 'left', element.scrollLeft + (deltaX * i.settings.wheelSpeed)); + } else if (i.scrollbarYActive && !i.scrollbarXActive) { + // only vertical scrollbar is active and useBothWheelAxes option is + // active, so let's scroll vertical bar using both mouse wheel axes + if (deltaY) { + updateScroll(element, 'top', element.scrollTop - (deltaY * i.settings.wheelSpeed)); + } else { + updateScroll(element, 'top', element.scrollTop + (deltaX * i.settings.wheelSpeed)); + } + shouldPrevent = true; + } else if (i.scrollbarXActive && !i.scrollbarYActive) { + // useBothWheelAxes and only horizontal bar is active, so use both + // wheel axes for horizontal bar + if (deltaX) { + updateScroll(element, 'left', element.scrollLeft + (deltaX * i.settings.wheelSpeed)); + } else { + updateScroll(element, 'left', element.scrollLeft - (deltaY * i.settings.wheelSpeed)); + } + shouldPrevent = true; + } + + updateGeometry(element); + + shouldPrevent = (shouldPrevent || shouldPreventDefault(deltaX, deltaY)); + if (shouldPrevent) { + e.stopPropagation(); + e.preventDefault(); + } + } + + if (typeof window.onwheel !== "undefined") { + i.event.bind(element, 'wheel', mousewheelHandler); + } else if (typeof window.onmousewheel !== "undefined") { + i.event.bind(element, 'mousewheel', mousewheelHandler); + } +} + +module.exports = function (element) { + var i = instances.get(element); + bindMouseWheelHandler(element, i); +}; + +},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],14:[function(require,module,exports){ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); + +function bindNativeScrollHandler(element, i) { + i.event.bind(element, 'scroll', function () { + updateGeometry(element); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindNativeScrollHandler(element, i); +}; + +},{"../instances":18,"../update-geometry":19}],15:[function(require,module,exports){ +'use strict'; + +var _ = require('../../lib/helper'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindSelectionHandler(element, i) { + function getRangeNode() { + var selection = window.getSelection ? window.getSelection() : + document.getSelection ? document.getSelection() : ''; + if (selection.toString().length === 0) { + return null; + } else { + return selection.getRangeAt(0).commonAncestorContainer; + } + } + + var scrollingLoop = null; + var scrollDiff = {top: 0, left: 0}; + function startScrolling() { + if (!scrollingLoop) { + scrollingLoop = setInterval(function () { + if (!instances.get(element)) { + clearInterval(scrollingLoop); + return; + } + + updateScroll(element, 'top', element.scrollTop + scrollDiff.top); + updateScroll(element, 'left', element.scrollLeft + scrollDiff.left); + updateGeometry(element); + }, 50); // every .1 sec + } + } + function stopScrolling() { + if (scrollingLoop) { + clearInterval(scrollingLoop); + scrollingLoop = null; + } + _.stopScrolling(element); + } + + var isSelected = false; + i.event.bind(i.ownerDocument, 'selectionchange', function () { + if (element.contains(getRangeNode())) { + isSelected = true; + } else { + isSelected = false; + stopScrolling(); + } + }); + i.event.bind(window, 'mouseup', function () { + if (isSelected) { + isSelected = false; + stopScrolling(); + } + }); + i.event.bind(window, 'keyup', function () { + if (isSelected) { + isSelected = false; + stopScrolling(); + } + }); + + i.event.bind(window, 'mousemove', function (e) { + if (isSelected) { + var mousePosition = {x: e.pageX, y: e.pageY}; + var containerGeometry = { + left: element.offsetLeft, + right: element.offsetLeft + element.offsetWidth, + top: element.offsetTop, + bottom: element.offsetTop + element.offsetHeight + }; + + if (mousePosition.x < containerGeometry.left + 3) { + scrollDiff.left = -5; + _.startScrolling(element, 'x'); + } else if (mousePosition.x > containerGeometry.right - 3) { + scrollDiff.left = 5; + _.startScrolling(element, 'x'); + } else { + scrollDiff.left = 0; + } + + if (mousePosition.y < containerGeometry.top + 3) { + if (containerGeometry.top + 3 - mousePosition.y < 5) { + scrollDiff.top = -5; + } else { + scrollDiff.top = -20; + } + _.startScrolling(element, 'y'); + } else if (mousePosition.y > containerGeometry.bottom - 3) { + if (mousePosition.y - containerGeometry.bottom + 3 < 5) { + scrollDiff.top = 5; + } else { + scrollDiff.top = 20; + } + _.startScrolling(element, 'y'); + } else { + scrollDiff.top = 0; + } + + if (scrollDiff.top === 0 && scrollDiff.left === 0) { + stopScrolling(); + } else { + startScrolling(); + } + } + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindSelectionHandler(element, i); +}; + +},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],16:[function(require,module,exports){ +'use strict'; + +var _ = require('../../lib/helper'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindTouchHandler(element, i, supportsTouch, supportsIePointer) { + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + var scrollLeft = element.scrollLeft; + var magnitudeX = Math.abs(deltaX); + var magnitudeY = Math.abs(deltaY); + + if (magnitudeY > magnitudeX) { + // user is perhaps trying to swipe up/down the page + + if (((deltaY < 0) && (scrollTop === i.contentHeight - i.containerHeight)) || + ((deltaY > 0) && (scrollTop === 0))) { + return !i.settings.swipePropagation; + } + } else if (magnitudeX > magnitudeY) { + // user is perhaps trying to swipe left/right across the page + + if (((deltaX < 0) && (scrollLeft === i.contentWidth - i.containerWidth)) || + ((deltaX > 0) && (scrollLeft === 0))) { + return !i.settings.swipePropagation; + } + } + + return true; + } + + function applyTouchMove(differenceX, differenceY) { + updateScroll(element, 'top', element.scrollTop - differenceY); + updateScroll(element, 'left', element.scrollLeft - differenceX); + + updateGeometry(element); + } + + var startOffset = {}; + var startTime = 0; + var speed = {}; + var easingLoop = null; + var inGlobalTouch = false; + var inLocalTouch = false; + + function globalTouchStart() { + inGlobalTouch = true; + } + function globalTouchEnd() { + inGlobalTouch = false; + } + + function getTouch(e) { + if (e.targetTouches) { + return e.targetTouches[0]; + } else { + // Maybe IE pointer + return e; + } + } + function shouldHandle(e) { + if (e.targetTouches && e.targetTouches.length === 1) { + return true; + } + if (e.pointerType && e.pointerType !== 'mouse' && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) { + return true; + } + return false; + } + function touchStart(e) { + if (shouldHandle(e)) { + inLocalTouch = true; + + var touch = getTouch(e); + + startOffset.pageX = touch.pageX; + startOffset.pageY = touch.pageY; + + startTime = (new Date()).getTime(); + + if (easingLoop !== null) { + clearInterval(easingLoop); + } + + e.stopPropagation(); + } + } + function touchMove(e) { + if (!inLocalTouch && i.settings.swipePropagation) { + touchStart(e); + } + if (!inGlobalTouch && inLocalTouch && shouldHandle(e)) { + var touch = getTouch(e); + + var currentOffset = {pageX: touch.pageX, pageY: touch.pageY}; + + var differenceX = currentOffset.pageX - startOffset.pageX; + var differenceY = currentOffset.pageY - startOffset.pageY; + + applyTouchMove(differenceX, differenceY); + startOffset = currentOffset; + + var currentTime = (new Date()).getTime(); + + var timeGap = currentTime - startTime; + if (timeGap > 0) { + speed.x = differenceX / timeGap; + speed.y = differenceY / timeGap; + startTime = currentTime; + } + + if (shouldPreventDefault(differenceX, differenceY)) { + e.stopPropagation(); + e.preventDefault(); + } + } + } + function touchEnd() { + if (!inGlobalTouch && inLocalTouch) { + inLocalTouch = false; + + clearInterval(easingLoop); + easingLoop = setInterval(function () { + if (!instances.get(element)) { + clearInterval(easingLoop); + return; + } + + if (!speed.x && !speed.y) { + clearInterval(easingLoop); + return; + } + + if (Math.abs(speed.x) < 0.01 && Math.abs(speed.y) < 0.01) { + clearInterval(easingLoop); + return; + } + + applyTouchMove(speed.x * 30, speed.y * 30); + + speed.x *= 0.8; + speed.y *= 0.8; + }, 10); + } + } + + if (supportsTouch) { + i.event.bind(window, 'touchstart', globalTouchStart); + i.event.bind(window, 'touchend', globalTouchEnd); + i.event.bind(element, 'touchstart', touchStart); + i.event.bind(element, 'touchmove', touchMove); + i.event.bind(element, 'touchend', touchEnd); + } else if (supportsIePointer) { + if (window.PointerEvent) { + i.event.bind(window, 'pointerdown', globalTouchStart); + i.event.bind(window, 'pointerup', globalTouchEnd); + i.event.bind(element, 'pointerdown', touchStart); + i.event.bind(element, 'pointermove', touchMove); + i.event.bind(element, 'pointerup', touchEnd); + } else if (window.MSPointerEvent) { + i.event.bind(window, 'MSPointerDown', globalTouchStart); + i.event.bind(window, 'MSPointerUp', globalTouchEnd); + i.event.bind(element, 'MSPointerDown', touchStart); + i.event.bind(element, 'MSPointerMove', touchMove); + i.event.bind(element, 'MSPointerUp', touchEnd); + } + } +} + +module.exports = function (element) { + if (!_.env.supportsTouch && !_.env.supportsIePointer) { + return; + } + + var i = instances.get(element); + bindTouchHandler(element, i, _.env.supportsTouch, _.env.supportsIePointer); +}; + +},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],17:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var instances = require('./instances'); +var updateGeometry = require('./update-geometry'); + +// Handlers +var handlers = { + 'click-rail': require('./handler/click-rail'), + 'drag-scrollbar': require('./handler/drag-scrollbar'), + 'keyboard': require('./handler/keyboard'), + 'wheel': require('./handler/mouse-wheel'), + 'touch': require('./handler/touch'), + 'selection': require('./handler/selection') +}; +var nativeScrollHandler = require('./handler/native-scroll'); + +module.exports = function (element, userSettings) { + userSettings = typeof userSettings === 'object' ? userSettings : {}; + + cls.add(element, 'ps-container'); + + // Create a plugin instance. + var i = instances.add(element); + + i.settings = _.extend(i.settings, userSettings); + cls.add(element, 'ps-theme-' + i.settings.theme); + + i.settings.handlers.forEach(function (handlerName) { + handlers[handlerName](element); + }); + + nativeScrollHandler(element); + + updateGeometry(element); +}; + +},{"../lib/class":2,"../lib/helper":6,"./handler/click-rail":10,"./handler/drag-scrollbar":11,"./handler/keyboard":12,"./handler/mouse-wheel":13,"./handler/native-scroll":14,"./handler/selection":15,"./handler/touch":16,"./instances":18,"./update-geometry":19}],18:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var defaultSettings = require('./default-setting'); +var dom = require('../lib/dom'); +var EventManager = require('../lib/event-manager'); +var guid = require('../lib/guid'); + +var instances = {}; + +function Instance(element) { + var i = this; + + i.settings = _.clone(defaultSettings); + i.containerWidth = null; + i.containerHeight = null; + i.contentWidth = null; + i.contentHeight = null; + + i.isRtl = dom.css(element, 'direction') === "rtl"; + i.isNegativeScroll = (function () { + var originalScrollLeft = element.scrollLeft; + var result = null; + element.scrollLeft = -1; + result = element.scrollLeft < 0; + element.scrollLeft = originalScrollLeft; + return result; + })(); + i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0; + i.event = new EventManager(); + i.ownerDocument = element.ownerDocument || document; + + function focus() { + cls.add(element, 'ps-focus'); + } + + function blur() { + cls.remove(element, 'ps-focus'); + } + + i.scrollbarXRail = dom.appendTo(dom.e('div', 'ps-scrollbar-x-rail'), element); + i.scrollbarX = dom.appendTo(dom.e('div', 'ps-scrollbar-x'), i.scrollbarXRail); + i.scrollbarX.setAttribute('tabindex', 0); + i.event.bind(i.scrollbarX, 'focus', focus); + i.event.bind(i.scrollbarX, 'blur', blur); + i.scrollbarXActive = null; + i.scrollbarXWidth = null; + i.scrollbarXLeft = null; + i.scrollbarXBottom = _.toInt(dom.css(i.scrollbarXRail, 'bottom')); + i.isScrollbarXUsingBottom = i.scrollbarXBottom === i.scrollbarXBottom; // !isNaN + i.scrollbarXTop = i.isScrollbarXUsingBottom ? null : _.toInt(dom.css(i.scrollbarXRail, 'top')); + i.railBorderXWidth = _.toInt(dom.css(i.scrollbarXRail, 'borderLeftWidth')) + _.toInt(dom.css(i.scrollbarXRail, 'borderRightWidth')); + // Set rail to display:block to calculate margins + dom.css(i.scrollbarXRail, 'display', 'block'); + i.railXMarginWidth = _.toInt(dom.css(i.scrollbarXRail, 'marginLeft')) + _.toInt(dom.css(i.scrollbarXRail, 'marginRight')); + dom.css(i.scrollbarXRail, 'display', ''); + i.railXWidth = null; + i.railXRatio = null; + + i.scrollbarYRail = dom.appendTo(dom.e('div', 'ps-scrollbar-y-rail'), element); + i.scrollbarY = dom.appendTo(dom.e('div', 'ps-scrollbar-y'), i.scrollbarYRail); + i.scrollbarY.setAttribute('tabindex', 0); + i.event.bind(i.scrollbarY, 'focus', focus); + i.event.bind(i.scrollbarY, 'blur', blur); + i.scrollbarYActive = null; + i.scrollbarYHeight = null; + i.scrollbarYTop = null; + i.scrollbarYRight = _.toInt(dom.css(i.scrollbarYRail, 'right')); + i.isScrollbarYUsingRight = i.scrollbarYRight === i.scrollbarYRight; // !isNaN + i.scrollbarYLeft = i.isScrollbarYUsingRight ? null : _.toInt(dom.css(i.scrollbarYRail, 'left')); + i.scrollbarYOuterWidth = i.isRtl ? _.outerWidth(i.scrollbarY) : null; + i.railBorderYWidth = _.toInt(dom.css(i.scrollbarYRail, 'borderTopWidth')) + _.toInt(dom.css(i.scrollbarYRail, 'borderBottomWidth')); + dom.css(i.scrollbarYRail, 'display', 'block'); + i.railYMarginHeight = _.toInt(dom.css(i.scrollbarYRail, 'marginTop')) + _.toInt(dom.css(i.scrollbarYRail, 'marginBottom')); + dom.css(i.scrollbarYRail, 'display', ''); + i.railYHeight = null; + i.railYRatio = null; +} + +function getId(element) { + return element.getAttribute('data-ps-id'); +} + +function setId(element, id) { + element.setAttribute('data-ps-id', id); +} + +function removeId(element) { + element.removeAttribute('data-ps-id'); +} + +exports.add = function (element) { + var newId = guid(); + setId(element, newId); + instances[newId] = new Instance(element); + return instances[newId]; +}; + +exports.remove = function (element) { + delete instances[getId(element)]; + removeId(element); +}; + +exports.get = function (element) { + return instances[getId(element)]; +}; + +},{"../lib/class":2,"../lib/dom":3,"../lib/event-manager":4,"../lib/guid":5,"../lib/helper":6,"./default-setting":8}],19:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var dom = require('../lib/dom'); +var instances = require('./instances'); +var updateScroll = require('./update-scroll'); + +function getThumbSize(i, thumbSize) { + if (i.settings.minScrollbarLength) { + thumbSize = Math.max(thumbSize, i.settings.minScrollbarLength); + } + if (i.settings.maxScrollbarLength) { + thumbSize = Math.min(thumbSize, i.settings.maxScrollbarLength); + } + return thumbSize; +} + +function updateCss(element, i) { + var xRailOffset = {width: i.railXWidth}; + if (i.isRtl) { + xRailOffset.left = i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth - i.contentWidth; + } else { + xRailOffset.left = element.scrollLeft; + } + if (i.isScrollbarXUsingBottom) { + xRailOffset.bottom = i.scrollbarXBottom - element.scrollTop; + } else { + xRailOffset.top = i.scrollbarXTop + element.scrollTop; + } + dom.css(i.scrollbarXRail, xRailOffset); + + var yRailOffset = {top: element.scrollTop, height: i.railYHeight}; + if (i.isScrollbarYUsingRight) { + if (i.isRtl) { + yRailOffset.right = i.contentWidth - (i.negativeScrollAdjustment + element.scrollLeft) - i.scrollbarYRight - i.scrollbarYOuterWidth; + } else { + yRailOffset.right = i.scrollbarYRight - element.scrollLeft; + } + } else { + if (i.isRtl) { + yRailOffset.left = i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth * 2 - i.contentWidth - i.scrollbarYLeft - i.scrollbarYOuterWidth; + } else { + yRailOffset.left = i.scrollbarYLeft + element.scrollLeft; + } + } + dom.css(i.scrollbarYRail, yRailOffset); + + dom.css(i.scrollbarX, {left: i.scrollbarXLeft, width: i.scrollbarXWidth - i.railBorderXWidth}); + dom.css(i.scrollbarY, {top: i.scrollbarYTop, height: i.scrollbarYHeight - i.railBorderYWidth}); +} + +module.exports = function (element) { + var i = instances.get(element); + + i.containerWidth = element.clientWidth; + i.containerHeight = element.clientHeight; + i.contentWidth = element.scrollWidth; + i.contentHeight = element.scrollHeight; + + var existingRails; + if (!element.contains(i.scrollbarXRail)) { + existingRails = dom.queryChildren(element, '.ps-scrollbar-x-rail'); + if (existingRails.length > 0) { + existingRails.forEach(function (rail) { + dom.remove(rail); + }); + } + dom.appendTo(i.scrollbarXRail, element); + } + if (!element.contains(i.scrollbarYRail)) { + existingRails = dom.queryChildren(element, '.ps-scrollbar-y-rail'); + if (existingRails.length > 0) { + existingRails.forEach(function (rail) { + dom.remove(rail); + }); + } + dom.appendTo(i.scrollbarYRail, element); + } + + if (!i.settings.suppressScrollX && i.containerWidth + i.settings.scrollXMarginOffset < i.contentWidth) { + i.scrollbarXActive = true; + i.railXWidth = i.containerWidth - i.railXMarginWidth; + i.railXRatio = i.containerWidth / i.railXWidth; + i.scrollbarXWidth = getThumbSize(i, _.toInt(i.railXWidth * i.containerWidth / i.contentWidth)); + i.scrollbarXLeft = _.toInt((i.negativeScrollAdjustment + element.scrollLeft) * (i.railXWidth - i.scrollbarXWidth) / (i.contentWidth - i.containerWidth)); + } else { + i.scrollbarXActive = false; + } + + if (!i.settings.suppressScrollY && i.containerHeight + i.settings.scrollYMarginOffset < i.contentHeight) { + i.scrollbarYActive = true; + i.railYHeight = i.containerHeight - i.railYMarginHeight; + i.railYRatio = i.containerHeight / i.railYHeight; + i.scrollbarYHeight = getThumbSize(i, _.toInt(i.railYHeight * i.containerHeight / i.contentHeight)); + i.scrollbarYTop = _.toInt(element.scrollTop * (i.railYHeight - i.scrollbarYHeight) / (i.contentHeight - i.containerHeight)); + } else { + i.scrollbarYActive = false; + } + + if (i.scrollbarXLeft >= i.railXWidth - i.scrollbarXWidth) { + i.scrollbarXLeft = i.railXWidth - i.scrollbarXWidth; + } + if (i.scrollbarYTop >= i.railYHeight - i.scrollbarYHeight) { + i.scrollbarYTop = i.railYHeight - i.scrollbarYHeight; + } + + updateCss(element, i); + + if (i.scrollbarXActive) { + cls.add(element, 'ps-active-x'); + } else { + cls.remove(element, 'ps-active-x'); + i.scrollbarXWidth = 0; + i.scrollbarXLeft = 0; + updateScroll(element, 'left', 0); + } + if (i.scrollbarYActive) { + cls.add(element, 'ps-active-y'); + } else { + cls.remove(element, 'ps-active-y'); + i.scrollbarYHeight = 0; + i.scrollbarYTop = 0; + updateScroll(element, 'top', 0); + } +}; + +},{"../lib/class":2,"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-scroll":20}],20:[function(require,module,exports){ +'use strict'; + +var instances = require('./instances'); + +var lastTop; +var lastLeft; + +var createDOMEvent = function (name) { + var event = document.createEvent("Event"); + event.initEvent(name, true, true); + return event; +}; + +module.exports = function (element, axis, value) { + if (typeof element === 'undefined') { + throw 'You must provide an element to the update-scroll function'; + } + + if (typeof axis === 'undefined') { + throw 'You must provide an axis to the update-scroll function'; + } + + if (typeof value === 'undefined') { + throw 'You must provide a value to the update-scroll function'; + } + + if (axis === 'top' && value <= 0) { + element.scrollTop = value = 0; // don't allow negative scroll + element.dispatchEvent(createDOMEvent('ps-y-reach-start')); + } + + if (axis === 'left' && value <= 0) { + element.scrollLeft = value = 0; // don't allow negative scroll + element.dispatchEvent(createDOMEvent('ps-x-reach-start')); + } + + var i = instances.get(element); + + if (axis === 'top' && value >= i.contentHeight - i.containerHeight) { + // don't allow scroll past container + value = i.contentHeight - i.containerHeight; + if (value - element.scrollTop <= 1) { + // mitigates rounding errors on non-subpixel scroll values + value = element.scrollTop; + } else { + element.scrollTop = value; + } + element.dispatchEvent(createDOMEvent('ps-y-reach-end')); + } + + if (axis === 'left' && value >= i.contentWidth - i.containerWidth) { + // don't allow scroll past container + value = i.contentWidth - i.containerWidth; + if (value - element.scrollLeft <= 1) { + // mitigates rounding errors on non-subpixel scroll values + value = element.scrollLeft; + } else { + element.scrollLeft = value; + } + element.dispatchEvent(createDOMEvent('ps-x-reach-end')); + } + + if (!lastTop) { + lastTop = element.scrollTop; + } + + if (!lastLeft) { + lastLeft = element.scrollLeft; + } + + if (axis === 'top' && value < lastTop) { + element.dispatchEvent(createDOMEvent('ps-scroll-up')); + } + + if (axis === 'top' && value > lastTop) { + element.dispatchEvent(createDOMEvent('ps-scroll-down')); + } + + if (axis === 'left' && value < lastLeft) { + element.dispatchEvent(createDOMEvent('ps-scroll-left')); + } + + if (axis === 'left' && value > lastLeft) { + element.dispatchEvent(createDOMEvent('ps-scroll-right')); + } + + if (axis === 'top') { + element.scrollTop = lastTop = value; + element.dispatchEvent(createDOMEvent('ps-scroll-y')); + } + + if (axis === 'left') { + element.scrollLeft = lastLeft = value; + element.dispatchEvent(createDOMEvent('ps-scroll-x')); + } + +}; + +},{"./instances":18}],21:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var dom = require('../lib/dom'); +var instances = require('./instances'); +var updateGeometry = require('./update-geometry'); +var updateScroll = require('./update-scroll'); + +module.exports = function (element) { + var i = instances.get(element); + + if (!i) { + return; + } + + // Recalcuate negative scrollLeft adjustment + i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0; + + // Recalculate rail margins + dom.css(i.scrollbarXRail, 'display', 'block'); + dom.css(i.scrollbarYRail, 'display', 'block'); + i.railXMarginWidth = _.toInt(dom.css(i.scrollbarXRail, 'marginLeft')) + _.toInt(dom.css(i.scrollbarXRail, 'marginRight')); + i.railYMarginHeight = _.toInt(dom.css(i.scrollbarYRail, 'marginTop')) + _.toInt(dom.css(i.scrollbarYRail, 'marginBottom')); + + // Hide scrollbars not to affect scrollWidth and scrollHeight + dom.css(i.scrollbarXRail, 'display', 'none'); + dom.css(i.scrollbarYRail, 'display', 'none'); + + updateGeometry(element); + + // Update top/left scroll to trigger events + updateScroll(element, 'top', element.scrollTop); + updateScroll(element, 'left', element.scrollLeft); + + dom.css(i.scrollbarXRail, 'display', ''); + dom.css(i.scrollbarYRail, 'display', ''); +}; + +},{"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-geometry":19,"./update-scroll":20}]},{},[1]); diff --git a/view/js/perfect-scrollbar/js/perfect-scrollbar.jquery.min.js b/view/js/perfect-scrollbar/js/perfect-scrollbar.jquery.min.js new file mode 100644 index 0000000000..01bc768881 --- /dev/null +++ b/view/js/perfect-scrollbar/js/perfect-scrollbar.jquery.min.js @@ -0,0 +1,2 @@ +/* perfect-scrollbar v0.6.16 */ +!function t(e,n,r){function o(i,s){if(!n[i]){if(!e[i]){var a="function"==typeof require&&require;if(!s&&a)return a(i,!0);if(l)return l(i,!0);var c=new Error("Cannot find module '"+i+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[i]={exports:{}};e[i][0].call(u.exports,function(t){var n=e[i][1][t];return o(n?n:t)},u,u.exports,t,e,n,r)}return n[i].exports}for(var l="function"==typeof require&&require,i=0;i=0&&n.splice(r,1),t.className=n.join(" ")}n.add=function(t,e){t.classList?t.classList.add(e):r(t,e)},n.remove=function(t,e){t.classList?t.classList.remove(e):o(t,e)},n.list=function(t){return t.classList?Array.prototype.slice.apply(t.classList):t.className.split(" ")}},{}],3:[function(t,e,n){"use strict";function r(t,e){return window.getComputedStyle(t)[e]}function o(t,e,n){return"number"==typeof n&&(n=n.toString()+"px"),t.style[e]=n,t}function l(t,e){for(var n in e){var r=e[n];"number"==typeof r&&(r=r.toString()+"px"),t.style[n]=r}return t}var i={};i.e=function(t,e){var n=document.createElement(t);return n.className=e,n},i.appendTo=function(t,e){return e.appendChild(t),t},i.css=function(t,e,n){return"object"==typeof e?l(t,e):"undefined"==typeof n?r(t,e):o(t,e,n)},i.matches=function(t,e){return"undefined"!=typeof t.matches?t.matches(e):"undefined"!=typeof t.matchesSelector?t.matchesSelector(e):"undefined"!=typeof t.webkitMatchesSelector?t.webkitMatchesSelector(e):"undefined"!=typeof t.mozMatchesSelector?t.mozMatchesSelector(e):"undefined"!=typeof t.msMatchesSelector?t.msMatchesSelector(e):void 0},i.remove=function(t){"undefined"!=typeof t.remove?t.remove():t.parentNode&&t.parentNode.removeChild(t)},i.queryChildren=function(t,e){return Array.prototype.filter.call(t.childNodes,function(t){return i.matches(t,e)})},e.exports=i},{}],4:[function(t,e,n){"use strict";var r=function(t){this.element=t,this.events={}};r.prototype.bind=function(t,e){"undefined"==typeof this.events[t]&&(this.events[t]=[]),this.events[t].push(e),this.element.addEventListener(t,e,!1)},r.prototype.unbind=function(t,e){var n="undefined"!=typeof e;this.events[t]=this.events[t].filter(function(r){return!(!n||r===e)||(this.element.removeEventListener(t,r,!1),!1)},this)},r.prototype.unbindAll=function(){for(var t in this.events)this.unbind(t)};var o=function(){this.eventElements=[]};o.prototype.eventElement=function(t){var e=this.eventElements.filter(function(e){return e.element===t})[0];return"undefined"==typeof e&&(e=new r(t),this.eventElements.push(e)),e},o.prototype.bind=function(t,e,n){this.eventElement(t).bind(e,n)},o.prototype.unbind=function(t,e,n){this.eventElement(t).unbind(e,n)},o.prototype.unbindAll=function(){for(var t=0;te.scrollbarYTop?1:-1;i(t,"top",t.scrollTop+s*e.containerHeight),l(t),r.stopPropagation()}),e.event.bind(e.scrollbarX,"click",r),e.event.bind(e.scrollbarXRail,"click",function(r){var o=r.pageX-window.pageXOffset-n(e.scrollbarXRail).left,s=o>e.scrollbarXLeft?1:-1;i(t,"left",t.scrollLeft+s*e.containerWidth),l(t),r.stopPropagation()})}var o=t("../instances"),l=t("../update-geometry"),i=t("../update-scroll");e.exports=function(t){var e=o.get(t);r(t,e)}},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],11:[function(t,e,n){"use strict";function r(t,e){function n(n){var o=r+n*e.railXRatio,i=Math.max(0,e.scrollbarXRail.getBoundingClientRect().left)+e.railXRatio*(e.railXWidth-e.scrollbarXWidth);o<0?e.scrollbarXLeft=0:o>i?e.scrollbarXLeft=i:e.scrollbarXLeft=o;var s=l.toInt(e.scrollbarXLeft*(e.contentWidth-e.containerWidth)/(e.containerWidth-e.railXRatio*e.scrollbarXWidth))-e.negativeScrollAdjustment;c(t,"left",s)}var r=null,o=null,s=function(e){n(e.pageX-o),a(t),e.stopPropagation(),e.preventDefault()},u=function(){l.stopScrolling(t,"x"),e.event.unbind(e.ownerDocument,"mousemove",s)};e.event.bind(e.scrollbarX,"mousedown",function(n){o=n.pageX,r=l.toInt(i.css(e.scrollbarX,"left"))*e.railXRatio,l.startScrolling(t,"x"),e.event.bind(e.ownerDocument,"mousemove",s),e.event.once(e.ownerDocument,"mouseup",u),n.stopPropagation(),n.preventDefault()})}function o(t,e){function n(n){var o=r+n*e.railYRatio,i=Math.max(0,e.scrollbarYRail.getBoundingClientRect().top)+e.railYRatio*(e.railYHeight-e.scrollbarYHeight);o<0?e.scrollbarYTop=0:o>i?e.scrollbarYTop=i:e.scrollbarYTop=o;var s=l.toInt(e.scrollbarYTop*(e.contentHeight-e.containerHeight)/(e.containerHeight-e.railYRatio*e.scrollbarYHeight));c(t,"top",s)}var r=null,o=null,s=function(e){n(e.pageY-o),a(t),e.stopPropagation(),e.preventDefault()},u=function(){l.stopScrolling(t,"y"),e.event.unbind(e.ownerDocument,"mousemove",s)};e.event.bind(e.scrollbarY,"mousedown",function(n){o=n.pageY,r=l.toInt(i.css(e.scrollbarY,"top"))*e.railYRatio,l.startScrolling(t,"y"),e.event.bind(e.ownerDocument,"mousemove",s),e.event.once(e.ownerDocument,"mouseup",u),n.stopPropagation(),n.preventDefault()})}var l=t("../../lib/helper"),i=t("../../lib/dom"),s=t("../instances"),a=t("../update-geometry"),c=t("../update-scroll");e.exports=function(t){var e=s.get(t);r(t,e),o(t,e)}},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],12:[function(t,e,n){"use strict";function r(t,e){function n(n,r){var o=t.scrollTop;if(0===n){if(!e.scrollbarYActive)return!1;if(0===o&&r>0||o>=e.contentHeight-e.containerHeight&&r<0)return!e.settings.wheelPropagation}var l=t.scrollLeft;if(0===r){if(!e.scrollbarXActive)return!1;if(0===l&&n<0||l>=e.contentWidth-e.containerWidth&&n>0)return!e.settings.wheelPropagation}return!0}var r=!1;e.event.bind(t,"mouseenter",function(){r=!0}),e.event.bind(t,"mouseleave",function(){r=!1});var i=!1;e.event.bind(e.ownerDocument,"keydown",function(c){if(!(c.isDefaultPrevented&&c.isDefaultPrevented()||c.defaultPrevented)){var u=l.matches(e.scrollbarX,":focus")||l.matches(e.scrollbarY,":focus");if(r||u){var d=document.activeElement?document.activeElement:e.ownerDocument.activeElement;if(d){if("IFRAME"===d.tagName)d=d.contentDocument.activeElement;else for(;d.shadowRoot;)d=d.shadowRoot.activeElement;if(o.isEditable(d))return}var p=0,f=0;switch(c.which){case 37:p=c.metaKey?-e.contentWidth:c.altKey?-e.containerWidth:-30;break;case 38:f=c.metaKey?e.contentHeight:c.altKey?e.containerHeight:30;break;case 39:p=c.metaKey?e.contentWidth:c.altKey?e.containerWidth:30;break;case 40:f=c.metaKey?-e.contentHeight:c.altKey?-e.containerHeight:-30;break;case 33:f=90;break;case 32:f=c.shiftKey?90:-90;break;case 34:f=-90;break;case 35:f=c.ctrlKey?-e.contentHeight:-e.containerHeight;break;case 36:f=c.ctrlKey?t.scrollTop:e.containerHeight;break;default:return}a(t,"top",t.scrollTop-f),a(t,"left",t.scrollLeft+p),s(t),i=n(p,f),i&&c.preventDefault()}}})}var o=t("../../lib/helper"),l=t("../../lib/dom"),i=t("../instances"),s=t("../update-geometry"),a=t("../update-scroll");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],13:[function(t,e,n){"use strict";function r(t,e){function n(n,r){var o=t.scrollTop;if(0===n){if(!e.scrollbarYActive)return!1;if(0===o&&r>0||o>=e.contentHeight-e.containerHeight&&r<0)return!e.settings.wheelPropagation}var l=t.scrollLeft;if(0===r){if(!e.scrollbarXActive)return!1;if(0===l&&n<0||l>=e.contentWidth-e.containerWidth&&n>0)return!e.settings.wheelPropagation}return!0}function r(t){var e=t.deltaX,n=-1*t.deltaY;return"undefined"!=typeof e&&"undefined"!=typeof n||(e=-1*t.wheelDeltaX/6,n=t.wheelDeltaY/6),t.deltaMode&&1===t.deltaMode&&(e*=10,n*=10),e!==e&&n!==n&&(e=0,n=t.wheelDelta),t.shiftKey?[-n,-e]:[e,n]}function o(e,n){var r=t.querySelector("textarea:hover, select[multiple]:hover, .ps-child:hover");if(r){if(!window.getComputedStyle(r).overflow.match(/(scroll|auto)/))return!1;var o=r.scrollHeight-r.clientHeight;if(o>0&&!(0===r.scrollTop&&n>0||r.scrollTop===o&&n<0))return!0;var l=r.scrollLeft-r.clientWidth;if(l>0&&!(0===r.scrollLeft&&e<0||r.scrollLeft===l&&e>0))return!0}return!1}function s(s){var c=r(s),u=c[0],d=c[1];o(u,d)||(a=!1,e.settings.useBothWheelAxes?e.scrollbarYActive&&!e.scrollbarXActive?(d?i(t,"top",t.scrollTop-d*e.settings.wheelSpeed):i(t,"top",t.scrollTop+u*e.settings.wheelSpeed),a=!0):e.scrollbarXActive&&!e.scrollbarYActive&&(u?i(t,"left",t.scrollLeft+u*e.settings.wheelSpeed):i(t,"left",t.scrollLeft-d*e.settings.wheelSpeed),a=!0):(i(t,"top",t.scrollTop-d*e.settings.wheelSpeed),i(t,"left",t.scrollLeft+u*e.settings.wheelSpeed)),l(t),a=a||n(u,d),a&&(s.stopPropagation(),s.preventDefault()))}var a=!1;"undefined"!=typeof window.onwheel?e.event.bind(t,"wheel",s):"undefined"!=typeof window.onmousewheel&&e.event.bind(t,"mousewheel",s)}var o=t("../instances"),l=t("../update-geometry"),i=t("../update-scroll");e.exports=function(t){var e=o.get(t);r(t,e)}},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],14:[function(t,e,n){"use strict";function r(t,e){e.event.bind(t,"scroll",function(){l(t)})}var o=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=o.get(t);r(t,e)}},{"../instances":18,"../update-geometry":19}],15:[function(t,e,n){"use strict";function r(t,e){function n(){var t=window.getSelection?window.getSelection():document.getSelection?document.getSelection():"";return 0===t.toString().length?null:t.getRangeAt(0).commonAncestorContainer}function r(){c||(c=setInterval(function(){return l.get(t)?(s(t,"top",t.scrollTop+u.top),s(t,"left",t.scrollLeft+u.left),void i(t)):void clearInterval(c)},50))}function a(){c&&(clearInterval(c),c=null),o.stopScrolling(t)}var c=null,u={top:0,left:0},d=!1;e.event.bind(e.ownerDocument,"selectionchange",function(){t.contains(n())?d=!0:(d=!1,a())}),e.event.bind(window,"mouseup",function(){d&&(d=!1,a())}),e.event.bind(window,"keyup",function(){d&&(d=!1,a())}),e.event.bind(window,"mousemove",function(e){if(d){var n={x:e.pageX,y:e.pageY},l={left:t.offsetLeft,right:t.offsetLeft+t.offsetWidth,top:t.offsetTop,bottom:t.offsetTop+t.offsetHeight};n.xl.right-3?(u.left=5,o.startScrolling(t,"x")):u.left=0,n.yl.bottom-3?(n.y-l.bottom+3<5?u.top=5:u.top=20,o.startScrolling(t,"y")):u.top=0,0===u.top&&0===u.left?a():r()}})}var o=t("../../lib/helper"),l=t("../instances"),i=t("../update-geometry"),s=t("../update-scroll");e.exports=function(t){var e=l.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],16:[function(t,e,n){"use strict";function r(t,e,n,r){function o(n,r){var o=t.scrollTop,l=t.scrollLeft,i=Math.abs(n),s=Math.abs(r);if(s>i){if(r<0&&o===e.contentHeight-e.containerHeight||r>0&&0===o)return!e.settings.swipePropagation}else if(i>s&&(n<0&&l===e.contentWidth-e.containerWidth||n>0&&0===l))return!e.settings.swipePropagation;return!0}function a(e,n){s(t,"top",t.scrollTop-n),s(t,"left",t.scrollLeft-e),i(t)}function c(){w=!0}function u(){w=!1}function d(t){return t.targetTouches?t.targetTouches[0]:t}function p(t){return!(!t.targetTouches||1!==t.targetTouches.length)||!(!t.pointerType||"mouse"===t.pointerType||t.pointerType===t.MSPOINTER_TYPE_MOUSE)}function f(t){if(p(t)){Y=!0;var e=d(t);g.pageX=e.pageX,g.pageY=e.pageY,v=(new Date).getTime(),null!==y&&clearInterval(y),t.stopPropagation()}}function h(t){if(!Y&&e.settings.swipePropagation&&f(t),!w&&Y&&p(t)){var n=d(t),r={pageX:n.pageX,pageY:n.pageY},l=r.pageX-g.pageX,i=r.pageY-g.pageY;a(l,i),g=r;var s=(new Date).getTime(),c=s-v;c>0&&(m.x=l/c,m.y=i/c,v=s),o(l,i)&&(t.stopPropagation(),t.preventDefault())}}function b(){!w&&Y&&(Y=!1,clearInterval(y),y=setInterval(function(){return l.get(t)&&(m.x||m.y)?Math.abs(m.x)<.01&&Math.abs(m.y)<.01?void clearInterval(y):(a(30*m.x,30*m.y),m.x*=.8,void(m.y*=.8)):void clearInterval(y)},10))}var g={},v=0,m={},y=null,w=!1,Y=!1;n?(e.event.bind(window,"touchstart",c),e.event.bind(window,"touchend",u),e.event.bind(t,"touchstart",f),e.event.bind(t,"touchmove",h),e.event.bind(t,"touchend",b)):r&&(window.PointerEvent?(e.event.bind(window,"pointerdown",c),e.event.bind(window,"pointerup",u),e.event.bind(t,"pointerdown",f),e.event.bind(t,"pointermove",h),e.event.bind(t,"pointerup",b)):window.MSPointerEvent&&(e.event.bind(window,"MSPointerDown",c),e.event.bind(window,"MSPointerUp",u),e.event.bind(t,"MSPointerDown",f),e.event.bind(t,"MSPointerMove",h),e.event.bind(t,"MSPointerUp",b)))}var o=t("../../lib/helper"),l=t("../instances"),i=t("../update-geometry"),s=t("../update-scroll");e.exports=function(t){if(o.env.supportsTouch||o.env.supportsIePointer){var e=l.get(t);r(t,e,o.env.supportsTouch,o.env.supportsIePointer)}}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],17:[function(t,e,n){"use strict";var r=t("../lib/helper"),o=t("../lib/class"),l=t("./instances"),i=t("./update-geometry"),s={"click-rail":t("./handler/click-rail"),"drag-scrollbar":t("./handler/drag-scrollbar"),keyboard:t("./handler/keyboard"),wheel:t("./handler/mouse-wheel"),touch:t("./handler/touch"),selection:t("./handler/selection")},a=t("./handler/native-scroll");e.exports=function(t,e){e="object"==typeof e?e:{},o.add(t,"ps-container");var n=l.add(t);n.settings=r.extend(n.settings,e),o.add(t,"ps-theme-"+n.settings.theme),n.settings.handlers.forEach(function(e){s[e](t)}),a(t),i(t)}},{"../lib/class":2,"../lib/helper":6,"./handler/click-rail":10,"./handler/drag-scrollbar":11,"./handler/keyboard":12,"./handler/mouse-wheel":13,"./handler/native-scroll":14,"./handler/selection":15,"./handler/touch":16,"./instances":18,"./update-geometry":19}],18:[function(t,e,n){"use strict";function r(t){function e(){a.add(t,"ps-focus")}function n(){a.remove(t,"ps-focus")}var r=this;r.settings=s.clone(c),r.containerWidth=null,r.containerHeight=null,r.contentWidth=null,r.contentHeight=null,r.isRtl="rtl"===u.css(t,"direction"),r.isNegativeScroll=function(){var e=t.scrollLeft,n=null;return t.scrollLeft=-1,n=t.scrollLeft<0,t.scrollLeft=e,n}(),r.negativeScrollAdjustment=r.isNegativeScroll?t.scrollWidth-t.clientWidth:0,r.event=new d,r.ownerDocument=t.ownerDocument||document,r.scrollbarXRail=u.appendTo(u.e("div","ps-scrollbar-x-rail"),t),r.scrollbarX=u.appendTo(u.e("div","ps-scrollbar-x"),r.scrollbarXRail),r.scrollbarX.setAttribute("tabindex",0),r.event.bind(r.scrollbarX,"focus",e),r.event.bind(r.scrollbarX,"blur",n),r.scrollbarXActive=null,r.scrollbarXWidth=null,r.scrollbarXLeft=null,r.scrollbarXBottom=s.toInt(u.css(r.scrollbarXRail,"bottom")),r.isScrollbarXUsingBottom=r.scrollbarXBottom===r.scrollbarXBottom,r.scrollbarXTop=r.isScrollbarXUsingBottom?null:s.toInt(u.css(r.scrollbarXRail,"top")),r.railBorderXWidth=s.toInt(u.css(r.scrollbarXRail,"borderLeftWidth"))+s.toInt(u.css(r.scrollbarXRail,"borderRightWidth")),u.css(r.scrollbarXRail,"display","block"),r.railXMarginWidth=s.toInt(u.css(r.scrollbarXRail,"marginLeft"))+s.toInt(u.css(r.scrollbarXRail,"marginRight")),u.css(r.scrollbarXRail,"display",""),r.railXWidth=null,r.railXRatio=null,r.scrollbarYRail=u.appendTo(u.e("div","ps-scrollbar-y-rail"),t),r.scrollbarY=u.appendTo(u.e("div","ps-scrollbar-y"),r.scrollbarYRail),r.scrollbarY.setAttribute("tabindex",0),r.event.bind(r.scrollbarY,"focus",e),r.event.bind(r.scrollbarY,"blur",n),r.scrollbarYActive=null,r.scrollbarYHeight=null,r.scrollbarYTop=null,r.scrollbarYRight=s.toInt(u.css(r.scrollbarYRail,"right")),r.isScrollbarYUsingRight=r.scrollbarYRight===r.scrollbarYRight,r.scrollbarYLeft=r.isScrollbarYUsingRight?null:s.toInt(u.css(r.scrollbarYRail,"left")),r.scrollbarYOuterWidth=r.isRtl?s.outerWidth(r.scrollbarY):null,r.railBorderYWidth=s.toInt(u.css(r.scrollbarYRail,"borderTopWidth"))+s.toInt(u.css(r.scrollbarYRail,"borderBottomWidth")),u.css(r.scrollbarYRail,"display","block"),r.railYMarginHeight=s.toInt(u.css(r.scrollbarYRail,"marginTop"))+s.toInt(u.css(r.scrollbarYRail,"marginBottom")),u.css(r.scrollbarYRail,"display",""),r.railYHeight=null,r.railYRatio=null}function o(t){return t.getAttribute("data-ps-id")}function l(t,e){t.setAttribute("data-ps-id",e)}function i(t){t.removeAttribute("data-ps-id")}var s=t("../lib/helper"),a=t("../lib/class"),c=t("./default-setting"),u=t("../lib/dom"),d=t("../lib/event-manager"),p=t("../lib/guid"),f={};n.add=function(t){var e=p();return l(t,e),f[e]=new r(t),f[e]},n.remove=function(t){delete f[o(t)],i(t)},n.get=function(t){return f[o(t)]}},{"../lib/class":2,"../lib/dom":3,"../lib/event-manager":4,"../lib/guid":5,"../lib/helper":6,"./default-setting":8}],19:[function(t,e,n){"use strict";function r(t,e){return t.settings.minScrollbarLength&&(e=Math.max(e,t.settings.minScrollbarLength)),t.settings.maxScrollbarLength&&(e=Math.min(e,t.settings.maxScrollbarLength)),e}function o(t,e){var n={width:e.railXWidth};e.isRtl?n.left=e.negativeScrollAdjustment+t.scrollLeft+e.containerWidth-e.contentWidth:n.left=t.scrollLeft,e.isScrollbarXUsingBottom?n.bottom=e.scrollbarXBottom-t.scrollTop:n.top=e.scrollbarXTop+t.scrollTop,s.css(e.scrollbarXRail,n);var r={top:t.scrollTop,height:e.railYHeight};e.isScrollbarYUsingRight?e.isRtl?r.right=e.contentWidth-(e.negativeScrollAdjustment+t.scrollLeft)-e.scrollbarYRight-e.scrollbarYOuterWidth:r.right=e.scrollbarYRight-t.scrollLeft:e.isRtl?r.left=e.negativeScrollAdjustment+t.scrollLeft+2*e.containerWidth-e.contentWidth-e.scrollbarYLeft-e.scrollbarYOuterWidth:r.left=e.scrollbarYLeft+t.scrollLeft,s.css(e.scrollbarYRail,r),s.css(e.scrollbarX,{left:e.scrollbarXLeft,width:e.scrollbarXWidth-e.railBorderXWidth}),s.css(e.scrollbarY,{top:e.scrollbarYTop,height:e.scrollbarYHeight-e.railBorderYWidth})}var l=t("../lib/helper"),i=t("../lib/class"),s=t("../lib/dom"),a=t("./instances"),c=t("./update-scroll");e.exports=function(t){var e=a.get(t);e.containerWidth=t.clientWidth,e.containerHeight=t.clientHeight,e.contentWidth=t.scrollWidth,e.contentHeight=t.scrollHeight;var n;t.contains(e.scrollbarXRail)||(n=s.queryChildren(t,".ps-scrollbar-x-rail"),n.length>0&&n.forEach(function(t){s.remove(t)}),s.appendTo(e.scrollbarXRail,t)),t.contains(e.scrollbarYRail)||(n=s.queryChildren(t,".ps-scrollbar-y-rail"),n.length>0&&n.forEach(function(t){s.remove(t)}),s.appendTo(e.scrollbarYRail,t)),!e.settings.suppressScrollX&&e.containerWidth+e.settings.scrollXMarginOffset=e.railXWidth-e.scrollbarXWidth&&(e.scrollbarXLeft=e.railXWidth-e.scrollbarXWidth),e.scrollbarYTop>=e.railYHeight-e.scrollbarYHeight&&(e.scrollbarYTop=e.railYHeight-e.scrollbarYHeight),o(t,e),e.scrollbarXActive?i.add(t,"ps-active-x"):(i.remove(t,"ps-active-x"),e.scrollbarXWidth=0,e.scrollbarXLeft=0,c(t,"left",0)),e.scrollbarYActive?i.add(t,"ps-active-y"):(i.remove(t,"ps-active-y"),e.scrollbarYHeight=0,e.scrollbarYTop=0,c(t,"top",0))}},{"../lib/class":2,"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-scroll":20}],20:[function(t,e,n){"use strict";var r,o,l=t("./instances"),i=function(t){var e=document.createEvent("Event");return e.initEvent(t,!0,!0),e};e.exports=function(t,e,n){if("undefined"==typeof t)throw"You must provide an element to the update-scroll function";if("undefined"==typeof e)throw"You must provide an axis to the update-scroll function";if("undefined"==typeof n)throw"You must provide a value to the update-scroll function";"top"===e&&n<=0&&(t.scrollTop=n=0,t.dispatchEvent(i("ps-y-reach-start"))),"left"===e&&n<=0&&(t.scrollLeft=n=0,t.dispatchEvent(i("ps-x-reach-start")));var s=l.get(t);"top"===e&&n>=s.contentHeight-s.containerHeight&&(n=s.contentHeight-s.containerHeight,n-t.scrollTop<=1?n=t.scrollTop:t.scrollTop=n,t.dispatchEvent(i("ps-y-reach-end"))),"left"===e&&n>=s.contentWidth-s.containerWidth&&(n=s.contentWidth-s.containerWidth,n-t.scrollLeft<=1?n=t.scrollLeft:t.scrollLeft=n,t.dispatchEvent(i("ps-x-reach-end"))),r||(r=t.scrollTop),o||(o=t.scrollLeft),"top"===e&&nr&&t.dispatchEvent(i("ps-scroll-down")),"left"===e&&no&&t.dispatchEvent(i("ps-scroll-right")),"top"===e&&(t.scrollTop=r=n,t.dispatchEvent(i("ps-scroll-y"))),"left"===e&&(t.scrollLeft=o=n,t.dispatchEvent(i("ps-scroll-x")))}},{"./instances":18}],21:[function(t,e,n){"use strict";var r=t("../lib/helper"),o=t("../lib/dom"),l=t("./instances"),i=t("./update-geometry"),s=t("./update-scroll");e.exports=function(t){var e=l.get(t);e&&(e.negativeScrollAdjustment=e.isNegativeScroll?t.scrollWidth-t.clientWidth:0,o.css(e.scrollbarXRail,"display","block"),o.css(e.scrollbarYRail,"display","block"),e.railXMarginWidth=r.toInt(o.css(e.scrollbarXRail,"marginLeft"))+r.toInt(o.css(e.scrollbarXRail,"marginRight")),e.railYMarginHeight=r.toInt(o.css(e.scrollbarYRail,"marginTop"))+r.toInt(o.css(e.scrollbarYRail,"marginBottom")),o.css(e.scrollbarXRail,"display","none"),o.css(e.scrollbarYRail,"display","none"),i(t),s(t,"top",t.scrollTop),s(t,"left",t.scrollLeft),o.css(e.scrollbarXRail,"display",""),o.css(e.scrollbarYRail,"display",""))}},{"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-geometry":19,"./update-scroll":20}]},{},[1]); \ No newline at end of file diff --git a/view/js/perfect-scrollbar/js/perfect-scrollbar.js b/view/js/perfect-scrollbar/js/perfect-scrollbar.js new file mode 100644 index 0000000000..ad0e71f3d1 --- /dev/null +++ b/view/js/perfect-scrollbar/js/perfect-scrollbar.js @@ -0,0 +1,1550 @@ +/* perfect-scrollbar v0.6.16 */ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0) { + classes.splice(idx, 1); + } + element.className = classes.join(' '); +} + +exports.add = function (element, className) { + if (element.classList) { + element.classList.add(className); + } else { + oldAdd(element, className); + } +}; + +exports.remove = function (element, className) { + if (element.classList) { + element.classList.remove(className); + } else { + oldRemove(element, className); + } +}; + +exports.list = function (element) { + if (element.classList) { + return Array.prototype.slice.apply(element.classList); + } else { + return element.className.split(' '); + } +}; + +},{}],3:[function(require,module,exports){ +'use strict'; + +var DOM = {}; + +DOM.e = function (tagName, className) { + var element = document.createElement(tagName); + element.className = className; + return element; +}; + +DOM.appendTo = function (child, parent) { + parent.appendChild(child); + return child; +}; + +function cssGet(element, styleName) { + return window.getComputedStyle(element)[styleName]; +} + +function cssSet(element, styleName, styleValue) { + if (typeof styleValue === 'number') { + styleValue = styleValue.toString() + 'px'; + } + element.style[styleName] = styleValue; + return element; +} + +function cssMultiSet(element, obj) { + for (var key in obj) { + var val = obj[key]; + if (typeof val === 'number') { + val = val.toString() + 'px'; + } + element.style[key] = val; + } + return element; +} + +DOM.css = function (element, styleNameOrObject, styleValue) { + if (typeof styleNameOrObject === 'object') { + // multiple set with object + return cssMultiSet(element, styleNameOrObject); + } else { + if (typeof styleValue === 'undefined') { + return cssGet(element, styleNameOrObject); + } else { + return cssSet(element, styleNameOrObject, styleValue); + } + } +}; + +DOM.matches = function (element, query) { + if (typeof element.matches !== 'undefined') { + return element.matches(query); + } else { + if (typeof element.matchesSelector !== 'undefined') { + return element.matchesSelector(query); + } else if (typeof element.webkitMatchesSelector !== 'undefined') { + return element.webkitMatchesSelector(query); + } else if (typeof element.mozMatchesSelector !== 'undefined') { + return element.mozMatchesSelector(query); + } else if (typeof element.msMatchesSelector !== 'undefined') { + return element.msMatchesSelector(query); + } + } +}; + +DOM.remove = function (element) { + if (typeof element.remove !== 'undefined') { + element.remove(); + } else { + if (element.parentNode) { + element.parentNode.removeChild(element); + } + } +}; + +DOM.queryChildren = function (element, selector) { + return Array.prototype.filter.call(element.childNodes, function (child) { + return DOM.matches(child, selector); + }); +}; + +module.exports = DOM; + +},{}],4:[function(require,module,exports){ +'use strict'; + +var EventElement = function (element) { + this.element = element; + this.events = {}; +}; + +EventElement.prototype.bind = function (eventName, handler) { + if (typeof this.events[eventName] === 'undefined') { + this.events[eventName] = []; + } + this.events[eventName].push(handler); + this.element.addEventListener(eventName, handler, false); +}; + +EventElement.prototype.unbind = function (eventName, handler) { + var isHandlerProvided = (typeof handler !== 'undefined'); + this.events[eventName] = this.events[eventName].filter(function (hdlr) { + if (isHandlerProvided && hdlr !== handler) { + return true; + } + this.element.removeEventListener(eventName, hdlr, false); + return false; + }, this); +}; + +EventElement.prototype.unbindAll = function () { + for (var name in this.events) { + this.unbind(name); + } +}; + +var EventManager = function () { + this.eventElements = []; +}; + +EventManager.prototype.eventElement = function (element) { + var ee = this.eventElements.filter(function (eventElement) { + return eventElement.element === element; + })[0]; + if (typeof ee === 'undefined') { + ee = new EventElement(element); + this.eventElements.push(ee); + } + return ee; +}; + +EventManager.prototype.bind = function (element, eventName, handler) { + this.eventElement(element).bind(eventName, handler); +}; + +EventManager.prototype.unbind = function (element, eventName, handler) { + this.eventElement(element).unbind(eventName, handler); +}; + +EventManager.prototype.unbindAll = function () { + for (var i = 0; i < this.eventElements.length; i++) { + this.eventElements[i].unbindAll(); + } +}; + +EventManager.prototype.once = function (element, eventName, handler) { + var ee = this.eventElement(element); + var onceHandler = function (e) { + ee.unbind(eventName, onceHandler); + handler(e); + }; + ee.bind(eventName, onceHandler); +}; + +module.exports = EventManager; + +},{}],5:[function(require,module,exports){ +'use strict'; + +module.exports = (function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return function () { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + }; +})(); + +},{}],6:[function(require,module,exports){ +'use strict'; + +var cls = require('./class'); +var dom = require('./dom'); + +var toInt = exports.toInt = function (x) { + return parseInt(x, 10) || 0; +}; + +var clone = exports.clone = function (obj) { + if (!obj) { + return null; + } else if (obj.constructor === Array) { + return obj.map(clone); + } else if (typeof obj === 'object') { + var result = {}; + for (var key in obj) { + result[key] = clone(obj[key]); + } + return result; + } else { + return obj; + } +}; + +exports.extend = function (original, source) { + var result = clone(original); + for (var key in source) { + result[key] = clone(source[key]); + } + return result; +}; + +exports.isEditable = function (el) { + return dom.matches(el, "input,[contenteditable]") || + dom.matches(el, "select,[contenteditable]") || + dom.matches(el, "textarea,[contenteditable]") || + dom.matches(el, "button,[contenteditable]"); +}; + +exports.removePsClasses = function (element) { + var clsList = cls.list(element); + for (var i = 0; i < clsList.length; i++) { + var className = clsList[i]; + if (className.indexOf('ps-') === 0) { + cls.remove(element, className); + } + } +}; + +exports.outerWidth = function (element) { + return toInt(dom.css(element, 'width')) + + toInt(dom.css(element, 'paddingLeft')) + + toInt(dom.css(element, 'paddingRight')) + + toInt(dom.css(element, 'borderLeftWidth')) + + toInt(dom.css(element, 'borderRightWidth')); +}; + +exports.startScrolling = function (element, axis) { + cls.add(element, 'ps-in-scrolling'); + if (typeof axis !== 'undefined') { + cls.add(element, 'ps-' + axis); + } else { + cls.add(element, 'ps-x'); + cls.add(element, 'ps-y'); + } +}; + +exports.stopScrolling = function (element, axis) { + cls.remove(element, 'ps-in-scrolling'); + if (typeof axis !== 'undefined') { + cls.remove(element, 'ps-' + axis); + } else { + cls.remove(element, 'ps-x'); + cls.remove(element, 'ps-y'); + } +}; + +exports.env = { + isWebKit: 'WebkitAppearance' in document.documentElement.style, + supportsTouch: (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch), + supportsIePointer: window.navigator.msMaxTouchPoints !== null +}; + +},{"./class":2,"./dom":3}],7:[function(require,module,exports){ +'use strict'; + +var destroy = require('./plugin/destroy'); +var initialize = require('./plugin/initialize'); +var update = require('./plugin/update'); + +module.exports = { + initialize: initialize, + update: update, + destroy: destroy +}; + +},{"./plugin/destroy":9,"./plugin/initialize":17,"./plugin/update":21}],8:[function(require,module,exports){ +'use strict'; + +module.exports = { + handlers: ['click-rail', 'drag-scrollbar', 'keyboard', 'wheel', 'touch'], + maxScrollbarLength: null, + minScrollbarLength: null, + scrollXMarginOffset: 0, + scrollYMarginOffset: 0, + suppressScrollX: false, + suppressScrollY: false, + swipePropagation: true, + useBothWheelAxes: false, + wheelPropagation: false, + wheelSpeed: 1, + theme: 'default' +}; + +},{}],9:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var dom = require('../lib/dom'); +var instances = require('./instances'); + +module.exports = function (element) { + var i = instances.get(element); + + if (!i) { + return; + } + + i.event.unbindAll(); + dom.remove(i.scrollbarX); + dom.remove(i.scrollbarY); + dom.remove(i.scrollbarXRail); + dom.remove(i.scrollbarYRail); + _.removePsClasses(element); + + instances.remove(element); +}; + +},{"../lib/dom":3,"../lib/helper":6,"./instances":18}],10:[function(require,module,exports){ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindClickRailHandler(element, i) { + function pageOffset(el) { + return el.getBoundingClientRect(); + } + var stopPropagation = function (e) { e.stopPropagation(); }; + + i.event.bind(i.scrollbarY, 'click', stopPropagation); + i.event.bind(i.scrollbarYRail, 'click', function (e) { + var positionTop = e.pageY - window.pageYOffset - pageOffset(i.scrollbarYRail).top; + var direction = positionTop > i.scrollbarYTop ? 1 : -1; + + updateScroll(element, 'top', element.scrollTop + direction * i.containerHeight); + updateGeometry(element); + + e.stopPropagation(); + }); + + i.event.bind(i.scrollbarX, 'click', stopPropagation); + i.event.bind(i.scrollbarXRail, 'click', function (e) { + var positionLeft = e.pageX - window.pageXOffset - pageOffset(i.scrollbarXRail).left; + var direction = positionLeft > i.scrollbarXLeft ? 1 : -1; + + updateScroll(element, 'left', element.scrollLeft + direction * i.containerWidth); + updateGeometry(element); + + e.stopPropagation(); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindClickRailHandler(element, i); +}; + +},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],11:[function(require,module,exports){ +'use strict'; + +var _ = require('../../lib/helper'); +var dom = require('../../lib/dom'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindMouseScrollXHandler(element, i) { + var currentLeft = null; + var currentPageX = null; + + function updateScrollLeft(deltaX) { + var newLeft = currentLeft + (deltaX * i.railXRatio); + var maxLeft = Math.max(0, i.scrollbarXRail.getBoundingClientRect().left) + (i.railXRatio * (i.railXWidth - i.scrollbarXWidth)); + + if (newLeft < 0) { + i.scrollbarXLeft = 0; + } else if (newLeft > maxLeft) { + i.scrollbarXLeft = maxLeft; + } else { + i.scrollbarXLeft = newLeft; + } + + var scrollLeft = _.toInt(i.scrollbarXLeft * (i.contentWidth - i.containerWidth) / (i.containerWidth - (i.railXRatio * i.scrollbarXWidth))) - i.negativeScrollAdjustment; + updateScroll(element, 'left', scrollLeft); + } + + var mouseMoveHandler = function (e) { + updateScrollLeft(e.pageX - currentPageX); + updateGeometry(element); + e.stopPropagation(); + e.preventDefault(); + }; + + var mouseUpHandler = function () { + _.stopScrolling(element, 'x'); + i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler); + }; + + i.event.bind(i.scrollbarX, 'mousedown', function (e) { + currentPageX = e.pageX; + currentLeft = _.toInt(dom.css(i.scrollbarX, 'left')) * i.railXRatio; + _.startScrolling(element, 'x'); + + i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler); + i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler); + + e.stopPropagation(); + e.preventDefault(); + }); +} + +function bindMouseScrollYHandler(element, i) { + var currentTop = null; + var currentPageY = null; + + function updateScrollTop(deltaY) { + var newTop = currentTop + (deltaY * i.railYRatio); + var maxTop = Math.max(0, i.scrollbarYRail.getBoundingClientRect().top) + (i.railYRatio * (i.railYHeight - i.scrollbarYHeight)); + + if (newTop < 0) { + i.scrollbarYTop = 0; + } else if (newTop > maxTop) { + i.scrollbarYTop = maxTop; + } else { + i.scrollbarYTop = newTop; + } + + var scrollTop = _.toInt(i.scrollbarYTop * (i.contentHeight - i.containerHeight) / (i.containerHeight - (i.railYRatio * i.scrollbarYHeight))); + updateScroll(element, 'top', scrollTop); + } + + var mouseMoveHandler = function (e) { + updateScrollTop(e.pageY - currentPageY); + updateGeometry(element); + e.stopPropagation(); + e.preventDefault(); + }; + + var mouseUpHandler = function () { + _.stopScrolling(element, 'y'); + i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler); + }; + + i.event.bind(i.scrollbarY, 'mousedown', function (e) { + currentPageY = e.pageY; + currentTop = _.toInt(dom.css(i.scrollbarY, 'top')) * i.railYRatio; + _.startScrolling(element, 'y'); + + i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler); + i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler); + + e.stopPropagation(); + e.preventDefault(); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindMouseScrollXHandler(element, i); + bindMouseScrollYHandler(element, i); +}; + +},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],12:[function(require,module,exports){ +'use strict'; + +var _ = require('../../lib/helper'); +var dom = require('../../lib/dom'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindKeyboardHandler(element, i) { + var hovered = false; + i.event.bind(element, 'mouseenter', function () { + hovered = true; + }); + i.event.bind(element, 'mouseleave', function () { + hovered = false; + }); + + var shouldPrevent = false; + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + if (deltaX === 0) { + if (!i.scrollbarYActive) { + return false; + } + if ((scrollTop === 0 && deltaY > 0) || (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)) { + return !i.settings.wheelPropagation; + } + } + + var scrollLeft = element.scrollLeft; + if (deltaY === 0) { + if (!i.scrollbarXActive) { + return false; + } + if ((scrollLeft === 0 && deltaX < 0) || (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)) { + return !i.settings.wheelPropagation; + } + } + return true; + } + + i.event.bind(i.ownerDocument, 'keydown', function (e) { + if ((e.isDefaultPrevented && e.isDefaultPrevented()) || e.defaultPrevented) { + return; + } + + var focused = dom.matches(i.scrollbarX, ':focus') || + dom.matches(i.scrollbarY, ':focus'); + + if (!hovered && !focused) { + return; + } + + var activeElement = document.activeElement ? document.activeElement : i.ownerDocument.activeElement; + if (activeElement) { + if (activeElement.tagName === 'IFRAME') { + activeElement = activeElement.contentDocument.activeElement; + } else { + // go deeper if element is a webcomponent + while (activeElement.shadowRoot) { + activeElement = activeElement.shadowRoot.activeElement; + } + } + if (_.isEditable(activeElement)) { + return; + } + } + + var deltaX = 0; + var deltaY = 0; + + switch (e.which) { + case 37: // left + if (e.metaKey) { + deltaX = -i.contentWidth; + } else if (e.altKey) { + deltaX = -i.containerWidth; + } else { + deltaX = -30; + } + break; + case 38: // up + if (e.metaKey) { + deltaY = i.contentHeight; + } else if (e.altKey) { + deltaY = i.containerHeight; + } else { + deltaY = 30; + } + break; + case 39: // right + if (e.metaKey) { + deltaX = i.contentWidth; + } else if (e.altKey) { + deltaX = i.containerWidth; + } else { + deltaX = 30; + } + break; + case 40: // down + if (e.metaKey) { + deltaY = -i.contentHeight; + } else if (e.altKey) { + deltaY = -i.containerHeight; + } else { + deltaY = -30; + } + break; + case 33: // page up + deltaY = 90; + break; + case 32: // space bar + if (e.shiftKey) { + deltaY = 90; + } else { + deltaY = -90; + } + break; + case 34: // page down + deltaY = -90; + break; + case 35: // end + if (e.ctrlKey) { + deltaY = -i.contentHeight; + } else { + deltaY = -i.containerHeight; + } + break; + case 36: // home + if (e.ctrlKey) { + deltaY = element.scrollTop; + } else { + deltaY = i.containerHeight; + } + break; + default: + return; + } + + updateScroll(element, 'top', element.scrollTop - deltaY); + updateScroll(element, 'left', element.scrollLeft + deltaX); + updateGeometry(element); + + shouldPrevent = shouldPreventDefault(deltaX, deltaY); + if (shouldPrevent) { + e.preventDefault(); + } + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindKeyboardHandler(element, i); +}; + +},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],13:[function(require,module,exports){ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindMouseWheelHandler(element, i) { + var shouldPrevent = false; + + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + if (deltaX === 0) { + if (!i.scrollbarYActive) { + return false; + } + if ((scrollTop === 0 && deltaY > 0) || (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)) { + return !i.settings.wheelPropagation; + } + } + + var scrollLeft = element.scrollLeft; + if (deltaY === 0) { + if (!i.scrollbarXActive) { + return false; + } + if ((scrollLeft === 0 && deltaX < 0) || (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)) { + return !i.settings.wheelPropagation; + } + } + return true; + } + + function getDeltaFromEvent(e) { + var deltaX = e.deltaX; + var deltaY = -1 * e.deltaY; + + if (typeof deltaX === "undefined" || typeof deltaY === "undefined") { + // OS X Safari + deltaX = -1 * e.wheelDeltaX / 6; + deltaY = e.wheelDeltaY / 6; + } + + if (e.deltaMode && e.deltaMode === 1) { + // Firefox in deltaMode 1: Line scrolling + deltaX *= 10; + deltaY *= 10; + } + + if (deltaX !== deltaX && deltaY !== deltaY/* NaN checks */) { + // IE in some mouse drivers + deltaX = 0; + deltaY = e.wheelDelta; + } + + if (e.shiftKey) { + // reverse axis with shift key + return [-deltaY, -deltaX]; + } + return [deltaX, deltaY]; + } + + function shouldBeConsumedByChild(deltaX, deltaY) { + var child = element.querySelector('textarea:hover, select[multiple]:hover, .ps-child:hover'); + if (child) { + if (!window.getComputedStyle(child).overflow.match(/(scroll|auto)/)) { + // if not scrollable + return false; + } + + var maxScrollTop = child.scrollHeight - child.clientHeight; + if (maxScrollTop > 0) { + if (!(child.scrollTop === 0 && deltaY > 0) && !(child.scrollTop === maxScrollTop && deltaY < 0)) { + return true; + } + } + var maxScrollLeft = child.scrollLeft - child.clientWidth; + if (maxScrollLeft > 0) { + if (!(child.scrollLeft === 0 && deltaX < 0) && !(child.scrollLeft === maxScrollLeft && deltaX > 0)) { + return true; + } + } + } + return false; + } + + function mousewheelHandler(e) { + var delta = getDeltaFromEvent(e); + + var deltaX = delta[0]; + var deltaY = delta[1]; + + if (shouldBeConsumedByChild(deltaX, deltaY)) { + return; + } + + shouldPrevent = false; + if (!i.settings.useBothWheelAxes) { + // deltaX will only be used for horizontal scrolling and deltaY will + // only be used for vertical scrolling - this is the default + updateScroll(element, 'top', element.scrollTop - (deltaY * i.settings.wheelSpeed)); + updateScroll(element, 'left', element.scrollLeft + (deltaX * i.settings.wheelSpeed)); + } else if (i.scrollbarYActive && !i.scrollbarXActive) { + // only vertical scrollbar is active and useBothWheelAxes option is + // active, so let's scroll vertical bar using both mouse wheel axes + if (deltaY) { + updateScroll(element, 'top', element.scrollTop - (deltaY * i.settings.wheelSpeed)); + } else { + updateScroll(element, 'top', element.scrollTop + (deltaX * i.settings.wheelSpeed)); + } + shouldPrevent = true; + } else if (i.scrollbarXActive && !i.scrollbarYActive) { + // useBothWheelAxes and only horizontal bar is active, so use both + // wheel axes for horizontal bar + if (deltaX) { + updateScroll(element, 'left', element.scrollLeft + (deltaX * i.settings.wheelSpeed)); + } else { + updateScroll(element, 'left', element.scrollLeft - (deltaY * i.settings.wheelSpeed)); + } + shouldPrevent = true; + } + + updateGeometry(element); + + shouldPrevent = (shouldPrevent || shouldPreventDefault(deltaX, deltaY)); + if (shouldPrevent) { + e.stopPropagation(); + e.preventDefault(); + } + } + + if (typeof window.onwheel !== "undefined") { + i.event.bind(element, 'wheel', mousewheelHandler); + } else if (typeof window.onmousewheel !== "undefined") { + i.event.bind(element, 'mousewheel', mousewheelHandler); + } +} + +module.exports = function (element) { + var i = instances.get(element); + bindMouseWheelHandler(element, i); +}; + +},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],14:[function(require,module,exports){ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); + +function bindNativeScrollHandler(element, i) { + i.event.bind(element, 'scroll', function () { + updateGeometry(element); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindNativeScrollHandler(element, i); +}; + +},{"../instances":18,"../update-geometry":19}],15:[function(require,module,exports){ +'use strict'; + +var _ = require('../../lib/helper'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindSelectionHandler(element, i) { + function getRangeNode() { + var selection = window.getSelection ? window.getSelection() : + document.getSelection ? document.getSelection() : ''; + if (selection.toString().length === 0) { + return null; + } else { + return selection.getRangeAt(0).commonAncestorContainer; + } + } + + var scrollingLoop = null; + var scrollDiff = {top: 0, left: 0}; + function startScrolling() { + if (!scrollingLoop) { + scrollingLoop = setInterval(function () { + if (!instances.get(element)) { + clearInterval(scrollingLoop); + return; + } + + updateScroll(element, 'top', element.scrollTop + scrollDiff.top); + updateScroll(element, 'left', element.scrollLeft + scrollDiff.left); + updateGeometry(element); + }, 50); // every .1 sec + } + } + function stopScrolling() { + if (scrollingLoop) { + clearInterval(scrollingLoop); + scrollingLoop = null; + } + _.stopScrolling(element); + } + + var isSelected = false; + i.event.bind(i.ownerDocument, 'selectionchange', function () { + if (element.contains(getRangeNode())) { + isSelected = true; + } else { + isSelected = false; + stopScrolling(); + } + }); + i.event.bind(window, 'mouseup', function () { + if (isSelected) { + isSelected = false; + stopScrolling(); + } + }); + i.event.bind(window, 'keyup', function () { + if (isSelected) { + isSelected = false; + stopScrolling(); + } + }); + + i.event.bind(window, 'mousemove', function (e) { + if (isSelected) { + var mousePosition = {x: e.pageX, y: e.pageY}; + var containerGeometry = { + left: element.offsetLeft, + right: element.offsetLeft + element.offsetWidth, + top: element.offsetTop, + bottom: element.offsetTop + element.offsetHeight + }; + + if (mousePosition.x < containerGeometry.left + 3) { + scrollDiff.left = -5; + _.startScrolling(element, 'x'); + } else if (mousePosition.x > containerGeometry.right - 3) { + scrollDiff.left = 5; + _.startScrolling(element, 'x'); + } else { + scrollDiff.left = 0; + } + + if (mousePosition.y < containerGeometry.top + 3) { + if (containerGeometry.top + 3 - mousePosition.y < 5) { + scrollDiff.top = -5; + } else { + scrollDiff.top = -20; + } + _.startScrolling(element, 'y'); + } else if (mousePosition.y > containerGeometry.bottom - 3) { + if (mousePosition.y - containerGeometry.bottom + 3 < 5) { + scrollDiff.top = 5; + } else { + scrollDiff.top = 20; + } + _.startScrolling(element, 'y'); + } else { + scrollDiff.top = 0; + } + + if (scrollDiff.top === 0 && scrollDiff.left === 0) { + stopScrolling(); + } else { + startScrolling(); + } + } + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindSelectionHandler(element, i); +}; + +},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],16:[function(require,module,exports){ +'use strict'; + +var _ = require('../../lib/helper'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindTouchHandler(element, i, supportsTouch, supportsIePointer) { + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + var scrollLeft = element.scrollLeft; + var magnitudeX = Math.abs(deltaX); + var magnitudeY = Math.abs(deltaY); + + if (magnitudeY > magnitudeX) { + // user is perhaps trying to swipe up/down the page + + if (((deltaY < 0) && (scrollTop === i.contentHeight - i.containerHeight)) || + ((deltaY > 0) && (scrollTop === 0))) { + return !i.settings.swipePropagation; + } + } else if (magnitudeX > magnitudeY) { + // user is perhaps trying to swipe left/right across the page + + if (((deltaX < 0) && (scrollLeft === i.contentWidth - i.containerWidth)) || + ((deltaX > 0) && (scrollLeft === 0))) { + return !i.settings.swipePropagation; + } + } + + return true; + } + + function applyTouchMove(differenceX, differenceY) { + updateScroll(element, 'top', element.scrollTop - differenceY); + updateScroll(element, 'left', element.scrollLeft - differenceX); + + updateGeometry(element); + } + + var startOffset = {}; + var startTime = 0; + var speed = {}; + var easingLoop = null; + var inGlobalTouch = false; + var inLocalTouch = false; + + function globalTouchStart() { + inGlobalTouch = true; + } + function globalTouchEnd() { + inGlobalTouch = false; + } + + function getTouch(e) { + if (e.targetTouches) { + return e.targetTouches[0]; + } else { + // Maybe IE pointer + return e; + } + } + function shouldHandle(e) { + if (e.targetTouches && e.targetTouches.length === 1) { + return true; + } + if (e.pointerType && e.pointerType !== 'mouse' && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) { + return true; + } + return false; + } + function touchStart(e) { + if (shouldHandle(e)) { + inLocalTouch = true; + + var touch = getTouch(e); + + startOffset.pageX = touch.pageX; + startOffset.pageY = touch.pageY; + + startTime = (new Date()).getTime(); + + if (easingLoop !== null) { + clearInterval(easingLoop); + } + + e.stopPropagation(); + } + } + function touchMove(e) { + if (!inLocalTouch && i.settings.swipePropagation) { + touchStart(e); + } + if (!inGlobalTouch && inLocalTouch && shouldHandle(e)) { + var touch = getTouch(e); + + var currentOffset = {pageX: touch.pageX, pageY: touch.pageY}; + + var differenceX = currentOffset.pageX - startOffset.pageX; + var differenceY = currentOffset.pageY - startOffset.pageY; + + applyTouchMove(differenceX, differenceY); + startOffset = currentOffset; + + var currentTime = (new Date()).getTime(); + + var timeGap = currentTime - startTime; + if (timeGap > 0) { + speed.x = differenceX / timeGap; + speed.y = differenceY / timeGap; + startTime = currentTime; + } + + if (shouldPreventDefault(differenceX, differenceY)) { + e.stopPropagation(); + e.preventDefault(); + } + } + } + function touchEnd() { + if (!inGlobalTouch && inLocalTouch) { + inLocalTouch = false; + + clearInterval(easingLoop); + easingLoop = setInterval(function () { + if (!instances.get(element)) { + clearInterval(easingLoop); + return; + } + + if (!speed.x && !speed.y) { + clearInterval(easingLoop); + return; + } + + if (Math.abs(speed.x) < 0.01 && Math.abs(speed.y) < 0.01) { + clearInterval(easingLoop); + return; + } + + applyTouchMove(speed.x * 30, speed.y * 30); + + speed.x *= 0.8; + speed.y *= 0.8; + }, 10); + } + } + + if (supportsTouch) { + i.event.bind(window, 'touchstart', globalTouchStart); + i.event.bind(window, 'touchend', globalTouchEnd); + i.event.bind(element, 'touchstart', touchStart); + i.event.bind(element, 'touchmove', touchMove); + i.event.bind(element, 'touchend', touchEnd); + } else if (supportsIePointer) { + if (window.PointerEvent) { + i.event.bind(window, 'pointerdown', globalTouchStart); + i.event.bind(window, 'pointerup', globalTouchEnd); + i.event.bind(element, 'pointerdown', touchStart); + i.event.bind(element, 'pointermove', touchMove); + i.event.bind(element, 'pointerup', touchEnd); + } else if (window.MSPointerEvent) { + i.event.bind(window, 'MSPointerDown', globalTouchStart); + i.event.bind(window, 'MSPointerUp', globalTouchEnd); + i.event.bind(element, 'MSPointerDown', touchStart); + i.event.bind(element, 'MSPointerMove', touchMove); + i.event.bind(element, 'MSPointerUp', touchEnd); + } + } +} + +module.exports = function (element) { + if (!_.env.supportsTouch && !_.env.supportsIePointer) { + return; + } + + var i = instances.get(element); + bindTouchHandler(element, i, _.env.supportsTouch, _.env.supportsIePointer); +}; + +},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],17:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var instances = require('./instances'); +var updateGeometry = require('./update-geometry'); + +// Handlers +var handlers = { + 'click-rail': require('./handler/click-rail'), + 'drag-scrollbar': require('./handler/drag-scrollbar'), + 'keyboard': require('./handler/keyboard'), + 'wheel': require('./handler/mouse-wheel'), + 'touch': require('./handler/touch'), + 'selection': require('./handler/selection') +}; +var nativeScrollHandler = require('./handler/native-scroll'); + +module.exports = function (element, userSettings) { + userSettings = typeof userSettings === 'object' ? userSettings : {}; + + cls.add(element, 'ps-container'); + + // Create a plugin instance. + var i = instances.add(element); + + i.settings = _.extend(i.settings, userSettings); + cls.add(element, 'ps-theme-' + i.settings.theme); + + i.settings.handlers.forEach(function (handlerName) { + handlers[handlerName](element); + }); + + nativeScrollHandler(element); + + updateGeometry(element); +}; + +},{"../lib/class":2,"../lib/helper":6,"./handler/click-rail":10,"./handler/drag-scrollbar":11,"./handler/keyboard":12,"./handler/mouse-wheel":13,"./handler/native-scroll":14,"./handler/selection":15,"./handler/touch":16,"./instances":18,"./update-geometry":19}],18:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var defaultSettings = require('./default-setting'); +var dom = require('../lib/dom'); +var EventManager = require('../lib/event-manager'); +var guid = require('../lib/guid'); + +var instances = {}; + +function Instance(element) { + var i = this; + + i.settings = _.clone(defaultSettings); + i.containerWidth = null; + i.containerHeight = null; + i.contentWidth = null; + i.contentHeight = null; + + i.isRtl = dom.css(element, 'direction') === "rtl"; + i.isNegativeScroll = (function () { + var originalScrollLeft = element.scrollLeft; + var result = null; + element.scrollLeft = -1; + result = element.scrollLeft < 0; + element.scrollLeft = originalScrollLeft; + return result; + })(); + i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0; + i.event = new EventManager(); + i.ownerDocument = element.ownerDocument || document; + + function focus() { + cls.add(element, 'ps-focus'); + } + + function blur() { + cls.remove(element, 'ps-focus'); + } + + i.scrollbarXRail = dom.appendTo(dom.e('div', 'ps-scrollbar-x-rail'), element); + i.scrollbarX = dom.appendTo(dom.e('div', 'ps-scrollbar-x'), i.scrollbarXRail); + i.scrollbarX.setAttribute('tabindex', 0); + i.event.bind(i.scrollbarX, 'focus', focus); + i.event.bind(i.scrollbarX, 'blur', blur); + i.scrollbarXActive = null; + i.scrollbarXWidth = null; + i.scrollbarXLeft = null; + i.scrollbarXBottom = _.toInt(dom.css(i.scrollbarXRail, 'bottom')); + i.isScrollbarXUsingBottom = i.scrollbarXBottom === i.scrollbarXBottom; // !isNaN + i.scrollbarXTop = i.isScrollbarXUsingBottom ? null : _.toInt(dom.css(i.scrollbarXRail, 'top')); + i.railBorderXWidth = _.toInt(dom.css(i.scrollbarXRail, 'borderLeftWidth')) + _.toInt(dom.css(i.scrollbarXRail, 'borderRightWidth')); + // Set rail to display:block to calculate margins + dom.css(i.scrollbarXRail, 'display', 'block'); + i.railXMarginWidth = _.toInt(dom.css(i.scrollbarXRail, 'marginLeft')) + _.toInt(dom.css(i.scrollbarXRail, 'marginRight')); + dom.css(i.scrollbarXRail, 'display', ''); + i.railXWidth = null; + i.railXRatio = null; + + i.scrollbarYRail = dom.appendTo(dom.e('div', 'ps-scrollbar-y-rail'), element); + i.scrollbarY = dom.appendTo(dom.e('div', 'ps-scrollbar-y'), i.scrollbarYRail); + i.scrollbarY.setAttribute('tabindex', 0); + i.event.bind(i.scrollbarY, 'focus', focus); + i.event.bind(i.scrollbarY, 'blur', blur); + i.scrollbarYActive = null; + i.scrollbarYHeight = null; + i.scrollbarYTop = null; + i.scrollbarYRight = _.toInt(dom.css(i.scrollbarYRail, 'right')); + i.isScrollbarYUsingRight = i.scrollbarYRight === i.scrollbarYRight; // !isNaN + i.scrollbarYLeft = i.isScrollbarYUsingRight ? null : _.toInt(dom.css(i.scrollbarYRail, 'left')); + i.scrollbarYOuterWidth = i.isRtl ? _.outerWidth(i.scrollbarY) : null; + i.railBorderYWidth = _.toInt(dom.css(i.scrollbarYRail, 'borderTopWidth')) + _.toInt(dom.css(i.scrollbarYRail, 'borderBottomWidth')); + dom.css(i.scrollbarYRail, 'display', 'block'); + i.railYMarginHeight = _.toInt(dom.css(i.scrollbarYRail, 'marginTop')) + _.toInt(dom.css(i.scrollbarYRail, 'marginBottom')); + dom.css(i.scrollbarYRail, 'display', ''); + i.railYHeight = null; + i.railYRatio = null; +} + +function getId(element) { + return element.getAttribute('data-ps-id'); +} + +function setId(element, id) { + element.setAttribute('data-ps-id', id); +} + +function removeId(element) { + element.removeAttribute('data-ps-id'); +} + +exports.add = function (element) { + var newId = guid(); + setId(element, newId); + instances[newId] = new Instance(element); + return instances[newId]; +}; + +exports.remove = function (element) { + delete instances[getId(element)]; + removeId(element); +}; + +exports.get = function (element) { + return instances[getId(element)]; +}; + +},{"../lib/class":2,"../lib/dom":3,"../lib/event-manager":4,"../lib/guid":5,"../lib/helper":6,"./default-setting":8}],19:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var dom = require('../lib/dom'); +var instances = require('./instances'); +var updateScroll = require('./update-scroll'); + +function getThumbSize(i, thumbSize) { + if (i.settings.minScrollbarLength) { + thumbSize = Math.max(thumbSize, i.settings.minScrollbarLength); + } + if (i.settings.maxScrollbarLength) { + thumbSize = Math.min(thumbSize, i.settings.maxScrollbarLength); + } + return thumbSize; +} + +function updateCss(element, i) { + var xRailOffset = {width: i.railXWidth}; + if (i.isRtl) { + xRailOffset.left = i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth - i.contentWidth; + } else { + xRailOffset.left = element.scrollLeft; + } + if (i.isScrollbarXUsingBottom) { + xRailOffset.bottom = i.scrollbarXBottom - element.scrollTop; + } else { + xRailOffset.top = i.scrollbarXTop + element.scrollTop; + } + dom.css(i.scrollbarXRail, xRailOffset); + + var yRailOffset = {top: element.scrollTop, height: i.railYHeight}; + if (i.isScrollbarYUsingRight) { + if (i.isRtl) { + yRailOffset.right = i.contentWidth - (i.negativeScrollAdjustment + element.scrollLeft) - i.scrollbarYRight - i.scrollbarYOuterWidth; + } else { + yRailOffset.right = i.scrollbarYRight - element.scrollLeft; + } + } else { + if (i.isRtl) { + yRailOffset.left = i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth * 2 - i.contentWidth - i.scrollbarYLeft - i.scrollbarYOuterWidth; + } else { + yRailOffset.left = i.scrollbarYLeft + element.scrollLeft; + } + } + dom.css(i.scrollbarYRail, yRailOffset); + + dom.css(i.scrollbarX, {left: i.scrollbarXLeft, width: i.scrollbarXWidth - i.railBorderXWidth}); + dom.css(i.scrollbarY, {top: i.scrollbarYTop, height: i.scrollbarYHeight - i.railBorderYWidth}); +} + +module.exports = function (element) { + var i = instances.get(element); + + i.containerWidth = element.clientWidth; + i.containerHeight = element.clientHeight; + i.contentWidth = element.scrollWidth; + i.contentHeight = element.scrollHeight; + + var existingRails; + if (!element.contains(i.scrollbarXRail)) { + existingRails = dom.queryChildren(element, '.ps-scrollbar-x-rail'); + if (existingRails.length > 0) { + existingRails.forEach(function (rail) { + dom.remove(rail); + }); + } + dom.appendTo(i.scrollbarXRail, element); + } + if (!element.contains(i.scrollbarYRail)) { + existingRails = dom.queryChildren(element, '.ps-scrollbar-y-rail'); + if (existingRails.length > 0) { + existingRails.forEach(function (rail) { + dom.remove(rail); + }); + } + dom.appendTo(i.scrollbarYRail, element); + } + + if (!i.settings.suppressScrollX && i.containerWidth + i.settings.scrollXMarginOffset < i.contentWidth) { + i.scrollbarXActive = true; + i.railXWidth = i.containerWidth - i.railXMarginWidth; + i.railXRatio = i.containerWidth / i.railXWidth; + i.scrollbarXWidth = getThumbSize(i, _.toInt(i.railXWidth * i.containerWidth / i.contentWidth)); + i.scrollbarXLeft = _.toInt((i.negativeScrollAdjustment + element.scrollLeft) * (i.railXWidth - i.scrollbarXWidth) / (i.contentWidth - i.containerWidth)); + } else { + i.scrollbarXActive = false; + } + + if (!i.settings.suppressScrollY && i.containerHeight + i.settings.scrollYMarginOffset < i.contentHeight) { + i.scrollbarYActive = true; + i.railYHeight = i.containerHeight - i.railYMarginHeight; + i.railYRatio = i.containerHeight / i.railYHeight; + i.scrollbarYHeight = getThumbSize(i, _.toInt(i.railYHeight * i.containerHeight / i.contentHeight)); + i.scrollbarYTop = _.toInt(element.scrollTop * (i.railYHeight - i.scrollbarYHeight) / (i.contentHeight - i.containerHeight)); + } else { + i.scrollbarYActive = false; + } + + if (i.scrollbarXLeft >= i.railXWidth - i.scrollbarXWidth) { + i.scrollbarXLeft = i.railXWidth - i.scrollbarXWidth; + } + if (i.scrollbarYTop >= i.railYHeight - i.scrollbarYHeight) { + i.scrollbarYTop = i.railYHeight - i.scrollbarYHeight; + } + + updateCss(element, i); + + if (i.scrollbarXActive) { + cls.add(element, 'ps-active-x'); + } else { + cls.remove(element, 'ps-active-x'); + i.scrollbarXWidth = 0; + i.scrollbarXLeft = 0; + updateScroll(element, 'left', 0); + } + if (i.scrollbarYActive) { + cls.add(element, 'ps-active-y'); + } else { + cls.remove(element, 'ps-active-y'); + i.scrollbarYHeight = 0; + i.scrollbarYTop = 0; + updateScroll(element, 'top', 0); + } +}; + +},{"../lib/class":2,"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-scroll":20}],20:[function(require,module,exports){ +'use strict'; + +var instances = require('./instances'); + +var lastTop; +var lastLeft; + +var createDOMEvent = function (name) { + var event = document.createEvent("Event"); + event.initEvent(name, true, true); + return event; +}; + +module.exports = function (element, axis, value) { + if (typeof element === 'undefined') { + throw 'You must provide an element to the update-scroll function'; + } + + if (typeof axis === 'undefined') { + throw 'You must provide an axis to the update-scroll function'; + } + + if (typeof value === 'undefined') { + throw 'You must provide a value to the update-scroll function'; + } + + if (axis === 'top' && value <= 0) { + element.scrollTop = value = 0; // don't allow negative scroll + element.dispatchEvent(createDOMEvent('ps-y-reach-start')); + } + + if (axis === 'left' && value <= 0) { + element.scrollLeft = value = 0; // don't allow negative scroll + element.dispatchEvent(createDOMEvent('ps-x-reach-start')); + } + + var i = instances.get(element); + + if (axis === 'top' && value >= i.contentHeight - i.containerHeight) { + // don't allow scroll past container + value = i.contentHeight - i.containerHeight; + if (value - element.scrollTop <= 1) { + // mitigates rounding errors on non-subpixel scroll values + value = element.scrollTop; + } else { + element.scrollTop = value; + } + element.dispatchEvent(createDOMEvent('ps-y-reach-end')); + } + + if (axis === 'left' && value >= i.contentWidth - i.containerWidth) { + // don't allow scroll past container + value = i.contentWidth - i.containerWidth; + if (value - element.scrollLeft <= 1) { + // mitigates rounding errors on non-subpixel scroll values + value = element.scrollLeft; + } else { + element.scrollLeft = value; + } + element.dispatchEvent(createDOMEvent('ps-x-reach-end')); + } + + if (!lastTop) { + lastTop = element.scrollTop; + } + + if (!lastLeft) { + lastLeft = element.scrollLeft; + } + + if (axis === 'top' && value < lastTop) { + element.dispatchEvent(createDOMEvent('ps-scroll-up')); + } + + if (axis === 'top' && value > lastTop) { + element.dispatchEvent(createDOMEvent('ps-scroll-down')); + } + + if (axis === 'left' && value < lastLeft) { + element.dispatchEvent(createDOMEvent('ps-scroll-left')); + } + + if (axis === 'left' && value > lastLeft) { + element.dispatchEvent(createDOMEvent('ps-scroll-right')); + } + + if (axis === 'top') { + element.scrollTop = lastTop = value; + element.dispatchEvent(createDOMEvent('ps-scroll-y')); + } + + if (axis === 'left') { + element.scrollLeft = lastLeft = value; + element.dispatchEvent(createDOMEvent('ps-scroll-x')); + } + +}; + +},{"./instances":18}],21:[function(require,module,exports){ +'use strict'; + +var _ = require('../lib/helper'); +var dom = require('../lib/dom'); +var instances = require('./instances'); +var updateGeometry = require('./update-geometry'); +var updateScroll = require('./update-scroll'); + +module.exports = function (element) { + var i = instances.get(element); + + if (!i) { + return; + } + + // Recalcuate negative scrollLeft adjustment + i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0; + + // Recalculate rail margins + dom.css(i.scrollbarXRail, 'display', 'block'); + dom.css(i.scrollbarYRail, 'display', 'block'); + i.railXMarginWidth = _.toInt(dom.css(i.scrollbarXRail, 'marginLeft')) + _.toInt(dom.css(i.scrollbarXRail, 'marginRight')); + i.railYMarginHeight = _.toInt(dom.css(i.scrollbarYRail, 'marginTop')) + _.toInt(dom.css(i.scrollbarYRail, 'marginBottom')); + + // Hide scrollbars not to affect scrollWidth and scrollHeight + dom.css(i.scrollbarXRail, 'display', 'none'); + dom.css(i.scrollbarYRail, 'display', 'none'); + + updateGeometry(element); + + // Update top/left scroll to trigger events + updateScroll(element, 'top', element.scrollTop); + updateScroll(element, 'left', element.scrollLeft); + + dom.css(i.scrollbarXRail, 'display', ''); + dom.css(i.scrollbarYRail, 'display', ''); +}; + +},{"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-geometry":19,"./update-scroll":20}]},{},[1]); diff --git a/view/js/perfect-scrollbar/js/perfect-scrollbar.min.js b/view/js/perfect-scrollbar/js/perfect-scrollbar.min.js new file mode 100644 index 0000000000..4d76567e82 --- /dev/null +++ b/view/js/perfect-scrollbar/js/perfect-scrollbar.min.js @@ -0,0 +1,2 @@ +/* perfect-scrollbar v0.6.16 */ +!function t(e,n,r){function o(i,s){if(!n[i]){if(!e[i]){var a="function"==typeof require&&require;if(!s&&a)return a(i,!0);if(l)return l(i,!0);var c=new Error("Cannot find module '"+i+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[i]={exports:{}};e[i][0].call(u.exports,function(t){var n=e[i][1][t];return o(n?n:t)},u,u.exports,t,e,n,r)}return n[i].exports}for(var l="function"==typeof require&&require,i=0;i=0&&n.splice(r,1),t.className=n.join(" ")}n.add=function(t,e){t.classList?t.classList.add(e):r(t,e)},n.remove=function(t,e){t.classList?t.classList.remove(e):o(t,e)},n.list=function(t){return t.classList?Array.prototype.slice.apply(t.classList):t.className.split(" ")}},{}],3:[function(t,e,n){"use strict";function r(t,e){return window.getComputedStyle(t)[e]}function o(t,e,n){return"number"==typeof n&&(n=n.toString()+"px"),t.style[e]=n,t}function l(t,e){for(var n in e){var r=e[n];"number"==typeof r&&(r=r.toString()+"px"),t.style[n]=r}return t}var i={};i.e=function(t,e){var n=document.createElement(t);return n.className=e,n},i.appendTo=function(t,e){return e.appendChild(t),t},i.css=function(t,e,n){return"object"==typeof e?l(t,e):"undefined"==typeof n?r(t,e):o(t,e,n)},i.matches=function(t,e){return"undefined"!=typeof t.matches?t.matches(e):"undefined"!=typeof t.matchesSelector?t.matchesSelector(e):"undefined"!=typeof t.webkitMatchesSelector?t.webkitMatchesSelector(e):"undefined"!=typeof t.mozMatchesSelector?t.mozMatchesSelector(e):"undefined"!=typeof t.msMatchesSelector?t.msMatchesSelector(e):void 0},i.remove=function(t){"undefined"!=typeof t.remove?t.remove():t.parentNode&&t.parentNode.removeChild(t)},i.queryChildren=function(t,e){return Array.prototype.filter.call(t.childNodes,function(t){return i.matches(t,e)})},e.exports=i},{}],4:[function(t,e,n){"use strict";var r=function(t){this.element=t,this.events={}};r.prototype.bind=function(t,e){"undefined"==typeof this.events[t]&&(this.events[t]=[]),this.events[t].push(e),this.element.addEventListener(t,e,!1)},r.prototype.unbind=function(t,e){var n="undefined"!=typeof e;this.events[t]=this.events[t].filter(function(r){return!(!n||r===e)||(this.element.removeEventListener(t,r,!1),!1)},this)},r.prototype.unbindAll=function(){for(var t in this.events)this.unbind(t)};var o=function(){this.eventElements=[]};o.prototype.eventElement=function(t){var e=this.eventElements.filter(function(e){return e.element===t})[0];return"undefined"==typeof e&&(e=new r(t),this.eventElements.push(e)),e},o.prototype.bind=function(t,e,n){this.eventElement(t).bind(e,n)},o.prototype.unbind=function(t,e,n){this.eventElement(t).unbind(e,n)},o.prototype.unbindAll=function(){for(var t=0;te.scrollbarYTop?1:-1;i(t,"top",t.scrollTop+s*e.containerHeight),l(t),r.stopPropagation()}),e.event.bind(e.scrollbarX,"click",r),e.event.bind(e.scrollbarXRail,"click",function(r){var o=r.pageX-window.pageXOffset-n(e.scrollbarXRail).left,s=o>e.scrollbarXLeft?1:-1;i(t,"left",t.scrollLeft+s*e.containerWidth),l(t),r.stopPropagation()})}var o=t("../instances"),l=t("../update-geometry"),i=t("../update-scroll");e.exports=function(t){var e=o.get(t);r(t,e)}},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],11:[function(t,e,n){"use strict";function r(t,e){function n(n){var o=r+n*e.railXRatio,i=Math.max(0,e.scrollbarXRail.getBoundingClientRect().left)+e.railXRatio*(e.railXWidth-e.scrollbarXWidth);o<0?e.scrollbarXLeft=0:o>i?e.scrollbarXLeft=i:e.scrollbarXLeft=o;var s=l.toInt(e.scrollbarXLeft*(e.contentWidth-e.containerWidth)/(e.containerWidth-e.railXRatio*e.scrollbarXWidth))-e.negativeScrollAdjustment;c(t,"left",s)}var r=null,o=null,s=function(e){n(e.pageX-o),a(t),e.stopPropagation(),e.preventDefault()},u=function(){l.stopScrolling(t,"x"),e.event.unbind(e.ownerDocument,"mousemove",s)};e.event.bind(e.scrollbarX,"mousedown",function(n){o=n.pageX,r=l.toInt(i.css(e.scrollbarX,"left"))*e.railXRatio,l.startScrolling(t,"x"),e.event.bind(e.ownerDocument,"mousemove",s),e.event.once(e.ownerDocument,"mouseup",u),n.stopPropagation(),n.preventDefault()})}function o(t,e){function n(n){var o=r+n*e.railYRatio,i=Math.max(0,e.scrollbarYRail.getBoundingClientRect().top)+e.railYRatio*(e.railYHeight-e.scrollbarYHeight);o<0?e.scrollbarYTop=0:o>i?e.scrollbarYTop=i:e.scrollbarYTop=o;var s=l.toInt(e.scrollbarYTop*(e.contentHeight-e.containerHeight)/(e.containerHeight-e.railYRatio*e.scrollbarYHeight));c(t,"top",s)}var r=null,o=null,s=function(e){n(e.pageY-o),a(t),e.stopPropagation(),e.preventDefault()},u=function(){l.stopScrolling(t,"y"),e.event.unbind(e.ownerDocument,"mousemove",s)};e.event.bind(e.scrollbarY,"mousedown",function(n){o=n.pageY,r=l.toInt(i.css(e.scrollbarY,"top"))*e.railYRatio,l.startScrolling(t,"y"),e.event.bind(e.ownerDocument,"mousemove",s),e.event.once(e.ownerDocument,"mouseup",u),n.stopPropagation(),n.preventDefault()})}var l=t("../../lib/helper"),i=t("../../lib/dom"),s=t("../instances"),a=t("../update-geometry"),c=t("../update-scroll");e.exports=function(t){var e=s.get(t);r(t,e),o(t,e)}},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],12:[function(t,e,n){"use strict";function r(t,e){function n(n,r){var o=t.scrollTop;if(0===n){if(!e.scrollbarYActive)return!1;if(0===o&&r>0||o>=e.contentHeight-e.containerHeight&&r<0)return!e.settings.wheelPropagation}var l=t.scrollLeft;if(0===r){if(!e.scrollbarXActive)return!1;if(0===l&&n<0||l>=e.contentWidth-e.containerWidth&&n>0)return!e.settings.wheelPropagation}return!0}var r=!1;e.event.bind(t,"mouseenter",function(){r=!0}),e.event.bind(t,"mouseleave",function(){r=!1});var i=!1;e.event.bind(e.ownerDocument,"keydown",function(c){if(!(c.isDefaultPrevented&&c.isDefaultPrevented()||c.defaultPrevented)){var u=l.matches(e.scrollbarX,":focus")||l.matches(e.scrollbarY,":focus");if(r||u){var d=document.activeElement?document.activeElement:e.ownerDocument.activeElement;if(d){if("IFRAME"===d.tagName)d=d.contentDocument.activeElement;else for(;d.shadowRoot;)d=d.shadowRoot.activeElement;if(o.isEditable(d))return}var p=0,f=0;switch(c.which){case 37:p=c.metaKey?-e.contentWidth:c.altKey?-e.containerWidth:-30;break;case 38:f=c.metaKey?e.contentHeight:c.altKey?e.containerHeight:30;break;case 39:p=c.metaKey?e.contentWidth:c.altKey?e.containerWidth:30;break;case 40:f=c.metaKey?-e.contentHeight:c.altKey?-e.containerHeight:-30;break;case 33:f=90;break;case 32:f=c.shiftKey?90:-90;break;case 34:f=-90;break;case 35:f=c.ctrlKey?-e.contentHeight:-e.containerHeight;break;case 36:f=c.ctrlKey?t.scrollTop:e.containerHeight;break;default:return}a(t,"top",t.scrollTop-f),a(t,"left",t.scrollLeft+p),s(t),i=n(p,f),i&&c.preventDefault()}}})}var o=t("../../lib/helper"),l=t("../../lib/dom"),i=t("../instances"),s=t("../update-geometry"),a=t("../update-scroll");e.exports=function(t){var e=i.get(t);r(t,e)}},{"../../lib/dom":3,"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],13:[function(t,e,n){"use strict";function r(t,e){function n(n,r){var o=t.scrollTop;if(0===n){if(!e.scrollbarYActive)return!1;if(0===o&&r>0||o>=e.contentHeight-e.containerHeight&&r<0)return!e.settings.wheelPropagation}var l=t.scrollLeft;if(0===r){if(!e.scrollbarXActive)return!1;if(0===l&&n<0||l>=e.contentWidth-e.containerWidth&&n>0)return!e.settings.wheelPropagation}return!0}function r(t){var e=t.deltaX,n=-1*t.deltaY;return"undefined"!=typeof e&&"undefined"!=typeof n||(e=-1*t.wheelDeltaX/6,n=t.wheelDeltaY/6),t.deltaMode&&1===t.deltaMode&&(e*=10,n*=10),e!==e&&n!==n&&(e=0,n=t.wheelDelta),t.shiftKey?[-n,-e]:[e,n]}function o(e,n){var r=t.querySelector("textarea:hover, select[multiple]:hover, .ps-child:hover");if(r){if(!window.getComputedStyle(r).overflow.match(/(scroll|auto)/))return!1;var o=r.scrollHeight-r.clientHeight;if(o>0&&!(0===r.scrollTop&&n>0||r.scrollTop===o&&n<0))return!0;var l=r.scrollLeft-r.clientWidth;if(l>0&&!(0===r.scrollLeft&&e<0||r.scrollLeft===l&&e>0))return!0}return!1}function s(s){var c=r(s),u=c[0],d=c[1];o(u,d)||(a=!1,e.settings.useBothWheelAxes?e.scrollbarYActive&&!e.scrollbarXActive?(d?i(t,"top",t.scrollTop-d*e.settings.wheelSpeed):i(t,"top",t.scrollTop+u*e.settings.wheelSpeed),a=!0):e.scrollbarXActive&&!e.scrollbarYActive&&(u?i(t,"left",t.scrollLeft+u*e.settings.wheelSpeed):i(t,"left",t.scrollLeft-d*e.settings.wheelSpeed),a=!0):(i(t,"top",t.scrollTop-d*e.settings.wheelSpeed),i(t,"left",t.scrollLeft+u*e.settings.wheelSpeed)),l(t),a=a||n(u,d),a&&(s.stopPropagation(),s.preventDefault()))}var a=!1;"undefined"!=typeof window.onwheel?e.event.bind(t,"wheel",s):"undefined"!=typeof window.onmousewheel&&e.event.bind(t,"mousewheel",s)}var o=t("../instances"),l=t("../update-geometry"),i=t("../update-scroll");e.exports=function(t){var e=o.get(t);r(t,e)}},{"../instances":18,"../update-geometry":19,"../update-scroll":20}],14:[function(t,e,n){"use strict";function r(t,e){e.event.bind(t,"scroll",function(){l(t)})}var o=t("../instances"),l=t("../update-geometry");e.exports=function(t){var e=o.get(t);r(t,e)}},{"../instances":18,"../update-geometry":19}],15:[function(t,e,n){"use strict";function r(t,e){function n(){var t=window.getSelection?window.getSelection():document.getSelection?document.getSelection():"";return 0===t.toString().length?null:t.getRangeAt(0).commonAncestorContainer}function r(){c||(c=setInterval(function(){return l.get(t)?(s(t,"top",t.scrollTop+u.top),s(t,"left",t.scrollLeft+u.left),void i(t)):void clearInterval(c)},50))}function a(){c&&(clearInterval(c),c=null),o.stopScrolling(t)}var c=null,u={top:0,left:0},d=!1;e.event.bind(e.ownerDocument,"selectionchange",function(){t.contains(n())?d=!0:(d=!1,a())}),e.event.bind(window,"mouseup",function(){d&&(d=!1,a())}),e.event.bind(window,"keyup",function(){d&&(d=!1,a())}),e.event.bind(window,"mousemove",function(e){if(d){var n={x:e.pageX,y:e.pageY},l={left:t.offsetLeft,right:t.offsetLeft+t.offsetWidth,top:t.offsetTop,bottom:t.offsetTop+t.offsetHeight};n.xl.right-3?(u.left=5,o.startScrolling(t,"x")):u.left=0,n.yl.bottom-3?(n.y-l.bottom+3<5?u.top=5:u.top=20,o.startScrolling(t,"y")):u.top=0,0===u.top&&0===u.left?a():r()}})}var o=t("../../lib/helper"),l=t("../instances"),i=t("../update-geometry"),s=t("../update-scroll");e.exports=function(t){var e=l.get(t);r(t,e)}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],16:[function(t,e,n){"use strict";function r(t,e,n,r){function o(n,r){var o=t.scrollTop,l=t.scrollLeft,i=Math.abs(n),s=Math.abs(r);if(s>i){if(r<0&&o===e.contentHeight-e.containerHeight||r>0&&0===o)return!e.settings.swipePropagation}else if(i>s&&(n<0&&l===e.contentWidth-e.containerWidth||n>0&&0===l))return!e.settings.swipePropagation;return!0}function a(e,n){s(t,"top",t.scrollTop-n),s(t,"left",t.scrollLeft-e),i(t)}function c(){w=!0}function u(){w=!1}function d(t){return t.targetTouches?t.targetTouches[0]:t}function p(t){return!(!t.targetTouches||1!==t.targetTouches.length)||!(!t.pointerType||"mouse"===t.pointerType||t.pointerType===t.MSPOINTER_TYPE_MOUSE)}function f(t){if(p(t)){Y=!0;var e=d(t);g.pageX=e.pageX,g.pageY=e.pageY,v=(new Date).getTime(),null!==y&&clearInterval(y),t.stopPropagation()}}function h(t){if(!Y&&e.settings.swipePropagation&&f(t),!w&&Y&&p(t)){var n=d(t),r={pageX:n.pageX,pageY:n.pageY},l=r.pageX-g.pageX,i=r.pageY-g.pageY;a(l,i),g=r;var s=(new Date).getTime(),c=s-v;c>0&&(m.x=l/c,m.y=i/c,v=s),o(l,i)&&(t.stopPropagation(),t.preventDefault())}}function b(){!w&&Y&&(Y=!1,clearInterval(y),y=setInterval(function(){return l.get(t)&&(m.x||m.y)?Math.abs(m.x)<.01&&Math.abs(m.y)<.01?void clearInterval(y):(a(30*m.x,30*m.y),m.x*=.8,void(m.y*=.8)):void clearInterval(y)},10))}var g={},v=0,m={},y=null,w=!1,Y=!1;n?(e.event.bind(window,"touchstart",c),e.event.bind(window,"touchend",u),e.event.bind(t,"touchstart",f),e.event.bind(t,"touchmove",h),e.event.bind(t,"touchend",b)):r&&(window.PointerEvent?(e.event.bind(window,"pointerdown",c),e.event.bind(window,"pointerup",u),e.event.bind(t,"pointerdown",f),e.event.bind(t,"pointermove",h),e.event.bind(t,"pointerup",b)):window.MSPointerEvent&&(e.event.bind(window,"MSPointerDown",c),e.event.bind(window,"MSPointerUp",u),e.event.bind(t,"MSPointerDown",f),e.event.bind(t,"MSPointerMove",h),e.event.bind(t,"MSPointerUp",b)))}var o=t("../../lib/helper"),l=t("../instances"),i=t("../update-geometry"),s=t("../update-scroll");e.exports=function(t){if(o.env.supportsTouch||o.env.supportsIePointer){var e=l.get(t);r(t,e,o.env.supportsTouch,o.env.supportsIePointer)}}},{"../../lib/helper":6,"../instances":18,"../update-geometry":19,"../update-scroll":20}],17:[function(t,e,n){"use strict";var r=t("../lib/helper"),o=t("../lib/class"),l=t("./instances"),i=t("./update-geometry"),s={"click-rail":t("./handler/click-rail"),"drag-scrollbar":t("./handler/drag-scrollbar"),keyboard:t("./handler/keyboard"),wheel:t("./handler/mouse-wheel"),touch:t("./handler/touch"),selection:t("./handler/selection")},a=t("./handler/native-scroll");e.exports=function(t,e){e="object"==typeof e?e:{},o.add(t,"ps-container");var n=l.add(t);n.settings=r.extend(n.settings,e),o.add(t,"ps-theme-"+n.settings.theme),n.settings.handlers.forEach(function(e){s[e](t)}),a(t),i(t)}},{"../lib/class":2,"../lib/helper":6,"./handler/click-rail":10,"./handler/drag-scrollbar":11,"./handler/keyboard":12,"./handler/mouse-wheel":13,"./handler/native-scroll":14,"./handler/selection":15,"./handler/touch":16,"./instances":18,"./update-geometry":19}],18:[function(t,e,n){"use strict";function r(t){function e(){a.add(t,"ps-focus")}function n(){a.remove(t,"ps-focus")}var r=this;r.settings=s.clone(c),r.containerWidth=null,r.containerHeight=null,r.contentWidth=null,r.contentHeight=null,r.isRtl="rtl"===u.css(t,"direction"),r.isNegativeScroll=function(){var e=t.scrollLeft,n=null;return t.scrollLeft=-1,n=t.scrollLeft<0,t.scrollLeft=e,n}(),r.negativeScrollAdjustment=r.isNegativeScroll?t.scrollWidth-t.clientWidth:0,r.event=new d,r.ownerDocument=t.ownerDocument||document,r.scrollbarXRail=u.appendTo(u.e("div","ps-scrollbar-x-rail"),t),r.scrollbarX=u.appendTo(u.e("div","ps-scrollbar-x"),r.scrollbarXRail),r.scrollbarX.setAttribute("tabindex",0),r.event.bind(r.scrollbarX,"focus",e),r.event.bind(r.scrollbarX,"blur",n),r.scrollbarXActive=null,r.scrollbarXWidth=null,r.scrollbarXLeft=null,r.scrollbarXBottom=s.toInt(u.css(r.scrollbarXRail,"bottom")),r.isScrollbarXUsingBottom=r.scrollbarXBottom===r.scrollbarXBottom,r.scrollbarXTop=r.isScrollbarXUsingBottom?null:s.toInt(u.css(r.scrollbarXRail,"top")),r.railBorderXWidth=s.toInt(u.css(r.scrollbarXRail,"borderLeftWidth"))+s.toInt(u.css(r.scrollbarXRail,"borderRightWidth")),u.css(r.scrollbarXRail,"display","block"),r.railXMarginWidth=s.toInt(u.css(r.scrollbarXRail,"marginLeft"))+s.toInt(u.css(r.scrollbarXRail,"marginRight")),u.css(r.scrollbarXRail,"display",""),r.railXWidth=null,r.railXRatio=null,r.scrollbarYRail=u.appendTo(u.e("div","ps-scrollbar-y-rail"),t),r.scrollbarY=u.appendTo(u.e("div","ps-scrollbar-y"),r.scrollbarYRail),r.scrollbarY.setAttribute("tabindex",0),r.event.bind(r.scrollbarY,"focus",e),r.event.bind(r.scrollbarY,"blur",n),r.scrollbarYActive=null,r.scrollbarYHeight=null,r.scrollbarYTop=null,r.scrollbarYRight=s.toInt(u.css(r.scrollbarYRail,"right")),r.isScrollbarYUsingRight=r.scrollbarYRight===r.scrollbarYRight,r.scrollbarYLeft=r.isScrollbarYUsingRight?null:s.toInt(u.css(r.scrollbarYRail,"left")),r.scrollbarYOuterWidth=r.isRtl?s.outerWidth(r.scrollbarY):null,r.railBorderYWidth=s.toInt(u.css(r.scrollbarYRail,"borderTopWidth"))+s.toInt(u.css(r.scrollbarYRail,"borderBottomWidth")),u.css(r.scrollbarYRail,"display","block"),r.railYMarginHeight=s.toInt(u.css(r.scrollbarYRail,"marginTop"))+s.toInt(u.css(r.scrollbarYRail,"marginBottom")),u.css(r.scrollbarYRail,"display",""),r.railYHeight=null,r.railYRatio=null}function o(t){return t.getAttribute("data-ps-id")}function l(t,e){t.setAttribute("data-ps-id",e)}function i(t){t.removeAttribute("data-ps-id")}var s=t("../lib/helper"),a=t("../lib/class"),c=t("./default-setting"),u=t("../lib/dom"),d=t("../lib/event-manager"),p=t("../lib/guid"),f={};n.add=function(t){var e=p();return l(t,e),f[e]=new r(t),f[e]},n.remove=function(t){delete f[o(t)],i(t)},n.get=function(t){return f[o(t)]}},{"../lib/class":2,"../lib/dom":3,"../lib/event-manager":4,"../lib/guid":5,"../lib/helper":6,"./default-setting":8}],19:[function(t,e,n){"use strict";function r(t,e){return t.settings.minScrollbarLength&&(e=Math.max(e,t.settings.minScrollbarLength)),t.settings.maxScrollbarLength&&(e=Math.min(e,t.settings.maxScrollbarLength)),e}function o(t,e){var n={width:e.railXWidth};e.isRtl?n.left=e.negativeScrollAdjustment+t.scrollLeft+e.containerWidth-e.contentWidth:n.left=t.scrollLeft,e.isScrollbarXUsingBottom?n.bottom=e.scrollbarXBottom-t.scrollTop:n.top=e.scrollbarXTop+t.scrollTop,s.css(e.scrollbarXRail,n);var r={top:t.scrollTop,height:e.railYHeight};e.isScrollbarYUsingRight?e.isRtl?r.right=e.contentWidth-(e.negativeScrollAdjustment+t.scrollLeft)-e.scrollbarYRight-e.scrollbarYOuterWidth:r.right=e.scrollbarYRight-t.scrollLeft:e.isRtl?r.left=e.negativeScrollAdjustment+t.scrollLeft+2*e.containerWidth-e.contentWidth-e.scrollbarYLeft-e.scrollbarYOuterWidth:r.left=e.scrollbarYLeft+t.scrollLeft,s.css(e.scrollbarYRail,r),s.css(e.scrollbarX,{left:e.scrollbarXLeft,width:e.scrollbarXWidth-e.railBorderXWidth}),s.css(e.scrollbarY,{top:e.scrollbarYTop,height:e.scrollbarYHeight-e.railBorderYWidth})}var l=t("../lib/helper"),i=t("../lib/class"),s=t("../lib/dom"),a=t("./instances"),c=t("./update-scroll");e.exports=function(t){var e=a.get(t);e.containerWidth=t.clientWidth,e.containerHeight=t.clientHeight,e.contentWidth=t.scrollWidth,e.contentHeight=t.scrollHeight;var n;t.contains(e.scrollbarXRail)||(n=s.queryChildren(t,".ps-scrollbar-x-rail"),n.length>0&&n.forEach(function(t){s.remove(t)}),s.appendTo(e.scrollbarXRail,t)),t.contains(e.scrollbarYRail)||(n=s.queryChildren(t,".ps-scrollbar-y-rail"),n.length>0&&n.forEach(function(t){s.remove(t)}),s.appendTo(e.scrollbarYRail,t)),!e.settings.suppressScrollX&&e.containerWidth+e.settings.scrollXMarginOffset=e.railXWidth-e.scrollbarXWidth&&(e.scrollbarXLeft=e.railXWidth-e.scrollbarXWidth),e.scrollbarYTop>=e.railYHeight-e.scrollbarYHeight&&(e.scrollbarYTop=e.railYHeight-e.scrollbarYHeight),o(t,e),e.scrollbarXActive?i.add(t,"ps-active-x"):(i.remove(t,"ps-active-x"),e.scrollbarXWidth=0,e.scrollbarXLeft=0,c(t,"left",0)),e.scrollbarYActive?i.add(t,"ps-active-y"):(i.remove(t,"ps-active-y"),e.scrollbarYHeight=0,e.scrollbarYTop=0,c(t,"top",0))}},{"../lib/class":2,"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-scroll":20}],20:[function(t,e,n){"use strict";var r,o,l=t("./instances"),i=function(t){var e=document.createEvent("Event");return e.initEvent(t,!0,!0),e};e.exports=function(t,e,n){if("undefined"==typeof t)throw"You must provide an element to the update-scroll function";if("undefined"==typeof e)throw"You must provide an axis to the update-scroll function";if("undefined"==typeof n)throw"You must provide a value to the update-scroll function";"top"===e&&n<=0&&(t.scrollTop=n=0,t.dispatchEvent(i("ps-y-reach-start"))),"left"===e&&n<=0&&(t.scrollLeft=n=0,t.dispatchEvent(i("ps-x-reach-start")));var s=l.get(t);"top"===e&&n>=s.contentHeight-s.containerHeight&&(n=s.contentHeight-s.containerHeight,n-t.scrollTop<=1?n=t.scrollTop:t.scrollTop=n,t.dispatchEvent(i("ps-y-reach-end"))),"left"===e&&n>=s.contentWidth-s.containerWidth&&(n=s.contentWidth-s.containerWidth,n-t.scrollLeft<=1?n=t.scrollLeft:t.scrollLeft=n,t.dispatchEvent(i("ps-x-reach-end"))),r||(r=t.scrollTop),o||(o=t.scrollLeft),"top"===e&&nr&&t.dispatchEvent(i("ps-scroll-down")),"left"===e&&no&&t.dispatchEvent(i("ps-scroll-right")),"top"===e&&(t.scrollTop=r=n,t.dispatchEvent(i("ps-scroll-y"))),"left"===e&&(t.scrollLeft=o=n,t.dispatchEvent(i("ps-scroll-x")))}},{"./instances":18}],21:[function(t,e,n){"use strict";var r=t("../lib/helper"),o=t("../lib/dom"),l=t("./instances"),i=t("./update-geometry"),s=t("./update-scroll");e.exports=function(t){var e=l.get(t);e&&(e.negativeScrollAdjustment=e.isNegativeScroll?t.scrollWidth-t.clientWidth:0,o.css(e.scrollbarXRail,"display","block"),o.css(e.scrollbarYRail,"display","block"),e.railXMarginWidth=r.toInt(o.css(e.scrollbarXRail,"marginLeft"))+r.toInt(o.css(e.scrollbarXRail,"marginRight")),e.railYMarginHeight=r.toInt(o.css(e.scrollbarYRail,"marginTop"))+r.toInt(o.css(e.scrollbarYRail,"marginBottom")),o.css(e.scrollbarXRail,"display","none"),o.css(e.scrollbarYRail,"display","none"),i(t),s(t,"top",t.scrollTop),s(t,"left",t.scrollLeft),o.css(e.scrollbarXRail,"display",""),o.css(e.scrollbarYRail,"display",""))}},{"../lib/dom":3,"../lib/helper":6,"./instances":18,"./update-geometry":19,"./update-scroll":20}]},{},[1]); \ No newline at end of file diff --git a/view/js/perfect-scrollbar/src/css/main.scss b/view/js/perfect-scrollbar/src/css/main.scss new file mode 100644 index 0000000000..ae1f655ad9 --- /dev/null +++ b/view/js/perfect-scrollbar/src/css/main.scss @@ -0,0 +1,3 @@ +@import 'variables'; +@import 'mixins'; +@import 'themes'; diff --git a/view/js/perfect-scrollbar/src/css/mixins.scss b/view/js/perfect-scrollbar/src/css/mixins.scss new file mode 100644 index 0000000000..79d3d2c9d4 --- /dev/null +++ b/view/js/perfect-scrollbar/src/css/mixins.scss @@ -0,0 +1,128 @@ +@mixin scrollbar-rail-default($theme) { + display: none; + position: absolute; /* please don't change 'position' */ + opacity: map_get($theme, rail-default-opacity); + transition: background-color .2s linear, opacity .2s linear; +} + +@mixin scrollbar-rail-hover($theme) { + background-color: map_get($theme, rail-hover-bg); + opacity: map_get($theme, rail-hover-opacity); +} + +@mixin scrollbar-default($theme) { + position: absolute; /* please don't change 'position' */ + background-color: map_get($theme, bar-container-hover-bg); + border-radius: map_get($theme, border-radius); + transition: background-color .2s linear, height .2s linear, width .2s ease-in-out, + border-radius .2s ease-in-out; +} + +@mixin scrollbar-hover($theme) { + background-color: map_get($theme, bar-hover-bg); +} + +@mixin in-scrolling($theme) { + &.ps-in-scrolling { + &.ps-x > .ps-scrollbar-x-rail { + @include scrollbar-rail-hover($theme); + > .ps-scrollbar-x { + @include scrollbar-hover($theme); + height: map_get($theme, scrollbar-x-hover-height); + } + } + &.ps-y > .ps-scrollbar-y-rail { + @include scrollbar-rail-hover($theme); + > .ps-scrollbar-y { + @include scrollbar-hover($theme); + width: map_get($theme, scrollbar-y-hover-width); + } + } + } +} + +// Layout and theme mixin +@mixin ps-container($theme) { + -ms-touch-action: auto; + touch-action: auto; + overflow: hidden !important; + -ms-overflow-style: none; + + // Edge + @supports (-ms-overflow-style: none) { + overflow: auto !important; + } + // IE10+ + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + overflow: auto !important; + } + + &.ps-active-x > .ps-scrollbar-x-rail, + &.ps-active-y > .ps-scrollbar-y-rail { + display: block; + background-color: map_get($theme, bar-bg); + } + + @include in-scrolling($theme); + + > .ps-scrollbar-x-rail { + @include scrollbar-rail-default($theme); + bottom: map_get($theme, scrollbar-x-rail-bottom); /* there must be 'bottom' for ps-scrollbar-x-rail */ + height: map_get($theme, scrollbar-x-rail-height); + + > .ps-scrollbar-x { + @include scrollbar-default($theme); + bottom: map_get($theme, scrollbar-x-bottom); /* there must be 'bottom' for ps-scrollbar-x */ + height: map_get($theme, scrollbar-x-height); + } + &:hover, + &:active { + > .ps-scrollbar-x { + height: map_get($theme, scrollbar-x-hover-height); + } + } + } + + > .ps-scrollbar-y-rail { + @include scrollbar-rail-default($theme); + right: map_get($theme, scrollbar-y-rail-right); /* there must be 'right' for ps-scrollbar-y-rail */ + width: map_get($theme, scrollbar-y-rail-width); + + > .ps-scrollbar-y { + @include scrollbar-default($theme); + right: map_get($theme, scrollbar-y-right); /* there must be 'right' for ps-scrollbar-y */ + width: map_get($theme, scrollbar-y-width); + } + &:hover, + &:active { + > .ps-scrollbar-y { + width: map_get($theme, scrollbar-y-hover-width); + } + } + } + + &:hover { + @include in-scrolling($theme); + + > .ps-scrollbar-x-rail, + > .ps-scrollbar-y-rail { + opacity: map_get($theme, rail-container-hover-opacity); + } + + > .ps-scrollbar-x-rail:hover { + @include scrollbar-rail-hover($theme); + + > .ps-scrollbar-x { + @include scrollbar-hover($theme); + } + } + + > .ps-scrollbar-y-rail:hover { + @include scrollbar-rail-hover($theme); + + > .ps-scrollbar-y { + @include scrollbar-hover($theme); + } + } + } +} diff --git a/view/js/perfect-scrollbar/src/css/themes.scss b/view/js/perfect-scrollbar/src/css/themes.scss new file mode 100644 index 0000000000..bf7729a7da --- /dev/null +++ b/view/js/perfect-scrollbar/src/css/themes.scss @@ -0,0 +1,25 @@ +$ps-theme-default: ( + border-radius: $ps-border-radius, + rail-default-opacity: $ps-rail-default-opacity, + rail-container-hover-opacity: $ps-rail-container-hover-opacity, + rail-hover-opacity: $ps-rail-hover-opacity, + bar-bg: $ps-bar-bg, + bar-container-hover-bg: $ps-bar-container-hover-bg, + bar-hover-bg: $ps-bar-hover-bg, + rail-hover-bg: $ps-rail-hover-bg, + scrollbar-x-rail-bottom: $ps-scrollbar-x-rail-bottom, + scrollbar-x-rail-height: $ps-scrollbar-x-rail-height, + scrollbar-x-bottom: $ps-scrollbar-x-bottom, + scrollbar-x-height: $ps-scrollbar-x-height, + scrollbar-x-hover-height: $ps-scrollbar-x-hover-height, + scrollbar-y-rail-right: $ps-scrollbar-y-rail-right, + scrollbar-y-rail-width: $ps-scrollbar-y-rail-width, + scrollbar-y-right: $ps-scrollbar-y-right, + scrollbar-y-width: $ps-scrollbar-y-width, + scrollbar-y-hover-width: $ps-scrollbar-y-hover-width, +); + +// Default theme +.ps-container { + @include ps-container($ps-theme-default); +} diff --git a/view/js/perfect-scrollbar/src/css/variables.scss b/view/js/perfect-scrollbar/src/css/variables.scss new file mode 100644 index 0000000000..7454fb0b0f --- /dev/null +++ b/view/js/perfect-scrollbar/src/css/variables.scss @@ -0,0 +1,24 @@ +// Colors +$ps-border-radius: 6px !default; + +$ps-rail-default-opacity: 0 !default; +$ps-rail-container-hover-opacity: 0.6 !default; +$ps-rail-hover-opacity: 0.9 !default; + +$ps-bar-bg: transparent !default; +$ps-bar-container-hover-bg: #aaa !default; +$ps-bar-hover-bg: #999 !default; +$ps-rail-hover-bg: #eee !default; + +// Sizes +$ps-scrollbar-x-rail-bottom: 0px !default; +$ps-scrollbar-x-rail-height: 15px !default; +$ps-scrollbar-x-bottom: 2px !default; +$ps-scrollbar-x-height: 6px !default; +$ps-scrollbar-x-hover-height: 11px !default; + +$ps-scrollbar-y-rail-right: 0 !default; +$ps-scrollbar-y-rail-width: 15px !default; +$ps-scrollbar-y-right: 2px !default; +$ps-scrollbar-y-width: 6px !default; +$ps-scrollbar-y-hover-width: 11px !default; diff --git a/view/js/perfect-scrollbar/src/js/adaptor/global.js b/view/js/perfect-scrollbar/src/js/adaptor/global.js new file mode 100644 index 0000000000..0438e361e4 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/adaptor/global.js @@ -0,0 +1,14 @@ +'use strict'; + +var ps = require('../main'); + +if (typeof define === 'function' && define.amd) { + // AMD + define(ps); +} else { + // Add to a global object. + window.PerfectScrollbar = ps; + if (typeof window.Ps === 'undefined') { + window.Ps = ps; + } +} diff --git a/view/js/perfect-scrollbar/src/js/adaptor/jquery.js b/view/js/perfect-scrollbar/src/js/adaptor/jquery.js new file mode 100644 index 0000000000..ef55e093e0 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/adaptor/jquery.js @@ -0,0 +1,41 @@ +'use strict'; + +var ps = require('../main'); +var psInstances = require('../plugin/instances'); + +function mountJQuery(jQuery) { + jQuery.fn.perfectScrollbar = function (settingOrCommand) { + return this.each(function () { + if (typeof settingOrCommand === 'object' || + typeof settingOrCommand === 'undefined') { + // If it's an object or none, initialize. + var settings = settingOrCommand; + + if (!psInstances.get(this)) { + ps.initialize(this, settings); + } + } else { + // Unless, it may be a command. + var command = settingOrCommand; + + if (command === 'update') { + ps.update(this); + } else if (command === 'destroy') { + ps.destroy(this); + } + } + }); + }; +} + +if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], mountJQuery); +} else { + var jq = window.jQuery ? window.jQuery : window.$; + if (typeof jq !== 'undefined') { + mountJQuery(jq); + } +} + +module.exports = mountJQuery; diff --git a/view/js/perfect-scrollbar/src/js/lib/class.js b/view/js/perfect-scrollbar/src/js/lib/class.js new file mode 100644 index 0000000000..951b10bb2e --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/lib/class.js @@ -0,0 +1,42 @@ +'use strict'; + +function oldAdd(element, className) { + var classes = element.className.split(' '); + if (classes.indexOf(className) < 0) { + classes.push(className); + } + element.className = classes.join(' '); +} + +function oldRemove(element, className) { + var classes = element.className.split(' '); + var idx = classes.indexOf(className); + if (idx >= 0) { + classes.splice(idx, 1); + } + element.className = classes.join(' '); +} + +exports.add = function (element, className) { + if (element.classList) { + element.classList.add(className); + } else { + oldAdd(element, className); + } +}; + +exports.remove = function (element, className) { + if (element.classList) { + element.classList.remove(className); + } else { + oldRemove(element, className); + } +}; + +exports.list = function (element) { + if (element.classList) { + return Array.prototype.slice.apply(element.classList); + } else { + return element.className.split(' '); + } +}; diff --git a/view/js/perfect-scrollbar/src/js/lib/dom.js b/view/js/perfect-scrollbar/src/js/lib/dom.js new file mode 100644 index 0000000000..b929a17ed7 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/lib/dom.js @@ -0,0 +1,84 @@ +'use strict'; + +var DOM = {}; + +DOM.e = function (tagName, className) { + var element = document.createElement(tagName); + element.className = className; + return element; +}; + +DOM.appendTo = function (child, parent) { + parent.appendChild(child); + return child; +}; + +function cssGet(element, styleName) { + return window.getComputedStyle(element)[styleName]; +} + +function cssSet(element, styleName, styleValue) { + if (typeof styleValue === 'number') { + styleValue = styleValue.toString() + 'px'; + } + element.style[styleName] = styleValue; + return element; +} + +function cssMultiSet(element, obj) { + for (var key in obj) { + var val = obj[key]; + if (typeof val === 'number') { + val = val.toString() + 'px'; + } + element.style[key] = val; + } + return element; +} + +DOM.css = function (element, styleNameOrObject, styleValue) { + if (typeof styleNameOrObject === 'object') { + // multiple set with object + return cssMultiSet(element, styleNameOrObject); + } else { + if (typeof styleValue === 'undefined') { + return cssGet(element, styleNameOrObject); + } else { + return cssSet(element, styleNameOrObject, styleValue); + } + } +}; + +DOM.matches = function (element, query) { + if (typeof element.matches !== 'undefined') { + return element.matches(query); + } else { + if (typeof element.matchesSelector !== 'undefined') { + return element.matchesSelector(query); + } else if (typeof element.webkitMatchesSelector !== 'undefined') { + return element.webkitMatchesSelector(query); + } else if (typeof element.mozMatchesSelector !== 'undefined') { + return element.mozMatchesSelector(query); + } else if (typeof element.msMatchesSelector !== 'undefined') { + return element.msMatchesSelector(query); + } + } +}; + +DOM.remove = function (element) { + if (typeof element.remove !== 'undefined') { + element.remove(); + } else { + if (element.parentNode) { + element.parentNode.removeChild(element); + } + } +}; + +DOM.queryChildren = function (element, selector) { + return Array.prototype.filter.call(element.childNodes, function (child) { + return DOM.matches(child, selector); + }); +}; + +module.exports = DOM; diff --git a/view/js/perfect-scrollbar/src/js/lib/event-manager.js b/view/js/perfect-scrollbar/src/js/lib/event-manager.js new file mode 100644 index 0000000000..d148ad8feb --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/lib/event-manager.js @@ -0,0 +1,71 @@ +'use strict'; + +var EventElement = function (element) { + this.element = element; + this.events = {}; +}; + +EventElement.prototype.bind = function (eventName, handler) { + if (typeof this.events[eventName] === 'undefined') { + this.events[eventName] = []; + } + this.events[eventName].push(handler); + this.element.addEventListener(eventName, handler, false); +}; + +EventElement.prototype.unbind = function (eventName, handler) { + var isHandlerProvided = (typeof handler !== 'undefined'); + this.events[eventName] = this.events[eventName].filter(function (hdlr) { + if (isHandlerProvided && hdlr !== handler) { + return true; + } + this.element.removeEventListener(eventName, hdlr, false); + return false; + }, this); +}; + +EventElement.prototype.unbindAll = function () { + for (var name in this.events) { + this.unbind(name); + } +}; + +var EventManager = function () { + this.eventElements = []; +}; + +EventManager.prototype.eventElement = function (element) { + var ee = this.eventElements.filter(function (eventElement) { + return eventElement.element === element; + })[0]; + if (typeof ee === 'undefined') { + ee = new EventElement(element); + this.eventElements.push(ee); + } + return ee; +}; + +EventManager.prototype.bind = function (element, eventName, handler) { + this.eventElement(element).bind(eventName, handler); +}; + +EventManager.prototype.unbind = function (element, eventName, handler) { + this.eventElement(element).unbind(eventName, handler); +}; + +EventManager.prototype.unbindAll = function () { + for (var i = 0; i < this.eventElements.length; i++) { + this.eventElements[i].unbindAll(); + } +}; + +EventManager.prototype.once = function (element, eventName, handler) { + var ee = this.eventElement(element); + var onceHandler = function (e) { + ee.unbind(eventName, onceHandler); + handler(e); + }; + ee.bind(eventName, onceHandler); +}; + +module.exports = EventManager; diff --git a/view/js/perfect-scrollbar/src/js/lib/guid.js b/view/js/perfect-scrollbar/src/js/lib/guid.js new file mode 100644 index 0000000000..84c7237eb1 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/lib/guid.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = (function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return function () { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + }; +})(); diff --git a/view/js/perfect-scrollbar/src/js/lib/helper.js b/view/js/perfect-scrollbar/src/js/lib/helper.js new file mode 100644 index 0000000000..a72f2e59f7 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/lib/helper.js @@ -0,0 +1,83 @@ +'use strict'; + +var cls = require('./class'); +var dom = require('./dom'); + +var toInt = exports.toInt = function (x) { + return parseInt(x, 10) || 0; +}; + +var clone = exports.clone = function (obj) { + if (!obj) { + return null; + } else if (obj.constructor === Array) { + return obj.map(clone); + } else if (typeof obj === 'object') { + var result = {}; + for (var key in obj) { + result[key] = clone(obj[key]); + } + return result; + } else { + return obj; + } +}; + +exports.extend = function (original, source) { + var result = clone(original); + for (var key in source) { + result[key] = clone(source[key]); + } + return result; +}; + +exports.isEditable = function (el) { + return dom.matches(el, "input,[contenteditable]") || + dom.matches(el, "select,[contenteditable]") || + dom.matches(el, "textarea,[contenteditable]") || + dom.matches(el, "button,[contenteditable]"); +}; + +exports.removePsClasses = function (element) { + var clsList = cls.list(element); + for (var i = 0; i < clsList.length; i++) { + var className = clsList[i]; + if (className.indexOf('ps-') === 0) { + cls.remove(element, className); + } + } +}; + +exports.outerWidth = function (element) { + return toInt(dom.css(element, 'width')) + + toInt(dom.css(element, 'paddingLeft')) + + toInt(dom.css(element, 'paddingRight')) + + toInt(dom.css(element, 'borderLeftWidth')) + + toInt(dom.css(element, 'borderRightWidth')); +}; + +exports.startScrolling = function (element, axis) { + cls.add(element, 'ps-in-scrolling'); + if (typeof axis !== 'undefined') { + cls.add(element, 'ps-' + axis); + } else { + cls.add(element, 'ps-x'); + cls.add(element, 'ps-y'); + } +}; + +exports.stopScrolling = function (element, axis) { + cls.remove(element, 'ps-in-scrolling'); + if (typeof axis !== 'undefined') { + cls.remove(element, 'ps-' + axis); + } else { + cls.remove(element, 'ps-x'); + cls.remove(element, 'ps-y'); + } +}; + +exports.env = { + isWebKit: 'WebkitAppearance' in document.documentElement.style, + supportsTouch: (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch), + supportsIePointer: window.navigator.msMaxTouchPoints !== null +}; diff --git a/view/js/perfect-scrollbar/src/js/main.js b/view/js/perfect-scrollbar/src/js/main.js new file mode 100644 index 0000000000..06b1c2be79 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/main.js @@ -0,0 +1,11 @@ +'use strict'; + +var destroy = require('./plugin/destroy'); +var initialize = require('./plugin/initialize'); +var update = require('./plugin/update'); + +module.exports = { + initialize: initialize, + update: update, + destroy: destroy +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/default-setting.js b/view/js/perfect-scrollbar/src/js/plugin/default-setting.js new file mode 100644 index 0000000000..b3f2ddd8bf --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/default-setting.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = { + handlers: ['click-rail', 'drag-scrollbar', 'keyboard', 'wheel', 'touch'], + maxScrollbarLength: null, + minScrollbarLength: null, + scrollXMarginOffset: 0, + scrollYMarginOffset: 0, + suppressScrollX: false, + suppressScrollY: false, + swipePropagation: true, + useBothWheelAxes: false, + wheelPropagation: false, + wheelSpeed: 1, + theme: 'default' +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/destroy.js b/view/js/perfect-scrollbar/src/js/plugin/destroy.js new file mode 100644 index 0000000000..97a83e0bc2 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/destroy.js @@ -0,0 +1,22 @@ +'use strict'; + +var _ = require('../lib/helper'); +var dom = require('../lib/dom'); +var instances = require('./instances'); + +module.exports = function (element) { + var i = instances.get(element); + + if (!i) { + return; + } + + i.event.unbindAll(); + dom.remove(i.scrollbarX); + dom.remove(i.scrollbarY); + dom.remove(i.scrollbarXRail); + dom.remove(i.scrollbarYRail); + _.removePsClasses(element); + + instances.remove(element); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/handler/click-rail.js b/view/js/perfect-scrollbar/src/js/plugin/handler/click-rail.js new file mode 100644 index 0000000000..bbd15218ed --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/handler/click-rail.js @@ -0,0 +1,39 @@ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindClickRailHandler(element, i) { + function pageOffset(el) { + return el.getBoundingClientRect(); + } + var stopPropagation = function (e) { e.stopPropagation(); }; + + i.event.bind(i.scrollbarY, 'click', stopPropagation); + i.event.bind(i.scrollbarYRail, 'click', function (e) { + var positionTop = e.pageY - window.pageYOffset - pageOffset(i.scrollbarYRail).top; + var direction = positionTop > i.scrollbarYTop ? 1 : -1; + + updateScroll(element, 'top', element.scrollTop + direction * i.containerHeight); + updateGeometry(element); + + e.stopPropagation(); + }); + + i.event.bind(i.scrollbarX, 'click', stopPropagation); + i.event.bind(i.scrollbarXRail, 'click', function (e) { + var positionLeft = e.pageX - window.pageXOffset - pageOffset(i.scrollbarXRail).left; + var direction = positionLeft > i.scrollbarXLeft ? 1 : -1; + + updateScroll(element, 'left', element.scrollLeft + direction * i.containerWidth); + updateGeometry(element); + + e.stopPropagation(); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindClickRailHandler(element, i); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/handler/drag-scrollbar.js b/view/js/perfect-scrollbar/src/js/plugin/handler/drag-scrollbar.js new file mode 100644 index 0000000000..fc99d00836 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/handler/drag-scrollbar.js @@ -0,0 +1,103 @@ +'use strict'; + +var _ = require('../../lib/helper'); +var dom = require('../../lib/dom'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindMouseScrollXHandler(element, i) { + var currentLeft = null; + var currentPageX = null; + + function updateScrollLeft(deltaX) { + var newLeft = currentLeft + (deltaX * i.railXRatio); + var maxLeft = Math.max(0, i.scrollbarXRail.getBoundingClientRect().left) + (i.railXRatio * (i.railXWidth - i.scrollbarXWidth)); + + if (newLeft < 0) { + i.scrollbarXLeft = 0; + } else if (newLeft > maxLeft) { + i.scrollbarXLeft = maxLeft; + } else { + i.scrollbarXLeft = newLeft; + } + + var scrollLeft = _.toInt(i.scrollbarXLeft * (i.contentWidth - i.containerWidth) / (i.containerWidth - (i.railXRatio * i.scrollbarXWidth))) - i.negativeScrollAdjustment; + updateScroll(element, 'left', scrollLeft); + } + + var mouseMoveHandler = function (e) { + updateScrollLeft(e.pageX - currentPageX); + updateGeometry(element); + e.stopPropagation(); + e.preventDefault(); + }; + + var mouseUpHandler = function () { + _.stopScrolling(element, 'x'); + i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler); + }; + + i.event.bind(i.scrollbarX, 'mousedown', function (e) { + currentPageX = e.pageX; + currentLeft = _.toInt(dom.css(i.scrollbarX, 'left')) * i.railXRatio; + _.startScrolling(element, 'x'); + + i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler); + i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler); + + e.stopPropagation(); + e.preventDefault(); + }); +} + +function bindMouseScrollYHandler(element, i) { + var currentTop = null; + var currentPageY = null; + + function updateScrollTop(deltaY) { + var newTop = currentTop + (deltaY * i.railYRatio); + var maxTop = Math.max(0, i.scrollbarYRail.getBoundingClientRect().top) + (i.railYRatio * (i.railYHeight - i.scrollbarYHeight)); + + if (newTop < 0) { + i.scrollbarYTop = 0; + } else if (newTop > maxTop) { + i.scrollbarYTop = maxTop; + } else { + i.scrollbarYTop = newTop; + } + + var scrollTop = _.toInt(i.scrollbarYTop * (i.contentHeight - i.containerHeight) / (i.containerHeight - (i.railYRatio * i.scrollbarYHeight))); + updateScroll(element, 'top', scrollTop); + } + + var mouseMoveHandler = function (e) { + updateScrollTop(e.pageY - currentPageY); + updateGeometry(element); + e.stopPropagation(); + e.preventDefault(); + }; + + var mouseUpHandler = function () { + _.stopScrolling(element, 'y'); + i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler); + }; + + i.event.bind(i.scrollbarY, 'mousedown', function (e) { + currentPageY = e.pageY; + currentTop = _.toInt(dom.css(i.scrollbarY, 'top')) * i.railYRatio; + _.startScrolling(element, 'y'); + + i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler); + i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler); + + e.stopPropagation(); + e.preventDefault(); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindMouseScrollXHandler(element, i); + bindMouseScrollYHandler(element, i); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/handler/keyboard.js b/view/js/perfect-scrollbar/src/js/plugin/handler/keyboard.js new file mode 100644 index 0000000000..b23a3bdb1d --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/handler/keyboard.js @@ -0,0 +1,154 @@ +'use strict'; + +var _ = require('../../lib/helper'); +var dom = require('../../lib/dom'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindKeyboardHandler(element, i) { + var hovered = false; + i.event.bind(element, 'mouseenter', function () { + hovered = true; + }); + i.event.bind(element, 'mouseleave', function () { + hovered = false; + }); + + var shouldPrevent = false; + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + if (deltaX === 0) { + if (!i.scrollbarYActive) { + return false; + } + if ((scrollTop === 0 && deltaY > 0) || (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)) { + return !i.settings.wheelPropagation; + } + } + + var scrollLeft = element.scrollLeft; + if (deltaY === 0) { + if (!i.scrollbarXActive) { + return false; + } + if ((scrollLeft === 0 && deltaX < 0) || (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)) { + return !i.settings.wheelPropagation; + } + } + return true; + } + + i.event.bind(i.ownerDocument, 'keydown', function (e) { + if ((e.isDefaultPrevented && e.isDefaultPrevented()) || e.defaultPrevented) { + return; + } + + var focused = dom.matches(i.scrollbarX, ':focus') || + dom.matches(i.scrollbarY, ':focus'); + + if (!hovered && !focused) { + return; + } + + var activeElement = document.activeElement ? document.activeElement : i.ownerDocument.activeElement; + if (activeElement) { + if (activeElement.tagName === 'IFRAME') { + activeElement = activeElement.contentDocument.activeElement; + } else { + // go deeper if element is a webcomponent + while (activeElement.shadowRoot) { + activeElement = activeElement.shadowRoot.activeElement; + } + } + if (_.isEditable(activeElement)) { + return; + } + } + + var deltaX = 0; + var deltaY = 0; + + switch (e.which) { + case 37: // left + if (e.metaKey) { + deltaX = -i.contentWidth; + } else if (e.altKey) { + deltaX = -i.containerWidth; + } else { + deltaX = -30; + } + break; + case 38: // up + if (e.metaKey) { + deltaY = i.contentHeight; + } else if (e.altKey) { + deltaY = i.containerHeight; + } else { + deltaY = 30; + } + break; + case 39: // right + if (e.metaKey) { + deltaX = i.contentWidth; + } else if (e.altKey) { + deltaX = i.containerWidth; + } else { + deltaX = 30; + } + break; + case 40: // down + if (e.metaKey) { + deltaY = -i.contentHeight; + } else if (e.altKey) { + deltaY = -i.containerHeight; + } else { + deltaY = -30; + } + break; + case 33: // page up + deltaY = 90; + break; + case 32: // space bar + if (e.shiftKey) { + deltaY = 90; + } else { + deltaY = -90; + } + break; + case 34: // page down + deltaY = -90; + break; + case 35: // end + if (e.ctrlKey) { + deltaY = -i.contentHeight; + } else { + deltaY = -i.containerHeight; + } + break; + case 36: // home + if (e.ctrlKey) { + deltaY = element.scrollTop; + } else { + deltaY = i.containerHeight; + } + break; + default: + return; + } + + updateScroll(element, 'top', element.scrollTop - deltaY); + updateScroll(element, 'left', element.scrollLeft + deltaX); + updateGeometry(element); + + shouldPrevent = shouldPreventDefault(deltaX, deltaY); + if (shouldPrevent) { + e.preventDefault(); + } + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindKeyboardHandler(element, i); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/handler/mouse-wheel.js b/view/js/perfect-scrollbar/src/js/plugin/handler/mouse-wheel.js new file mode 100644 index 0000000000..9e08f303b0 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/handler/mouse-wheel.js @@ -0,0 +1,141 @@ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindMouseWheelHandler(element, i) { + var shouldPrevent = false; + + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + if (deltaX === 0) { + if (!i.scrollbarYActive) { + return false; + } + if ((scrollTop === 0 && deltaY > 0) || (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)) { + return !i.settings.wheelPropagation; + } + } + + var scrollLeft = element.scrollLeft; + if (deltaY === 0) { + if (!i.scrollbarXActive) { + return false; + } + if ((scrollLeft === 0 && deltaX < 0) || (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)) { + return !i.settings.wheelPropagation; + } + } + return true; + } + + function getDeltaFromEvent(e) { + var deltaX = e.deltaX; + var deltaY = -1 * e.deltaY; + + if (typeof deltaX === "undefined" || typeof deltaY === "undefined") { + // OS X Safari + deltaX = -1 * e.wheelDeltaX / 6; + deltaY = e.wheelDeltaY / 6; + } + + if (e.deltaMode && e.deltaMode === 1) { + // Firefox in deltaMode 1: Line scrolling + deltaX *= 10; + deltaY *= 10; + } + + if (deltaX !== deltaX && deltaY !== deltaY/* NaN checks */) { + // IE in some mouse drivers + deltaX = 0; + deltaY = e.wheelDelta; + } + + if (e.shiftKey) { + // reverse axis with shift key + return [-deltaY, -deltaX]; + } + return [deltaX, deltaY]; + } + + function shouldBeConsumedByChild(deltaX, deltaY) { + var child = element.querySelector('textarea:hover, select[multiple]:hover, .ps-child:hover'); + if (child) { + if (!window.getComputedStyle(child).overflow.match(/(scroll|auto)/)) { + // if not scrollable + return false; + } + + var maxScrollTop = child.scrollHeight - child.clientHeight; + if (maxScrollTop > 0) { + if (!(child.scrollTop === 0 && deltaY > 0) && !(child.scrollTop === maxScrollTop && deltaY < 0)) { + return true; + } + } + var maxScrollLeft = child.scrollLeft - child.clientWidth; + if (maxScrollLeft > 0) { + if (!(child.scrollLeft === 0 && deltaX < 0) && !(child.scrollLeft === maxScrollLeft && deltaX > 0)) { + return true; + } + } + } + return false; + } + + function mousewheelHandler(e) { + var delta = getDeltaFromEvent(e); + + var deltaX = delta[0]; + var deltaY = delta[1]; + + if (shouldBeConsumedByChild(deltaX, deltaY)) { + return; + } + + shouldPrevent = false; + if (!i.settings.useBothWheelAxes) { + // deltaX will only be used for horizontal scrolling and deltaY will + // only be used for vertical scrolling - this is the default + updateScroll(element, 'top', element.scrollTop - (deltaY * i.settings.wheelSpeed)); + updateScroll(element, 'left', element.scrollLeft + (deltaX * i.settings.wheelSpeed)); + } else if (i.scrollbarYActive && !i.scrollbarXActive) { + // only vertical scrollbar is active and useBothWheelAxes option is + // active, so let's scroll vertical bar using both mouse wheel axes + if (deltaY) { + updateScroll(element, 'top', element.scrollTop - (deltaY * i.settings.wheelSpeed)); + } else { + updateScroll(element, 'top', element.scrollTop + (deltaX * i.settings.wheelSpeed)); + } + shouldPrevent = true; + } else if (i.scrollbarXActive && !i.scrollbarYActive) { + // useBothWheelAxes and only horizontal bar is active, so use both + // wheel axes for horizontal bar + if (deltaX) { + updateScroll(element, 'left', element.scrollLeft + (deltaX * i.settings.wheelSpeed)); + } else { + updateScroll(element, 'left', element.scrollLeft - (deltaY * i.settings.wheelSpeed)); + } + shouldPrevent = true; + } + + updateGeometry(element); + + shouldPrevent = (shouldPrevent || shouldPreventDefault(deltaX, deltaY)); + if (shouldPrevent) { + e.stopPropagation(); + e.preventDefault(); + } + } + + if (typeof window.onwheel !== "undefined") { + i.event.bind(element, 'wheel', mousewheelHandler); + } else if (typeof window.onmousewheel !== "undefined") { + i.event.bind(element, 'mousewheel', mousewheelHandler); + } +} + +module.exports = function (element) { + var i = instances.get(element); + bindMouseWheelHandler(element, i); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/handler/native-scroll.js b/view/js/perfect-scrollbar/src/js/plugin/handler/native-scroll.js new file mode 100644 index 0000000000..8664b23a36 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/handler/native-scroll.js @@ -0,0 +1,15 @@ +'use strict'; + +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); + +function bindNativeScrollHandler(element, i) { + i.event.bind(element, 'scroll', function () { + updateGeometry(element); + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindNativeScrollHandler(element, i); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/handler/selection.js b/view/js/perfect-scrollbar/src/js/plugin/handler/selection.js new file mode 100644 index 0000000000..705420f479 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/handler/selection.js @@ -0,0 +1,115 @@ +'use strict'; + +var _ = require('../../lib/helper'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindSelectionHandler(element, i) { + function getRangeNode() { + var selection = window.getSelection ? window.getSelection() : + document.getSelection ? document.getSelection() : ''; + if (selection.toString().length === 0) { + return null; + } else { + return selection.getRangeAt(0).commonAncestorContainer; + } + } + + var scrollingLoop = null; + var scrollDiff = {top: 0, left: 0}; + function startScrolling() { + if (!scrollingLoop) { + scrollingLoop = setInterval(function () { + if (!instances.get(element)) { + clearInterval(scrollingLoop); + return; + } + + updateScroll(element, 'top', element.scrollTop + scrollDiff.top); + updateScroll(element, 'left', element.scrollLeft + scrollDiff.left); + updateGeometry(element); + }, 50); // every .1 sec + } + } + function stopScrolling() { + if (scrollingLoop) { + clearInterval(scrollingLoop); + scrollingLoop = null; + } + _.stopScrolling(element); + } + + var isSelected = false; + i.event.bind(i.ownerDocument, 'selectionchange', function () { + if (element.contains(getRangeNode())) { + isSelected = true; + } else { + isSelected = false; + stopScrolling(); + } + }); + i.event.bind(window, 'mouseup', function () { + if (isSelected) { + isSelected = false; + stopScrolling(); + } + }); + i.event.bind(window, 'keyup', function () { + if (isSelected) { + isSelected = false; + stopScrolling(); + } + }); + + i.event.bind(window, 'mousemove', function (e) { + if (isSelected) { + var mousePosition = {x: e.pageX, y: e.pageY}; + var containerGeometry = { + left: element.offsetLeft, + right: element.offsetLeft + element.offsetWidth, + top: element.offsetTop, + bottom: element.offsetTop + element.offsetHeight + }; + + if (mousePosition.x < containerGeometry.left + 3) { + scrollDiff.left = -5; + _.startScrolling(element, 'x'); + } else if (mousePosition.x > containerGeometry.right - 3) { + scrollDiff.left = 5; + _.startScrolling(element, 'x'); + } else { + scrollDiff.left = 0; + } + + if (mousePosition.y < containerGeometry.top + 3) { + if (containerGeometry.top + 3 - mousePosition.y < 5) { + scrollDiff.top = -5; + } else { + scrollDiff.top = -20; + } + _.startScrolling(element, 'y'); + } else if (mousePosition.y > containerGeometry.bottom - 3) { + if (mousePosition.y - containerGeometry.bottom + 3 < 5) { + scrollDiff.top = 5; + } else { + scrollDiff.top = 20; + } + _.startScrolling(element, 'y'); + } else { + scrollDiff.top = 0; + } + + if (scrollDiff.top === 0 && scrollDiff.left === 0) { + stopScrolling(); + } else { + startScrolling(); + } + } + }); +} + +module.exports = function (element) { + var i = instances.get(element); + bindSelectionHandler(element, i); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/handler/touch.js b/view/js/perfect-scrollbar/src/js/plugin/handler/touch.js new file mode 100644 index 0000000000..ba5d13307d --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/handler/touch.js @@ -0,0 +1,179 @@ +'use strict'; + +var _ = require('../../lib/helper'); +var instances = require('../instances'); +var updateGeometry = require('../update-geometry'); +var updateScroll = require('../update-scroll'); + +function bindTouchHandler(element, i, supportsTouch, supportsIePointer) { + function shouldPreventDefault(deltaX, deltaY) { + var scrollTop = element.scrollTop; + var scrollLeft = element.scrollLeft; + var magnitudeX = Math.abs(deltaX); + var magnitudeY = Math.abs(deltaY); + + if (magnitudeY > magnitudeX) { + // user is perhaps trying to swipe up/down the page + + if (((deltaY < 0) && (scrollTop === i.contentHeight - i.containerHeight)) || + ((deltaY > 0) && (scrollTop === 0))) { + return !i.settings.swipePropagation; + } + } else if (magnitudeX > magnitudeY) { + // user is perhaps trying to swipe left/right across the page + + if (((deltaX < 0) && (scrollLeft === i.contentWidth - i.containerWidth)) || + ((deltaX > 0) && (scrollLeft === 0))) { + return !i.settings.swipePropagation; + } + } + + return true; + } + + function applyTouchMove(differenceX, differenceY) { + updateScroll(element, 'top', element.scrollTop - differenceY); + updateScroll(element, 'left', element.scrollLeft - differenceX); + + updateGeometry(element); + } + + var startOffset = {}; + var startTime = 0; + var speed = {}; + var easingLoop = null; + var inGlobalTouch = false; + var inLocalTouch = false; + + function globalTouchStart() { + inGlobalTouch = true; + } + function globalTouchEnd() { + inGlobalTouch = false; + } + + function getTouch(e) { + if (e.targetTouches) { + return e.targetTouches[0]; + } else { + // Maybe IE pointer + return e; + } + } + function shouldHandle(e) { + if (e.targetTouches && e.targetTouches.length === 1) { + return true; + } + if (e.pointerType && e.pointerType !== 'mouse' && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) { + return true; + } + return false; + } + function touchStart(e) { + if (shouldHandle(e)) { + inLocalTouch = true; + + var touch = getTouch(e); + + startOffset.pageX = touch.pageX; + startOffset.pageY = touch.pageY; + + startTime = (new Date()).getTime(); + + if (easingLoop !== null) { + clearInterval(easingLoop); + } + + e.stopPropagation(); + } + } + function touchMove(e) { + if (!inLocalTouch && i.settings.swipePropagation) { + touchStart(e); + } + if (!inGlobalTouch && inLocalTouch && shouldHandle(e)) { + var touch = getTouch(e); + + var currentOffset = {pageX: touch.pageX, pageY: touch.pageY}; + + var differenceX = currentOffset.pageX - startOffset.pageX; + var differenceY = currentOffset.pageY - startOffset.pageY; + + applyTouchMove(differenceX, differenceY); + startOffset = currentOffset; + + var currentTime = (new Date()).getTime(); + + var timeGap = currentTime - startTime; + if (timeGap > 0) { + speed.x = differenceX / timeGap; + speed.y = differenceY / timeGap; + startTime = currentTime; + } + + if (shouldPreventDefault(differenceX, differenceY)) { + e.stopPropagation(); + e.preventDefault(); + } + } + } + function touchEnd() { + if (!inGlobalTouch && inLocalTouch) { + inLocalTouch = false; + + clearInterval(easingLoop); + easingLoop = setInterval(function () { + if (!instances.get(element)) { + clearInterval(easingLoop); + return; + } + + if (!speed.x && !speed.y) { + clearInterval(easingLoop); + return; + } + + if (Math.abs(speed.x) < 0.01 && Math.abs(speed.y) < 0.01) { + clearInterval(easingLoop); + return; + } + + applyTouchMove(speed.x * 30, speed.y * 30); + + speed.x *= 0.8; + speed.y *= 0.8; + }, 10); + } + } + + if (supportsTouch) { + i.event.bind(window, 'touchstart', globalTouchStart); + i.event.bind(window, 'touchend', globalTouchEnd); + i.event.bind(element, 'touchstart', touchStart); + i.event.bind(element, 'touchmove', touchMove); + i.event.bind(element, 'touchend', touchEnd); + } else if (supportsIePointer) { + if (window.PointerEvent) { + i.event.bind(window, 'pointerdown', globalTouchStart); + i.event.bind(window, 'pointerup', globalTouchEnd); + i.event.bind(element, 'pointerdown', touchStart); + i.event.bind(element, 'pointermove', touchMove); + i.event.bind(element, 'pointerup', touchEnd); + } else if (window.MSPointerEvent) { + i.event.bind(window, 'MSPointerDown', globalTouchStart); + i.event.bind(window, 'MSPointerUp', globalTouchEnd); + i.event.bind(element, 'MSPointerDown', touchStart); + i.event.bind(element, 'MSPointerMove', touchMove); + i.event.bind(element, 'MSPointerUp', touchEnd); + } + } +} + +module.exports = function (element) { + if (!_.env.supportsTouch && !_.env.supportsIePointer) { + return; + } + + var i = instances.get(element); + bindTouchHandler(element, i, _.env.supportsTouch, _.env.supportsIePointer); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/initialize.js b/view/js/perfect-scrollbar/src/js/plugin/initialize.js new file mode 100644 index 0000000000..05918a5a2a --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/initialize.js @@ -0,0 +1,37 @@ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var instances = require('./instances'); +var updateGeometry = require('./update-geometry'); + +// Handlers +var handlers = { + 'click-rail': require('./handler/click-rail'), + 'drag-scrollbar': require('./handler/drag-scrollbar'), + 'keyboard': require('./handler/keyboard'), + 'wheel': require('./handler/mouse-wheel'), + 'touch': require('./handler/touch'), + 'selection': require('./handler/selection') +}; +var nativeScrollHandler = require('./handler/native-scroll'); + +module.exports = function (element, userSettings) { + userSettings = typeof userSettings === 'object' ? userSettings : {}; + + cls.add(element, 'ps-container'); + + // Create a plugin instance. + var i = instances.add(element); + + i.settings = _.extend(i.settings, userSettings); + cls.add(element, 'ps-theme-' + i.settings.theme); + + i.settings.handlers.forEach(function (handlerName) { + handlers[handlerName](element); + }); + + nativeScrollHandler(element); + + updateGeometry(element); +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/instances.js b/view/js/perfect-scrollbar/src/js/plugin/instances.js new file mode 100644 index 0000000000..d4d74f377e --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/instances.js @@ -0,0 +1,107 @@ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var defaultSettings = require('./default-setting'); +var dom = require('../lib/dom'); +var EventManager = require('../lib/event-manager'); +var guid = require('../lib/guid'); + +var instances = {}; + +function Instance(element) { + var i = this; + + i.settings = _.clone(defaultSettings); + i.containerWidth = null; + i.containerHeight = null; + i.contentWidth = null; + i.contentHeight = null; + + i.isRtl = dom.css(element, 'direction') === "rtl"; + i.isNegativeScroll = (function () { + var originalScrollLeft = element.scrollLeft; + var result = null; + element.scrollLeft = -1; + result = element.scrollLeft < 0; + element.scrollLeft = originalScrollLeft; + return result; + })(); + i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0; + i.event = new EventManager(); + i.ownerDocument = element.ownerDocument || document; + + function focus() { + cls.add(element, 'ps-focus'); + } + + function blur() { + cls.remove(element, 'ps-focus'); + } + + i.scrollbarXRail = dom.appendTo(dom.e('div', 'ps-scrollbar-x-rail'), element); + i.scrollbarX = dom.appendTo(dom.e('div', 'ps-scrollbar-x'), i.scrollbarXRail); + i.scrollbarX.setAttribute('tabindex', 0); + i.event.bind(i.scrollbarX, 'focus', focus); + i.event.bind(i.scrollbarX, 'blur', blur); + i.scrollbarXActive = null; + i.scrollbarXWidth = null; + i.scrollbarXLeft = null; + i.scrollbarXBottom = _.toInt(dom.css(i.scrollbarXRail, 'bottom')); + i.isScrollbarXUsingBottom = i.scrollbarXBottom === i.scrollbarXBottom; // !isNaN + i.scrollbarXTop = i.isScrollbarXUsingBottom ? null : _.toInt(dom.css(i.scrollbarXRail, 'top')); + i.railBorderXWidth = _.toInt(dom.css(i.scrollbarXRail, 'borderLeftWidth')) + _.toInt(dom.css(i.scrollbarXRail, 'borderRightWidth')); + // Set rail to display:block to calculate margins + dom.css(i.scrollbarXRail, 'display', 'block'); + i.railXMarginWidth = _.toInt(dom.css(i.scrollbarXRail, 'marginLeft')) + _.toInt(dom.css(i.scrollbarXRail, 'marginRight')); + dom.css(i.scrollbarXRail, 'display', ''); + i.railXWidth = null; + i.railXRatio = null; + + i.scrollbarYRail = dom.appendTo(dom.e('div', 'ps-scrollbar-y-rail'), element); + i.scrollbarY = dom.appendTo(dom.e('div', 'ps-scrollbar-y'), i.scrollbarYRail); + i.scrollbarY.setAttribute('tabindex', 0); + i.event.bind(i.scrollbarY, 'focus', focus); + i.event.bind(i.scrollbarY, 'blur', blur); + i.scrollbarYActive = null; + i.scrollbarYHeight = null; + i.scrollbarYTop = null; + i.scrollbarYRight = _.toInt(dom.css(i.scrollbarYRail, 'right')); + i.isScrollbarYUsingRight = i.scrollbarYRight === i.scrollbarYRight; // !isNaN + i.scrollbarYLeft = i.isScrollbarYUsingRight ? null : _.toInt(dom.css(i.scrollbarYRail, 'left')); + i.scrollbarYOuterWidth = i.isRtl ? _.outerWidth(i.scrollbarY) : null; + i.railBorderYWidth = _.toInt(dom.css(i.scrollbarYRail, 'borderTopWidth')) + _.toInt(dom.css(i.scrollbarYRail, 'borderBottomWidth')); + dom.css(i.scrollbarYRail, 'display', 'block'); + i.railYMarginHeight = _.toInt(dom.css(i.scrollbarYRail, 'marginTop')) + _.toInt(dom.css(i.scrollbarYRail, 'marginBottom')); + dom.css(i.scrollbarYRail, 'display', ''); + i.railYHeight = null; + i.railYRatio = null; +} + +function getId(element) { + return element.getAttribute('data-ps-id'); +} + +function setId(element, id) { + element.setAttribute('data-ps-id', id); +} + +function removeId(element) { + element.removeAttribute('data-ps-id'); +} + +exports.add = function (element) { + var newId = guid(); + setId(element, newId); + instances[newId] = new Instance(element); + return instances[newId]; +}; + +exports.remove = function (element) { + delete instances[getId(element)]; + removeId(element); +}; + +exports.get = function (element) { + return instances[getId(element)]; +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/update-geometry.js b/view/js/perfect-scrollbar/src/js/plugin/update-geometry.js new file mode 100644 index 0000000000..b936ea1d35 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/update-geometry.js @@ -0,0 +1,126 @@ +'use strict'; + +var _ = require('../lib/helper'); +var cls = require('../lib/class'); +var dom = require('../lib/dom'); +var instances = require('./instances'); +var updateScroll = require('./update-scroll'); + +function getThumbSize(i, thumbSize) { + if (i.settings.minScrollbarLength) { + thumbSize = Math.max(thumbSize, i.settings.minScrollbarLength); + } + if (i.settings.maxScrollbarLength) { + thumbSize = Math.min(thumbSize, i.settings.maxScrollbarLength); + } + return thumbSize; +} + +function updateCss(element, i) { + var xRailOffset = {width: i.railXWidth}; + if (i.isRtl) { + xRailOffset.left = i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth - i.contentWidth; + } else { + xRailOffset.left = element.scrollLeft; + } + if (i.isScrollbarXUsingBottom) { + xRailOffset.bottom = i.scrollbarXBottom - element.scrollTop; + } else { + xRailOffset.top = i.scrollbarXTop + element.scrollTop; + } + dom.css(i.scrollbarXRail, xRailOffset); + + var yRailOffset = {top: element.scrollTop, height: i.railYHeight}; + if (i.isScrollbarYUsingRight) { + if (i.isRtl) { + yRailOffset.right = i.contentWidth - (i.negativeScrollAdjustment + element.scrollLeft) - i.scrollbarYRight - i.scrollbarYOuterWidth; + } else { + yRailOffset.right = i.scrollbarYRight - element.scrollLeft; + } + } else { + if (i.isRtl) { + yRailOffset.left = i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth * 2 - i.contentWidth - i.scrollbarYLeft - i.scrollbarYOuterWidth; + } else { + yRailOffset.left = i.scrollbarYLeft + element.scrollLeft; + } + } + dom.css(i.scrollbarYRail, yRailOffset); + + dom.css(i.scrollbarX, {left: i.scrollbarXLeft, width: i.scrollbarXWidth - i.railBorderXWidth}); + dom.css(i.scrollbarY, {top: i.scrollbarYTop, height: i.scrollbarYHeight - i.railBorderYWidth}); +} + +module.exports = function (element) { + var i = instances.get(element); + + i.containerWidth = element.clientWidth; + i.containerHeight = element.clientHeight; + i.contentWidth = element.scrollWidth; + i.contentHeight = element.scrollHeight; + + var existingRails; + if (!element.contains(i.scrollbarXRail)) { + existingRails = dom.queryChildren(element, '.ps-scrollbar-x-rail'); + if (existingRails.length > 0) { + existingRails.forEach(function (rail) { + dom.remove(rail); + }); + } + dom.appendTo(i.scrollbarXRail, element); + } + if (!element.contains(i.scrollbarYRail)) { + existingRails = dom.queryChildren(element, '.ps-scrollbar-y-rail'); + if (existingRails.length > 0) { + existingRails.forEach(function (rail) { + dom.remove(rail); + }); + } + dom.appendTo(i.scrollbarYRail, element); + } + + if (!i.settings.suppressScrollX && i.containerWidth + i.settings.scrollXMarginOffset < i.contentWidth) { + i.scrollbarXActive = true; + i.railXWidth = i.containerWidth - i.railXMarginWidth; + i.railXRatio = i.containerWidth / i.railXWidth; + i.scrollbarXWidth = getThumbSize(i, _.toInt(i.railXWidth * i.containerWidth / i.contentWidth)); + i.scrollbarXLeft = _.toInt((i.negativeScrollAdjustment + element.scrollLeft) * (i.railXWidth - i.scrollbarXWidth) / (i.contentWidth - i.containerWidth)); + } else { + i.scrollbarXActive = false; + } + + if (!i.settings.suppressScrollY && i.containerHeight + i.settings.scrollYMarginOffset < i.contentHeight) { + i.scrollbarYActive = true; + i.railYHeight = i.containerHeight - i.railYMarginHeight; + i.railYRatio = i.containerHeight / i.railYHeight; + i.scrollbarYHeight = getThumbSize(i, _.toInt(i.railYHeight * i.containerHeight / i.contentHeight)); + i.scrollbarYTop = _.toInt(element.scrollTop * (i.railYHeight - i.scrollbarYHeight) / (i.contentHeight - i.containerHeight)); + } else { + i.scrollbarYActive = false; + } + + if (i.scrollbarXLeft >= i.railXWidth - i.scrollbarXWidth) { + i.scrollbarXLeft = i.railXWidth - i.scrollbarXWidth; + } + if (i.scrollbarYTop >= i.railYHeight - i.scrollbarYHeight) { + i.scrollbarYTop = i.railYHeight - i.scrollbarYHeight; + } + + updateCss(element, i); + + if (i.scrollbarXActive) { + cls.add(element, 'ps-active-x'); + } else { + cls.remove(element, 'ps-active-x'); + i.scrollbarXWidth = 0; + i.scrollbarXLeft = 0; + updateScroll(element, 'left', 0); + } + if (i.scrollbarYActive) { + cls.add(element, 'ps-active-y'); + } else { + cls.remove(element, 'ps-active-y'); + i.scrollbarYHeight = 0; + i.scrollbarYTop = 0; + updateScroll(element, 'top', 0); + } +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/update-scroll.js b/view/js/perfect-scrollbar/src/js/plugin/update-scroll.js new file mode 100644 index 0000000000..1a9ad2ccf4 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/update-scroll.js @@ -0,0 +1,97 @@ +'use strict'; + +var instances = require('./instances'); + +var lastTop; +var lastLeft; + +var createDOMEvent = function (name) { + var event = document.createEvent("Event"); + event.initEvent(name, true, true); + return event; +}; + +module.exports = function (element, axis, value) { + if (typeof element === 'undefined') { + throw 'You must provide an element to the update-scroll function'; + } + + if (typeof axis === 'undefined') { + throw 'You must provide an axis to the update-scroll function'; + } + + if (typeof value === 'undefined') { + throw 'You must provide a value to the update-scroll function'; + } + + if (axis === 'top' && value <= 0) { + element.scrollTop = value = 0; // don't allow negative scroll + element.dispatchEvent(createDOMEvent('ps-y-reach-start')); + } + + if (axis === 'left' && value <= 0) { + element.scrollLeft = value = 0; // don't allow negative scroll + element.dispatchEvent(createDOMEvent('ps-x-reach-start')); + } + + var i = instances.get(element); + + if (axis === 'top' && value >= i.contentHeight - i.containerHeight) { + // don't allow scroll past container + value = i.contentHeight - i.containerHeight; + if (value - element.scrollTop <= 1) { + // mitigates rounding errors on non-subpixel scroll values + value = element.scrollTop; + } else { + element.scrollTop = value; + } + element.dispatchEvent(createDOMEvent('ps-y-reach-end')); + } + + if (axis === 'left' && value >= i.contentWidth - i.containerWidth) { + // don't allow scroll past container + value = i.contentWidth - i.containerWidth; + if (value - element.scrollLeft <= 1) { + // mitigates rounding errors on non-subpixel scroll values + value = element.scrollLeft; + } else { + element.scrollLeft = value; + } + element.dispatchEvent(createDOMEvent('ps-x-reach-end')); + } + + if (!lastTop) { + lastTop = element.scrollTop; + } + + if (!lastLeft) { + lastLeft = element.scrollLeft; + } + + if (axis === 'top' && value < lastTop) { + element.dispatchEvent(createDOMEvent('ps-scroll-up')); + } + + if (axis === 'top' && value > lastTop) { + element.dispatchEvent(createDOMEvent('ps-scroll-down')); + } + + if (axis === 'left' && value < lastLeft) { + element.dispatchEvent(createDOMEvent('ps-scroll-left')); + } + + if (axis === 'left' && value > lastLeft) { + element.dispatchEvent(createDOMEvent('ps-scroll-right')); + } + + if (axis === 'top') { + element.scrollTop = lastTop = value; + element.dispatchEvent(createDOMEvent('ps-scroll-y')); + } + + if (axis === 'left') { + element.scrollLeft = lastLeft = value; + element.dispatchEvent(createDOMEvent('ps-scroll-x')); + } + +}; diff --git a/view/js/perfect-scrollbar/src/js/plugin/update.js b/view/js/perfect-scrollbar/src/js/plugin/update.js new file mode 100644 index 0000000000..cc9da15f43 --- /dev/null +++ b/view/js/perfect-scrollbar/src/js/plugin/update.js @@ -0,0 +1,37 @@ +'use strict'; + +var _ = require('../lib/helper'); +var dom = require('../lib/dom'); +var instances = require('./instances'); +var updateGeometry = require('./update-geometry'); +var updateScroll = require('./update-scroll'); + +module.exports = function (element) { + var i = instances.get(element); + + if (!i) { + return; + } + + // Recalcuate negative scrollLeft adjustment + i.negativeScrollAdjustment = i.isNegativeScroll ? element.scrollWidth - element.clientWidth : 0; + + // Recalculate rail margins + dom.css(i.scrollbarXRail, 'display', 'block'); + dom.css(i.scrollbarYRail, 'display', 'block'); + i.railXMarginWidth = _.toInt(dom.css(i.scrollbarXRail, 'marginLeft')) + _.toInt(dom.css(i.scrollbarXRail, 'marginRight')); + i.railYMarginHeight = _.toInt(dom.css(i.scrollbarYRail, 'marginTop')) + _.toInt(dom.css(i.scrollbarYRail, 'marginBottom')); + + // Hide scrollbars not to affect scrollWidth and scrollHeight + dom.css(i.scrollbarXRail, 'display', 'none'); + dom.css(i.scrollbarYRail, 'display', 'none'); + + updateGeometry(element); + + // Update top/left scroll to trigger events + updateScroll(element, 'top', element.scrollTop); + updateScroll(element, 'left', element.scrollLeft); + + dom.css(i.scrollbarXRail, 'display', ''); + dom.css(i.scrollbarYRail, 'display', ''); +}; diff --git a/view/templates/admin/item/source.tpl b/view/templates/admin/item/source.tpl index 4f985cbdce..a681d8739f 100644 --- a/view/templates/admin/item/source.tpl +++ b/view/templates/admin/item/source.tpl @@ -45,7 +45,7 @@ {{if $term.type == 8}}Implicit Mention{{/if}} - {{$term.term}} + {{$term.name}} {{$term.url}} diff --git a/view/templates/head.tpl b/view/templates/head.tpl index 3896a4b4ad..a83f3dd205 100644 --- a/view/templates/head.tpl +++ b/view/templates/head.tpl @@ -6,7 +6,7 @@ - + {{foreach $stylesheets as $stylesheetUrl}} @@ -40,7 +40,7 @@ - + diff --git a/view/theme/frio/templates/head.tpl b/view/theme/frio/templates/head.tpl index b539a90d96..ae00ea85ec 100644 --- a/view/theme/frio/templates/head.tpl +++ b/view/theme/frio/templates/head.tpl @@ -11,7 +11,7 @@ - + @@ -61,7 +61,7 @@ - +