diff --git a/.github/workflows/dokuwiki.yml b/.github/workflows/dokuwiki.yml new file mode 100644 index 0000000..ed134c5 --- /dev/null +++ b/.github/workflows/dokuwiki.yml @@ -0,0 +1,11 @@ +name: DokuWiki Default Tasks +on: + push: + pull_request: + schedule: + - cron: '14 17 21 * *' + + +jobs: + all: + uses: dokuwiki/github-action/.github/workflows/all.yml@main diff --git a/Search.php b/Search.php new file mode 100644 index 0000000..ff07f9c --- /dev/null +++ b/Search.php @@ -0,0 +1,659 @@ +sort = $sort['sort']; + $this->msort = $sort['msort']; + $this->rsort = $sort['rsort']; + $this->nsort = $sort['nsort']; + $this->hsort = $sort['hsort']; + } + + /** + * @param array $data results from search + * @param bool $isInit true if first level of nodes from tree, next levels false + * @return array|false + */ + public function buildFancytreeData($data, $isInit, $currentPage) { + if(empty($data)) return false; + + $children = []; + $this->makeNodes($data, -1, 0, $children, $currentPage); + + if($isInit) { + $nodes['children'] = $children; + $nodes['debug'] = $data; + return $nodes; + } else { + return $children; + } + + + } + + private function makeNodes(&$data, $indexLatestParsedItem, $previousLevel, &$nodes, $currentPage) { + $i = 0; + $counter = 0; + foreach($data as $i=> $item) { + if($i <= $indexLatestParsedItem) { + continue; + } + if($item['level'] < $previousLevel || $counter === 0 && $item['level'] == $previousLevel) { + return $i-1; + } + $node = [ + 'title' => $item['title'], + 'key' => $item['id'] . ($item['type'] ==='f' ? '' : ':'), //ensure ns is unique + 'hns' => $item['hns'] + ]; + + // f=file, d=directory, l=directory which is lazy loaded later + if($item['type'] == 'f') { + //set current page to active + if($currentPage == $item['id']) { + $node['active'] = true; + } + } + if($item['type'] !== 'f') { //f/d/l, assumption: if 'd' try always level deeper, maybe not true if d has no items in them by some filter settings?. + $node['folder'] = true; + if($item['open'] === true){ + $node['expanded'] = true; + } + if($item['type'] === 'd') { + $node['children'] = []; + $indexLatestParsedItem = $this->makeNodes($data, $i, $item['level'], $node['children'], $currentPage); + } else { // 'l' + $node['lazy'] = true; + } + } + $nodes[] = $node; + $previousLevel = $item['level']; + $counter++; + } + return $i; + } + + + /** + * Search pages/folders depending on the given options $opts + * + * @param string $ns + * @param array $opts + * $opts['skipns'] string regexp matching namespaceids to skip + * $opts['skipfile'] string regexp matching pageids to skip + * $opts['headpage'] string headpages options or pageids + * $opts['level'] int desired depth of main namespace, -1 = all levels + * $opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their own number of opened levels + * $opts['nons'] bool exclude namespace nodes + * $opts['max'] int If initially closed, the node at max level will retrieve all its child nodes through the AJAX mechanism + * $opts['nopg'] bool exclude page nodes + * $opts['hide_headpage'] int don't hide (0) or hide (1) + * $opts['js'] bool use js-render + * @return array The results of the search + */ + public function search($ns, $opts): array + { + if(!empty($opts['tempNew'])) { + $functionName = 'searchIndexmenuItemsNew'; //NEW: a bit different logic for lazy loading of opened/closed nodes + } else { + $functionName = 'searchIndexmenuItems'; + } + global $conf; + $dataDir = $conf['datadir']; + $data = array(); + $fsDir = "/" . utf8_encodeFN(str_replace(':', '/', $ns)); + if ($this->sort || $this->msort || $this->rsort || $this->hsort) { + $this->customSearch($data, $dataDir, array($this, $functionName), $opts, $fsDir); + } else { + search($data, $dataDir, array($this, $functionName), $opts, $fsDir); + } + return $data; + } + + /** + * Callback that adds an item of namespace/page to the browsable index, if it fits in the specified options + * + * @param array $data Already collected nodes + * @param string $base Where to start the search, usually this is $conf['datadir'] + * @param string $file Current file or directory relative to $base + * @param string $type Type either 'd' for directory or 'f' for file + * @param int $lvl Current recursion depth + * @param array $opts Option array as given to search(): + * $opts['skipns'] string regexp matching namespaceids to skip, + * $opts['skipfile'] string regexp matching pageids to skip, + * $opts['headpage'] string headpages options or pageids, + * $opts['level'] int desired depth of main namespace, -1 = all levels, + * $opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their own number of opened levels, + * $opts['nons'] bool Exclude namespace nodes, + * $opts['max'] int If initially closed, the node at max level will retrieve all its child nodes through the AJAX mechanism, + * $opts['nopg'] bool Exclude page nodes, + * $opts['hide_headpage'] int don't hide (0) or hide (1), + * $opts['js'] bool use js-render + * @return bool if this directory should be traversed (true) or not (false) + * + * @author Andreas Gohr + * modified by Samuele Tognini + */ + public function searchIndexmenuItems(&$data, $base, $file, $type, $lvl, $opts) { + global $conf; + $hns = false; + $isOpen = false; + $title = null; + $skipns = $opts['skipns']; + $skipfile = $opts['skipfile']; + $headpage = $opts['headpage']; + $id = pathID($file); + if($type == 'd') { + // Skip folders in plugin conf + foreach($skipns as $skipn) { + if(!empty($skipn) && preg_match($skipn, $id)){ + return false; + } + } + //check ACL (for sneaky_index namespaces too). + if($conf['sneaky_index'] && auth_quickaclcheck($id.':') < AUTH_READ) return false; + + //Open requested level + if($opts['level'] > $lvl || $opts['level'] == -1) { + $isOpen = true; + } + //Search optional subnamespaces with + if(!empty($opts['subnss'])) { + $subnss = $opts['subnss']; + for($a = 0; $a < count($subnss); $a++) { + if(preg_match("/^".$id."($|:.+)/i", $subnss[$a][0], $match)) { + //It contains a subnamespace + $isOpen = true; + } elseif(preg_match("/^".$subnss[$a][0]."(:.*)/i", $id, $match)) { + //It's inside a subnamespace, check level + // -1 is open all, otherwise count number of levels in the remainer of the pageid + // (match[0] is always prefixed with :) + if($subnss[$a][1] == -1 || substr_count($match[1], ":") < $subnss[$a][1]) { + $isOpen = true; + } else { + $isOpen = false; + } + } + } + } + if($opts['nons']) { + return $isOpen; + } elseif($opts['max'] > 0 && !$isOpen && $lvl >= $opts['max']) { + $isOpen = false; + //Stop recursive searching + $shouldBeTraversed = false; + //change type + $type = "l"; + } elseif($opts['js']) { + $shouldBeTraversed = true; //TODO if js tree, then traverse deeper??? + } else { + $shouldBeTraversed = $isOpen; + } + //Set title and headpage + $title = $this->getNamespaceTitle($id, $headpage, $hns); + //link namespace nodes to start pages when excluding page nodes + if(!$hns && $opts['nopg']) { + $hns = $id.":".$conf['start']; + } + } else { + //Nopg. Dont show pages + if($opts['nopg']) return false; + + $shouldBeTraversed = true; + //Nons.Set all pages at first level + if($opts['nons']) { + $lvl = 1; + } + //don't add + if(substr($file, -4) != '.txt') return false; + //check hiddens and acl + if(isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ) return false; + //Skip files in plugin conf + foreach($skipfile as $skipf) { + if(!empty($skipf) && preg_match($skipf, $id)) + return false; + } + //Skip headpages to hide + if(!$opts['nons'] && !empty($headpage) && $opts['hide_headpage']) { + //start page is in root + if($id == $conf['start']) return false; + + $ahp = explode(",", $headpage); + foreach($ahp as $hp) { + switch($hp) { + case ":inside:": + if(noNS($id) == noNS(getNS($id))) return false; + break; + case ":same:": + if(@is_dir(dirname(wikiFN($id))."/".utf8_encodeFN(noNS($id)))) return false; + break; + //it' s an inside start + case ":start:": + if(noNS($id) == $conf['start']) return false; + break; + default: + if(noNS($id) == cleanID($hp)) return false; + } + } + } + //Set title + if($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { + $title = p_get_first_heading($id, false); + } + if(is_null($title)) { + $title = noNS($id); + } + $title = htmlspecialchars($title, ENT_QUOTES); + } + + $item = array( + 'id' => $id, + 'type' => $type, + 'level' => $lvl, + 'open' => $isOpen, + 'title' => $title, + 'hns' => $hns, + 'file' => $file, + 'shouldBeTraversed' => $shouldBeTraversed + ); + $item['sort'] = $this->getSortValue($item); + $data[] = $item; + return $shouldBeTraversed; + } + + /** + * Callback that adds an item of namespace/page to the browsable index, if it fits in the specified options + * + + * testing version, for debuggin/fixing lazyloading... + + + * @author Andreas Gohr + * modified by Samuele Tognini + * + * @param array $data Already collected nodes + * @param string $base Where to start the search, usually this is $conf['datadir'] + * @param string $file Current file or directory relative to $base + * @param string $type Type either 'd' for directory or 'f' for file + * @param int $lvl Current recursion depth + * @param array $opts Option array as given to search() + * $opts['skipns'] string regexp matching namespaceids to skip + * $opts['skipfile'] string regexp matching pageids to skip + * $opts['headpage'] string headpages options or pageids + * $opts['level'] int desired depth of main namespace, -1 = all levels + * $opts['subnss'] array with entries: array(namespaceid,level) specifying namespaces with their own level + * $opts['nons'] bool exclude namespace nodes + * $opts['max'] int If initially closed, the node at max level will retrieve all its child nodes through the AJAX mechanism + * $opts['nopg'] bool exclude page nodes + * $opts['hide_headpage'] int don't hide (0) or hide (1) + * $opts['js'] bool use js-render + * @return bool if this directory should be traversed (true) or not (false) + */ + public function searchIndexmenuItemsNew(&$data, $base, $file, $type, $lvl, $opts) { + global $conf; + $hns = false; + $isOpen = false; + $title = null; + $skipns = $opts['skipns']; + $skipfile = $opts['skipfile']; + $headpage = $opts['headpage']; + $id = pathID($file); + + if($type == 'd') { + // Skip folders in plugin conf + foreach($skipns as $skipn) { + if(!empty($skipn) && preg_match($skipn, $id)){ + return false; + } + } + //check ACL (for sneaky_index namespaces too). + if($conf['sneaky_index'] && auth_quickaclcheck($id.':') < AUTH_READ) return false; + + //Open requested level + if($opts['level'] > $lvl || $opts['level'] == -1) { + $isOpen = true; + } + + //Search optional subnamespaces with + if(!empty($opts['subnss'])) { + $subnss = $opts['subnss']; + + for($a = 0; $a < count($subnss); $a++) { + if(preg_match("/^".$id."($|:.+)/i", $subnss[$a][0], $match)) { + //It contains a subnamespace + $isOpen = true; + } elseif(preg_match("/^".$subnss[$a][0]."(:.*)/i", $id, $match)) { + //It's inside a subnamespace, check level + if($subnss[$a][1] == -1 || substr_count($match[1], ":") < $subnss[$a][1]) { + $isOpen = true; + } else { + $isOpen = false; + } + } + } + } + if($opts['nons']) { + return $isOpen; + } elseif($opts['max'] > 0 && !$isOpen) { + // limited levels per request, node is closed + if($lvl >= $opts['max']) { // + //change type, more nodes should be loaded by ajax + $type = "l"; + $shouldBeTraversed = false; + } else { + //node is closed, but still more levels requested with max + $shouldBeTraversed = true; + } + } else { + $shouldBeTraversed = $isOpen; + } + //Set title and headpage + $title = $this->getNamespaceTitle($id, $headpage, $hns); + //link namespace nodes to start pages when excluding page nodes + if(!$hns && $opts['nopg']) { + $hns = $id.":".$conf['start']; + } + } else { + //Nopg.Dont show pages + if($opts['nopg']) return false; + + $shouldBeTraversed = true; + //Nons.Set all pages at first level + if($opts['nons']) { + $lvl = 1; + } + //don't add + if(substr($file, -4) != '.txt') return false; + //check hiddens and acl + if(isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ) return false; + //Skip files in plugin conf + foreach($skipfile as $skipf) { + if(!empty($skipf) && preg_match($skipf, $id)) + return false; + } + //Skip headpages to hide + if(!$opts['nons'] && !empty($headpage) && $opts['hide_headpage']) { + //start page is in root + if($id == $conf['start']) return false; + + $ahp = explode(",", $headpage); + foreach($ahp as $hp) { + switch($hp) { + case ":inside:": + if(noNS($id) == noNS(getNS($id))) return false; + break; + case ":same:": + if(@is_dir(dirname(wikiFN($id))."/".utf8_encodeFN(noNS($id)))) return false; + break; + //it' s an inside start + case ":start:": + if(noNS($id) == $conf['start']) return false; + break; + default: + if(noNS($id) == cleanID($hp)) return false; + } + } + } + + //Set title + if($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { + $title = p_get_first_heading($id, false); + } + if(is_null($title)) { + $title = noNS($id); + } + $title = htmlspecialchars($title, ENT_QUOTES); + } + + $item = array( + 'id' => $id, + 'type' => $type, + 'level' => $lvl, + 'open' => $isOpen, + 'title' => $title, + 'hns' => $hns, + 'file' => $file, + 'shouldBeTraversed' => $shouldBeTraversed + ); + $item['sort'] = $this->getSortValue($item); + $data[] = $item; + return $shouldBeTraversed; + } + + /** + * callback that recurse directory + * + * This function recurses into a given base directory + * and calls the supplied function for each file and directory + * + * Similar to search() of inc/search.php, but has extended sorting options + * + * @param array $data The results of the search are stored here + * @param string $base Where to start the search + * @param callback $func Callback (function name or array with object,method) + * @param array $opts List of indexmenu options + * @param string $dir Current directory beyond $base + * @param int $lvl Recursion Level + * + * @author Andreas Gohr + * @author modified by Samuele Tognini + */ + public function customSearch(&$data, $base, $func, $opts, $dir = '', $lvl = 1) { + $dirs = array(); + $files = array(); + $files_tmp = array(); + $dirs_tmp = array(); + $count = count($data); + + //read in directories and files + $dh = @opendir($base.'/'.$dir); + if(!$dh) return; + while(($file = readdir($dh)) !== false) { + //skip hidden files and upper dirs + if(preg_match('/^[._]/', $file)) continue; + if(is_dir($base.'/'.$dir.'/'.$file)) { + $dirs[] = $dir.'/'.$file; + continue; + } + $files[] = $dir.'/'.$file; + } + closedir($dh); + + //Collect and sort dirs + if($this->nsort) { + //collect the wanted directories in dirs_tmp + foreach($dirs as $dir) { + call_user_func_array($func, array(&$dirs_tmp, $base, $dir, 'd', $lvl, $opts)); + } + //sort directories + usort($dirs_tmp, array($this, "compareNodes")); + //add and search each directory + foreach($dirs_tmp as $dir) { + $data[] = $dir; + if($dir['shouldBeTraversed']) { + $this->customSearch($data, $base, $func, $opts, $dir['file'], $lvl + 1); + } + } + } else { + //sort by page name + sort($dirs); + //collect directories + foreach($dirs as $dir) { + if(call_user_func_array($func, array(&$data, $base, $dir, 'd', $lvl, $opts))) { + $this->customSearch($data, $base, $func, $opts, $dir, $lvl + 1); + } + } + } + + //Collect and sort files + foreach($files as $file) { + call_user_func_array($func, array(&$files_tmp, $base, $file, 'f', $lvl, $opts)); + } + usort($files_tmp, array($this, "compareNodes")); + + //count added items + $added = count($data) - $count; + + if($added === 0 && empty($files_tmp)) { + //remove empty directory again, only if it has not a headpage associated + $v = end($data); + if(!$v['hns']) { + array_pop($data); + } + } else { + //add files to index + $data = array_merge($data, $files_tmp); + } + } + + + /** + * Get namespace title, checking for headpages + * + * @author Samuele Tognini + * @param string $ns namespace + * @param string $headpage comma-separated headpages options and headpages + * @param string $hns reference pageid of headpage, false when not existing + * @return string when headpage & heading on: title of headpage, otherwise: namespace name + */ + public function getNamespaceTitle($ns, $headpage, &$hns) { + global $conf; + $hns = false; + $title = noNS($ns); + if(empty($headpage)) { + return $title; + } + $ahp = explode(",", $headpage); + foreach($ahp as $hp) { + switch($hp) { + case ":inside:": + $page = $ns.":".noNS($ns); + break; + case ":same:": + $page = $ns; + break; + //it's an inside start + case ":start:": + $page = ltrim($ns.":".$conf['start'], ":"); + break; + //inside pages + default: + $page = $ns.":".$hp; + } + //check headpage + if(@file_exists(wikiFN($page)) && auth_quickaclcheck($page) >= AUTH_READ) { + if($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { + $title_tmp = p_get_first_heading($page, false); + if(!is_null($title_tmp)) { + $title = $title_tmp; + } + } + $title = htmlspecialchars($title, ENT_QUOTES); + $hns = $page; + //headpage found, exit for + break; + } + } + return $title; + } + + + /** + * callback that sorts nodes + * + * @param array $a first node as array with 'sort' entry + * @param array $b second node as array with 'sort' entry + * @return int if less than zero 1st node is less than 2nd, otherwise equal respectively larger + */ + private function compareNodes($a, $b) { + if($this->rsort) { + return strnatcasecmp($b['sort'], $a['sort']); + } else { + return strnatcasecmp($a['sort'], $b['sort']); + } + } + + /** + * Add sort information to item. + * + * @author Samuele Tognini + * + * @param array $item + * @return bool|int|mixed|string + */ + private function getSortValue($item) { + global $conf; + + $sort = false; + $page = false; + if($item['type'] == 'd' || $item['type'] == 'l') { + //Fake order info when nsort is not requested + if($this->nsort) { + $page = $item['hns']; + } else { + $sort = 0; + } + } + if($item['type'] == 'f') { + $page = $item['id']; + } + if($page) { + if($this->hsort && noNS($item['id']) == $conf['start']) { + $sort = 1; + } + if($this->msort) { + $sort = p_get_metadata($page, $this->msort); + } + if(!$sort && $this->sort) { + switch($this->sort) { + case 't': + $sort = $item['title']; + break; + case 'd': + $sort = @filectime(wikiFN($page)); + break; + } + } + } + if($sort === false) { + $sort = noNS($item['id']); + } + return $sort; + } + +} diff --git a/_test/ActionTest.php b/_test/ActionTest.php new file mode 100644 index 0000000..140bf1a --- /dev/null +++ b/_test/ActionTest.php @@ -0,0 +1,36 @@ +pluginsEnabled[] = 'indexmenu'; + parent::setUp(); // this enables the include plugin +// $this->helper = plugin_load('helper', 'include'); + +// global $conf; +// $conf['hidepages'] = 'inclhidden:hidden'; + + // for testing hidden pages + saveWikiText('ns2:bpage', "======H1======\nText", 'Sort different naturally/title/page'); + saveWikiText('ns2:apage', "======H3======\nText", 'Sort different naturally/title/page'); + saveWikiText('ns2:cpage', "======H2======\nText", 'Sort different naturally/title/page'); + + // pages on different levels + saveWikiText('ns1:ns1:apage', 'Page on level 1', 'Created page on level 1'); + saveWikiText('ns1:lvl2:lvl3:lvl4:apage', 'Page on level 4', 'Created page on level 4'); + saveWikiText('ns1:ns2:apage', 'Page on level 2', 'Created page on level 2'); + saveWikiText('ns1:ns0:bpage', 'Page on level 2', 'Created page on level 2'); + } + +} diff --git a/_test/AjaxRequestsTest.php b/_test/AjaxRequestsTest.php new file mode 100644 index 0000000..ceeebdb --- /dev/null +++ b/_test/AjaxRequestsTest.php @@ -0,0 +1,322 @@ +pluginsEnabled[] = 'indexmenu'; + parent::setUp(); // this enables the indexmenu plugin + + //needed for 'tsort' to use First headings, sets title during search, otherwise as fallback page name used. + global $conf; + $conf['useheading'] = 'navigation'; + + + // for testing sorting pages + saveWikiText('ns2:cpage', "======Bb======\nText", 'Sort different page/title/creation date'); + sleep(1); // ensure different timestamps for 'dsort' + saveWikiText('ns2:bpage', "======Aa======\nText", 'Sort different page/title/creation date'); + sleep(1); + saveWikiText('ns2:apage', "======Cc======\nText", 'Sort different page/title/creation date'); + + //ensures title is added to metadata of page + idx_addPage('ns2:cpage'); + idx_addPage('ns2:bpage'); + idx_addPage('ns2:apage'); + + // pages on different levels + saveWikiText('ns1:ns2:apage', "======Bb======\nPage on level 2", 'Created page on level 2'); + saveWikiText('ns1:ns1:apage', "======Ee======\nPage on level 2", 'Created page on level 2'); + saveWikiText('ns1:ns1:lvl3:lvl4:apage', "======Cc======\nPage on levl 4", 'Page on level 4'); + saveWikiText('ns1:ns1:start', "======Aa======\nPage on level 2", 'Startpage on level 2'); + saveWikiText('ns1:ns0:bpage', "======Aa2======\nPage on level 2", 'Created page on level 2'); + saveWikiText('ns1:apage', "======Dd======\nPage on level 1", 'Created page on level 1'); + + //ensures title is added to metadata + idx_addPage('ns1:ns1:apage'); + idx_addPage('ns1:ns1:lvl3:lvl4:apage'); + idx_addPage('ns1:ns1:start'); + idx_addPage('ns1:ns2:apage'); + idx_addPage('ns1:ns0:bpage'); + idx_addPage('ns1:apage'); + } + + /** + * DataProvider for the builtin Ajax calls + * + * @return array + */ + public static function indexmenuCalls() + { + return [ + // Call, POST parameters, result function + [ + 'indexmenu', + AjaxRequestsTest::prepareParams(['level' => 1]), + 'expectedResultWiki' + ], + [ + 'indexmenu', + AjaxRequestsTest::prepareParams(['ns' => 'ns2', 'level' => 1]), + 'expectedResultNs2PageSort' + ], + [ + 'indexmenu', + AjaxRequestsTest::prepareParams(['ns' => 'ns2', 'level' => 1, 'sort' => 't']), + 'expectedResultNs2TitleSort' + ], + [ + 'indexmenu', + AjaxRequestsTest::prepareParams(['ns' => 'ns2', 'level' => 1, 'sort' => 'd']), + 'expectedResultNs2CreationDateSort' + ], + [ + 'indexmenu', + AjaxRequestsTest::prepareParams(['ns' => 'ns1', 'level' => 1, 'sort' => 't']), + 'expectedResultNs1TitleSort' + ], + [ + 'indexmenu', + AjaxRequestsTest::prepareParams(['ns' => 'ns1', 'level' => 1, 'sort' => 't', 'nsort' => 1]), + 'expectedResultNs1TitleSortNamespaceSort' + ] + ]; + } + + /** + * @dataProvider indexmenuCalls + * + * @param string $call + * @param array $post + * @param $expectedResult + */ + public function testBasicSorting($call, $post, $expectedResult) + { + $request = new TestRequest(); + $response = $request->post(['call' => $call] + $post, '/lib/exe/ajax.php'); +// $this->assertNotEquals("AJAX call '$call' unknown!\n", $response->getContent()); + +//var_export(json_decode($response->getContent()), true); // print as PHP array + + $actualArray = json_decode($response->getContent(), true); + unset($actualArray['debug']); + unset($actualArray['sort']); + unset($actualArray['opts']); + + $this->assertEquals($this->$expectedResult(), $actualArray); + +// $regexp: null, or regexp pattern to match +// example: '/^
exampleIndex = "{{indexmenu>:}}"; +// +// parent::__construct(); +// } + + /** + * Create from list of values the output array of handle() + * + * @param array $values + * @return array aligned similar to output of handle() + */ + private function createData($values) { + + [$ns, $theme, $identifier, $nocookie, $navbar, $noscroll, $maxjs, $notoc, $jsajax, $context, $nomenu, + $sort, $msort, $rsort, $nsort, $level, $nons, $nopg, $subnss, $max, $maxAjax, $js, $skipns, $skipfile, $hsort, + $headpage, $hide_headpage, $jsVersion] = $values; + + return [ + $ns, + [ + 'theme' => $theme, + 'identifier' => $identifier, + 'nocookie' => $nocookie, + 'navbar' => $navbar, + 'noscroll' => $noscroll, + 'maxJs' => $maxjs, + 'notoc' => $notoc, + 'jsAjax' => $jsajax, + 'context' => $context, + 'nomenu' => $nomenu, + ], + [ + 'sort' => $sort, + 'msort' => $msort, + 'rsort' => $rsort, + 'nsort' => $nsort, + 'hsort' => $hsort, + ], + [ + 'level' => $level, + 'nons' => $nons, + 'nopg' => $nopg, + 'subnss' => $subnss, + 'max' => $max, + 'js' => $js, + 'skipns' => $skipns, + 'skipfile' => $skipfile, + 'headpage' => $headpage, + 'hide_headpage' => $hide_headpage, + 'maxajax' => $maxAjax, + 'navbar' => $navbar, + 'theme' => $theme + ], + $jsVersion + ]; + } + + /** + * Data provider + * + * @return array[] + */ + public static function someSyntaxes() { + return [ + //root ns (empty is not recognized..) + // [syntax, data] + [ + "{{indexmenu>:}}", + [ + '', 'default', 'random', false, false, false, 1, false, '', false, false, + 0, false, false, false, -1, false, false, [], 0, 1, false, [''], [''], false, + ":start:,:same:,:inside:", 1, 1 + ] + ], + //root ns, #levels=1, js renderer + [ + "{{indexmenu>#1|js}}", + [ + '', 'default', 'random', false, false, false, 1, false, '', false, false, + 0, false, false, false, 1, false, false, [], 0, 1, true, [''], [''], false, + ":start:,:same:,:inside:", 1, 1 + ] + ], + //root ns, #levels=2, all not js specific options (nocookie is from context) + [ + "{{indexmenu>#2 test#6|navbar context tsort dsort msort hsort rsort nsort nons nopg}}", + [ + '', 'default', 'random', true, true, false, 1, false, '&sort=t&msort=indexmenu_n&rsort=1&nsort=1&hsort=1&nopg=1', true, false, + 't', 'indexmenu_n', true, true, 2, true, true, [['test', 6]], 0, 1, false, [''], [''], true, + ":start:,:same:,:inside:", 1, 1 + ] + ], + //root ns, #levels=2, js renderer, all not js specific options + [ + "{{indexmenu>#2 test#6|navbar js#bj_ubuntu.png context tsort dsort msort hsort rsort nsort nons nopg}}", + [ + '', 'bj_ubuntu.png', 'random', true, true, false, 1, false, '&sort=t&msort=indexmenu_n&rsort=1&nsort=1&hsort=1&nopg=1', true, false, + 't', 'indexmenu_n', true, true, 2, true, true, [['test', 6]], 0, 1, true, [''], [''], true, + ":start:,:same:,:inside:", 1, 1 + ] + ], + //root ns, #levels=1, all options + [ + "{{indexmenu>#1|navbar context nocookie noscroll notoc nomenu dsort msort#date:modified hsort rsort nsort nons nopg max#2#4 maxjs#3 id#54321}}", + [ + '', 'default', 'random', true, true, true, 1, true, '&sort=d&msort=date modified&rsort=1&nsort=1&hsort=1&nopg=1', true, true, + 'd', 'date modified', true, true, 1, true, true, [], 0, 1, false, [''], [''], true, + ":start:,:same:,:inside:", 1, 1 + ] + ], + //root ns, #levels=1, js renderer, all options + [ + "{{indexmenu>#1|js#bj_ubuntu.png navbar context nocookie noscroll notoc nomenu dsort msort#date:modified hsort rsort nsort nons nopg max#2#4 maxjs#3 id#54321}}", + [ + '', 'bj_ubuntu.png', 54321, true, true, true, 3, true, '&sort=d&msort=date modified&rsort=1&nsort=1&hsort=1&nopg=1&max=4', true, true, + 'd', 'date modified', true, true, 1, true, true, [], 2, 4, true, [''], [''], true, + ":start:,:same:,:inside:", 1, 1 + ] + ], + //root ns, #levels=1, skipfile and ns + + [ + "{{indexmenu>#1 test|skipfile+/(^myusers:spaces$|privatens:userss)/ skipns=/(^myusers:spaces$|privatens:users)/ id#ns}}", + [ + '', 'default', 'random', false, false, false, 1, false, '&skipns=%3D/%28%5Emyusers%3Aspaces%24%7Cprivatens%3Ausers%29/&skipfile=%2B/%28%5Emyusers%3Aspaces%24%7Cprivatens%3Auserss%29/', false, false, + 0, false, false, false, 1, false, false, [['test', -1]], 0, 1, false, ['/(^myusers:spaces$|privatens:users)/'], ['', '/(^myusers:spaces$|privatens:userss)/'], false, + ":start:,:same:,:inside:", 1, 1 + ] + ], + //root ns, #levels=1, js renderer, skipfile and ns + [ + "{{indexmenu>#1 test|js skipfile=/(^myusers:spaces$|privatens:userss)/ skipns+/(^myusers:spaces$|privatens:userssss)/ id#ns}}", + [ + '', 'default', 0, false, false, false, 1, false, '&skipns=%2B/%28%5Emyusers%3Aspaces%24%7Cprivatens%3Auserssss%29/&skipfile=%3D/%28%5Emyusers%3Aspaces%24%7Cprivatens%3Auserss%29/', false, false, + 0, false, false, false, 1, false, false, [['test', -1]], 0, 1, true, ['', '/(^myusers:spaces$|privatens:userssss)/'], ['/(^myusers:spaces$|privatens:userss)/'], false, + ":start:,:same:,:inside:", 1, 1 + ] + ] + ]; + } + + /** + * Parse the syntax to options + * expect: different combinations with or without js option, covers recognizing all syntax options + * + * @dataProvider someSyntaxes + */ + public function testHandle($syntax, $changedData) { + $plugin = new syntax_plugin_indexmenu_indexmenu(); + + $null = new Doku_Handler(); + $result = $plugin->handle($syntax, 0, 40, $null); + + //copy unique generated number, which is about 23 characters + $len_id = strlen($result[1]['identifier']); + if(!is_numeric($changedData[2]) && ($len_id > 20 && $len_id<=23)) { + $changedData[2] = $result[1]['identifier']; + } + $data = $this->createData($changedData); + + $this->assertEquals($data, $result, 'Data array corrupted'); + } + + + /** + * Data provider + * + * @return array[] + */ + public static function differentNSs() + { + $pageInRoot = 'page'; + $pageInLvl1 = 'ns:page'; + $pageInLvl2 = 'ns1:ns2:page'; + return [ + //indexmenu on page at root level + ['{{indexmenu>|}}', '', [], $pageInRoot], + ['{{indexmenu>#1}}', '', [], $pageInRoot], + ['{{indexmenu>:}}', '', [], $pageInRoot], + ['{{indexmenu>.}}', '', [], $pageInRoot], + ['{{indexmenu>.:}}', '', [], $pageInRoot], + ['{{indexmenu>..}}', '', [], $pageInRoot], + ['{{indexmenu>..:}}', '', [], $pageInRoot], + ['{{indexmenu>myns}}', 'myns', [], $pageInRoot], + ['{{indexmenu>:myns}}', 'myns', [], $pageInRoot], + ['{{indexmenu>.myns}}', 'myns', [], $pageInRoot], + ['{{indexmenu>.:myns}}', 'myns', [], $pageInRoot], + ['{{indexmenu>..myns}}', 'myns', [], $pageInRoot], + ['{{indexmenu>..:myns}}', 'myns', [], $pageInRoot], + + //indexmenu on page in a namespace + ['{{indexmenu>|}}', '', [], $pageInLvl1], + ['{{indexmenu>#1}}', '', [], $pageInLvl1], + ['{{indexmenu>:}}', '', [], $pageInLvl1], + ['{{indexmenu>.}}', 'ns', [], $pageInLvl1], + ['{{indexmenu>.:}}', 'ns', [], $pageInLvl1], + ['{{indexmenu>..}}', '', [], $pageInLvl1], + ['{{indexmenu>..:}}', '', [], $pageInLvl1], + ['{{indexmenu>myns}}', 'myns', [], $pageInLvl1], //was ns:myns + ['{{indexmenu>:myns}}', 'myns', [], $pageInLvl1], + ['{{indexmenu>.myns}}', 'ns:myns', [], $pageInLvl1], + ['{{indexmenu>.:myns}}', 'ns:myns', [], $pageInLvl1], + ['{{indexmenu>..myns}}', 'myns', [], $pageInLvl1], + ['{{indexmenu>..:myns}}', 'myns', [], $pageInLvl1], + ['{{indexmenu>myns:myns}}', 'myns:myns', [], $pageInLvl2], + + //indexmenu on page in a namespace + ['{{indexmenu>|}}', '', [], $pageInLvl2], + ['{{indexmenu>#1}}', '', [], $pageInLvl2], + ['{{indexmenu>:}}', '', [], $pageInLvl2], + ['{{indexmenu>.}}', 'ns1:ns2', [], $pageInLvl2], + ['{{indexmenu>.:}}', 'ns1:ns2', [], $pageInLvl2], + ['{{indexmenu>..}}', '', [], $pageInLvl2], //strange indexmenu specific exception! TODO remove? + ['{{indexmenu>..:}}', 'ns1', [], $pageInLvl2], + ['{{indexmenu>myns}}', 'myns', [], $pageInLvl2], //was ns1:ns2:myns + ['{{indexmenu>:myns}}', 'myns', [], $pageInLvl2], + ['{{indexmenu>.myns}}', 'ns1:ns2:myns', [], $pageInLvl2], + ['{{indexmenu>.:myns}}', 'ns1:ns2:myns', [], $pageInLvl2], + ['{{indexmenu>..myns}}', 'ns1:myns', [], $pageInLvl2], + ['{{indexmenu>..:myns}}', 'ns1:myns', [], $pageInLvl2], + ['{{indexmenu>myns:myns}}', 'myns:myns', [], $pageInLvl2], + + ['{{indexmenu>..:..:myns}}', 'ns1:myns', [], 'ns1:ns2:ns3:page'], + ['{{indexmenu>0}}', '0', [], 'ns1:page'], //was ns1:0 + + //indexmenu on page at root level and subns + ['{{indexmenu> #1|}}', '', [], $pageInLvl2], //no subns, spaces before are removed + ['{{indexmenu>#1 #1}}', '', [['', 1]], $pageInLvl2], + ['{{indexmenu>: :}}', '', [['', -1]], $pageInLvl2], + ['{{indexmenu>. .}}', 'ns1:ns2', [['ns1:ns2', -1]], $pageInLvl2], + ['{{indexmenu>.: .:}}', 'ns1:ns2', [['ns1:ns2', -1]], $pageInLvl2], + ['{{indexmenu>.. ..}}', '', [['', -1]], $pageInLvl2], + ['{{indexmenu>..: ..:}}', 'ns1', [['ns1', -1]], $pageInLvl2], + ['{{indexmenu>myns myns}}', 'myns', [['myns', -1]], $pageInLvl2], //was ns1:ns2:myns + ['{{indexmenu>:myns :myns}}', 'myns', [['myns', -1]], $pageInLvl2], + ['{{indexmenu>.myns .myns}}', 'ns1:ns2:myns', [['ns1:ns2:myns', -1]], $pageInLvl2], + ['{{indexmenu>.:myns .:myns}}', 'ns1:ns2:myns', [['ns1:ns2:myns', -1]], $pageInLvl2], + ['{{indexmenu>..myns ..myns}}', 'ns1:myns', [['ns1:myns', -1]], $pageInLvl2], + ['{{indexmenu>..:myns ..myns}}', 'ns1:myns', [['ns1:myns', -1]], $pageInLvl2], + ['{{indexmenu>myns:myns myns:myns}}', 'myns:myns', [['myns:myns', -1]], $pageInLvl2], + + //indexmenu on page in a namespace + ['{{indexmenu>|}}', '', [], $pageInLvl2], + ['{{indexmenu>#1}}', '', [], $pageInLvl2], + ['{{indexmenu>:}}', '', [], $pageInLvl2], + ['{{indexmenu>.}}', 'ns1:ns2', [], $pageInLvl2], + ['{{indexmenu>.:}}', 'ns1:ns2', [], $pageInLvl2], + ['{{indexmenu>..}}', '', [], $pageInLvl2], //strange indexmenu specific exception! TODO remove? + ['{{indexmenu>..:}}', 'ns1', [], $pageInLvl2], + ['{{indexmenu>myns:}}', 'myns', [], $pageInLvl2], //was ns1:ns2:myns + ['{{indexmenu>:myns:}}', 'myns', [], $pageInLvl2], + ['{{indexmenu>.myns:}}', 'ns1:ns2:myns', [], $pageInLvl2], + ['{{indexmenu>.:myns:}}', 'ns1:ns2:myns', [], $pageInLvl2], + ['{{indexmenu>..myns:}}', 'ns1:myns', [], $pageInLvl2], + ['{{indexmenu>..:myns:}}', 'ns1:myns', [], $pageInLvl2], + ['{{indexmenu>myns:myns:}}', 'myns:myns', [], $pageInLvl2], + ]; + } + + /** + * Parse the syntax to options + * expect: different combinations with or without js option, covers recognizing all syntax options + * + * @dataProvider differentNSs + */ + public function testResolving($syntax, $expectedNs, $expectedSubNss, $pageWithIndexmenu) { + global $ID; + $ID = $pageWithIndexmenu; + + $plugin = new syntax_plugin_indexmenu_indexmenu(); + + $null = new Doku_Handler(); + $result = $plugin->handle($syntax, 0, 40, $null); + + $this->assertEquals($expectedNs, $result[0], 'check resolved ns'); + $this->assertEquals($expectedSubNss, $result[3]['subnss'], 'check resolved subNSs'); + } + + /** + * Rendering for nonexisting namespace + * expect: no paragraph due to no message set + * expect: one paragraph, since message set + * expect: contains namespace which replaced {{ns}} + * expect: message contained rendered italic syntax + */ + public function testRenderEmptymsg() { + global $conf; + + $noexistns = 'nonexisting:namespace'; + $emptyindexsyntax = "{{indexmenu>$noexistns}}"; + + $xhtml = new Doku_Renderer_xhtml(); + $plugin = new syntax_plugin_indexmenu_indexmenu(); + + $null = new Doku_Handler(); + $result = $plugin->handle($emptyindexsyntax, 0, 10, $null); + + //no empty message + $plugin->render('xhtml', $xhtml, $result); + + $doc = (new Document())->html($xhtml->doc); + $this->assertEquals(0, $doc->find('p')->count()); + + // Fill in empty message + $conf['plugin']['indexmenu']['empty_msg'] = 'This namespace is //empty//: {{ns}}'; + $plugin->render('xhtml', $xhtml, $result); + $doc = (new Document())->html($xhtml->doc); + + $this->assertEquals(1, $doc->find('p')->count()); +// $this->assertEquals(1, $doc->find("p:contains($noexistns)")->count()); + $this->assertEquals(1, $doc->find("p em")->count()); + } + +} diff --git a/_test/indexmenu_syntax_indexmenu.test.php b/_test/indexmenu_syntax_indexmenu.test.php deleted file mode 100644 index 5c0f848..0000000 --- a/_test/indexmenu_syntax_indexmenu.test.php +++ /dev/null @@ -1,214 +0,0 @@ -pluginsEnabled[] = 'indexmenu'; - parent::setup(); - - //$conf['plugin']['indexmenu']['headpage'] = ''; - //$conf['plugin']['indexmenu']['hide_headpage'] = false; - - //saveWikiText('titleonly:sub:test', "====== Title ====== \n content", 'created'); - //saveWikiText('test', "====== Title ====== \n content", 'created'); - //idx_addPage('titleonly:sub:test'); - //idx_addPage('test'); - } - - function __construct() { - $this->exampleIndex = "{{indexmenu>:}}"; - } - - /** - * Create from list of values the output array of handle() - * - * @param array $values - * @return array aligned similar to output of handle() - */ - function createData($values) { - - list($ns, $theme, $identifier, $nocookie, $navbar, $noscroll, $maxjs, $notoc, $jsajax, $context, $nomenu, - $sort, $msort, $rsort, $nsort, $level, $nons, $nopg, $nss, $max, $js, $skipns, $skipfile, $hsort, - $headpage, $hide_headpage) = $values; - - return array( - $ns, - Array( - 'theme' => $theme, - 'identifier' => $identifier, - 'nocookie' => $nocookie, - 'navbar' => $navbar, - 'noscroll' => $noscroll, - 'maxjs' => $maxjs, - 'notoc' => $notoc, - 'jsajax' => $jsajax, - 'context' => $context, - 'nomenu' => $nomenu, - ), - $sort, - $msort, - $rsort, - $nsort, - array( - 'level' => $level, - 'nons' => $nons, - 'nopg' => $nopg, - 'nss' => $nss, - 'max' => $max, - 'js' => $js, - 'skip_index' => $skipns, - 'skip_file' => $skipfile, - 'headpage' => $headpage, - 'hide_headpage' => $hide_headpage - ), - $hsort - ); - } - - /** - * Parse the syntax to options - * expect: different combinations with or without js option, covers recognizing all syntax options - */ - function testHandle() { - global $conf; - - $plugin = new syntax_plugin_indexmenu_indexmenu(); - - $null = new Doku_Handler(); - $result = $plugin->handle($this->exampleIndex, 0, 40, $null); - - $idcalculatedfromns = sprintf("%u", crc32('')); - $tests = array( - //root ns (empty is not recognized..) - array( - 'syntax'=> "{{indexmenu>:}}", - 'data' => array( - '', 'default', 'random', false, false, false, 0, false, '', false, false, - 0, false, false, false, -1, false, false, array(), 0, false, array(''), array(''), false, - ":start:,:same:,:inside:", 1 - ) - ), - //root ns, #levels=1, js renderer - array( - 'syntax'=> "{{indexmenu>#1|js}}", - 'data' => array( - '', 'default', 'random', false, false, false, 0, false, '', false, false, - 0, false, false, false, 1, false, false, array(), 0, true, array(''), array(''), false, - ":start:,:same:,:inside:", 1 - ) - ), - //root ns, #levels=2, all not js specific options (nocookie is from context) - array( - 'syntax'=> "{{indexmenu>#2 test#6|navbar context tsort dsort msort hsort rsort nsort nons nopg}}", - 'data' => array( - '', 'default', 'random', true, true, false, 0, false, '&sort=t&msort=indexmenu_n&rsort=1&nsort=1&hsort=1&nopg=1', true, false, - 't', 'indexmenu_n', true, true, 2, true, true, array(array('test', 6)), 0, false, array(''), array(''), true, - ":start:,:same:,:inside:", 1 - ) - ), - //root ns, #levels=2, js renderer, all not js specific options - array( - 'syntax'=> "{{indexmenu>#2 test#6|navbar js#bj_ubuntu.png context tsort dsort msort hsort rsort nsort nons nopg}}", - 'data' => array( - '', 'bj_ubuntu.png', 'random', true, true, false, 0, false, '&sort=t&msort=indexmenu_n&rsort=1&nsort=1&hsort=1&nopg=1', true, false, - 't', 'indexmenu_n', true, true, 2, true, true, array(array('test', 6)), 0, true, array(''), array(''), true, - ":start:,:same:,:inside:", 1 - ), - ), - //root ns, #levels=1, all options - array( - 'syntax'=> "{{indexmenu>#1|navbar context nocookie noscroll notoc nomenu dsort msort#date:modified hsort rsort nsort nons nopg max#2#4 maxjs#3 id#54321}}", - 'data' => array( - '', 'default', 'random', true, true, true, 0, true, '&sort=d&msort=date modified&rsort=1&nsort=1&hsort=1&nopg=1', true, true, - 'd', 'date modified', true, true, 1, true, true, array(), 0, false, array(''), array(''), true, - ":start:,:same:,:inside:", 1 - ) - ), - //root ns, #levels=1, js renderer, all options - array( - 'syntax'=> "{{indexmenu>#1|js#bj_ubuntu.png navbar context nocookie noscroll notoc nomenu dsort msort#date:modified hsort rsort nsort nons nopg max#2#4 maxjs#3 id#54321}}", - 'data' => array( - '', 'bj_ubuntu.png', 54321, true, true, true, 3, true, '&sort=d&msort=date modified&rsort=1&nsort=1&hsort=1&nopg=1&max=4', true, true, - 'd', 'date modified', true, true, 1, true, true, array(), 2, true, array(''), array(''), true, - ":start:,:same:,:inside:", 1 - ) - ), - //root ns, #levels=1, skipfile and ns - - array( - 'syntax'=> "{{indexmenu>#1 test|skipfile+/(^myusers:spaces$|privatens:userss)/ skipns=/(^myusers:spaces$|privatens:users)/ id#ns}}", - 'data' => array( - '', 'default', 'random', false, false, false, 0, false, '&skipns=%3D/%28%5Emyusers%3Aspaces%24%7Cprivatens%3Ausers%29/&skipfile=%2B/%28%5Emyusers%3Aspaces%24%7Cprivatens%3Auserss%29/', false, false, - 0, false, false, false, 1, false, false, array(array('test', -1)), 0, false, array('/(^myusers:spaces$|privatens:users)/'), array('', '/(^myusers:spaces$|privatens:userss)/'), false, - ":start:,:same:,:inside:", 1 - ) - ), - //root ns, #levels=1, js renderer, skipfile and ns - array( - 'syntax'=> "{{indexmenu>#1 test|js skipfile=/(^myusers:spaces$|privatens:userss)/ skipns+/(^myusers:spaces$|privatens:userssss)/ id#ns}}", - 'data' => array( - '', 'default', 0, false, false, false, 0, false, '&skipns=%2B/%28%5Emyusers%3Aspaces%24%7Cprivatens%3Auserssss%29/&skipfile=%3D/%28%5Emyusers%3Aspaces%24%7Cprivatens%3Auserss%29/', false, false, - 0, false, false, false, 1, false, false, array(array('test', -1)), 0, true, array('', '/(^myusers:spaces$|privatens:userssss)/'), array('/(^myusers:spaces$|privatens:userss)/'), false, - ":start:,:same:,:inside:", 1 - ) - ) - ); - - foreach($tests as $test) { - $null = new Doku_Handler(); - $result = $plugin->handle($test['syntax'], 0, 40, $null); - - //copy unique generated number, which is about 23 characters - $len_id = strlen($result[1]['identifier']); - if(!is_numeric($test['data'][2]) && ($len_id > 20||$len_id<=23)) { - $test['data'][2] = $result[1]['identifier']; - } - $data = $this->createData($test['data']); - - $this->assertEquals($data, $result, 'Data array corrupted'); - } - } - - /** - * Rendering for nonexisting namespace - * expect: no paragraph due to no message set - * expect: one paragraph, since message set - * expect: contains namespace which replaced {{ns}} - * expect: message contained rendered italic syntax - */ - function testRenderEmptymsg() { - global $conf; - - $noexistns = 'nonexisting:namespace'; - $emptyindexsyntax = "{{indexmenu>$noexistns}}"; - - $xhtml = new Doku_Renderer_xhtml(); - $plugin = new syntax_plugin_indexmenu_indexmenu(); - - $null = new Doku_Handler(); - $result = $plugin->handle($emptyindexsyntax, 0, 10, $null); - - //no empty message - $plugin->render('xhtml', $xhtml, $result); - $doc = phpQuery::newDocument($xhtml->doc); - $this->assertEquals(0, pq('p', $doc)->length); - - // Fill in empty message - $conf['plugin']['indexmenu']['empty_msg'] = 'This namespace is //empty//: {{ns}}'; - $plugin->render('xhtml', $xhtml, $result); - $doc = phpQuery::newDocument($xhtml->doc); - - $this->assertEquals(1, pq('p', $doc)->length); - $this->assertEquals(1, pq("p:contains($noexistns)")->length); - $this->assertEquals(1, pq("p em")->length); - } - -} diff --git a/action.php b/action.php index 46af075..949ad0c 100644 --- a/action.php +++ b/action.php @@ -6,22 +6,36 @@ * @author Samuele Tognini */ -if(!defined('DOKU_INC')) die(); +use dokuwiki\Extension\Event; +use dokuwiki\Extension\EventHandler; +use dokuwiki\plugin\indexmenu\Search; +use dokuwiki\Ui\Index; +/** + * Class action_plugin_indexmenu + */ class action_plugin_indexmenu extends DokuWiki_Action_Plugin { /** * plugin should use this method to register its handlers with the dokuwiki's event controller * - * @param Doku_Event_Handler $controller DokuWiki's event controller object. + * @param EventHandler $controller DokuWiki's event controller object. */ - function register(Doku_Event_Handler $controller) { - if($this->getConf('only_admins')) $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, '_checkperm'); - if($this->getConf('page_index') != '') $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, '_loadindex'); - $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, '_extendJSINFO'); - $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, '_purgecache'); - if($this->getConf('show_sort')) $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, '_showsort'); - $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, '_ajax_call'); + public function register(EventHandler $controller) { + if($this->getConf('only_admins')) { + $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'removeSyntaxIfNotAdmin'); + } + if($this->getConf('page_index') != '') { + $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'loadOwnIndexPage'); + } + $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'extendJSINFO'); + $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'purgeCache'); + if($this->getConf('show_sort')) { + $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'showSortNumberAtTopOfPage'); + } + $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'ajaxCalls'); +// $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'getDataFancyTree'); + $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addStylesForUsedThemes'); } /** @@ -29,10 +43,9 @@ function register(Doku_Event_Handler $controller) { * * @author Samuele Tognini * - * @param Doku_Event $event - * @param mixed $param not defined + * @param Event $event */ - function _checkperm(&$event, $param) { + public function removeSyntaxIfNotAdmin(Event $event) { global $INFO; if(!$INFO['isadmin']) { $event->data[0][1] = preg_replace("/{{indexmenu(|_n)>.+?}}/", "", $event->data[0][1]); @@ -45,10 +58,9 @@ function _checkperm(&$event, $param) { * @author Samuele Tognini * @author Gerrit Uitslag * - * @param Doku_Event $event - * @param mixed $param not defined + * @param Event $event */ - function _extendJSINFO(&$event, $param) { + public function extendJSINFO(Event $event) { global $INFO, $JSINFO; $JSINFO['isadmin'] = (int) $INFO['isadmin']; $JSINFO['isauth'] = (int) $INFO['userinfo']; @@ -59,12 +71,14 @@ function _extendJSINFO(&$event, $param) { * * @author Samuele Tognini * - * @param Doku_Event $event - * @param mixed $param not defined + * @param Event $event */ - function _purgecache(&$event, $param) { + public function purgeCache(Event $event) { global $ID; global $conf; + global $INPUT; + global $INFO; + /** @var cache_parser $cache */ $cache = &$event->data; @@ -72,18 +86,21 @@ function _purgecache(&$event, $param) { //purge only xhtml cache if($cache->mode != "xhtml") return; //Check if it is an indexmenu page - if(!p_get_metadata($ID, 'indexmenu')) return; + if(!p_get_metadata($ID, 'indexmenu hasindexmenu')) return; $aclcache = $this->getConf('aclcache'); if($conf['useacl']) { $newkey = false; if($aclcache == 'user') { //Cache per user - if($_SERVER['REMOTE_USER']) $newkey = $_SERVER['REMOTE_USER']; + if($INPUT->server->str('REMOTE_USER')) { + $newkey = $INPUT->server->str('REMOTE_USER'); + } } else if($aclcache == 'groups') { //Cache per groups - global $INFO; - if($INFO['userinfo']['grps']) $newkey = implode('#', $INFO['userinfo']['grps']); + if(isset($INFO['userinfo']['grps'])) { + $newkey = implode('#', $INFO['userinfo']['grps']); + } } if($newkey) { $cache->key .= "#".$newkey; @@ -103,14 +120,15 @@ function _purgecache(&$event, $param) { * * @author Samuele Tognini * - * @param Doku_Event $event - * @param mixed $param not defined + * @param Event $event */ - function _loadindex(&$event, $param) { + public function loadOwnIndexPage(Event $event) { if('index' != $event->data) return; if(!file_exists(wikiFN($this->getConf('page_index')))) return; + global $lang; - print '

'.$lang['btn_index']."

\n"; + + print '

'.$lang['btn_index']."

\n"; print p_wiki_xhtml($this->getConf('page_index')); $event->preventDefault(); $event->stopPropagation(); @@ -122,16 +140,15 @@ function _loadindex(&$event, $param) { * * @author Samuele Tognini * - * @param Doku_Event $event - * @param mixed $param not defined + * @param Event $event */ - function _showsort(&$event, $param) { + public function showSortNumberAtTopOfPage(Event $event) { global $ID, $ACT, $INFO; if($INFO['isadmin'] && $ACT == 'show') { if($n = p_get_metadata($ID, 'indexmenu_n')) { - ptln('
'); - ptln($this->getLang('showsort').$n); - ptln('
'); + echo '
'; + echo $this->getLang('showsort') . $n; + echo '
'; } } } @@ -139,10 +156,9 @@ function _showsort(&$event, $param) { /** * Handles ajax requests for indexmenu * - * @param Doku_Event $event - * @param mixed $param not defined + * @param Event $event */ - function _ajax_call(&$event, $param) { + public function ajaxCalls(Event $event) { if($event->data !== 'indexmenu') { return; } @@ -150,28 +166,137 @@ function _ajax_call(&$event, $param) { $event->stopPropagation(); $event->preventDefault(); - switch($_REQUEST['req']) { + global $INPUT; + switch($INPUT->str('req')) { case 'local': //list themes - header('Content-Type: application/json'); - - $data = $this->_getlocalThemes(); - - // require_once DOKU_INC.'inc/JSON.php'; - $json = new JSON(); - echo ''.$json->encode($data).''; + $this->getlocalThemes(); break; case 'toc': //print toc preview - if(isset($_REQUEST['id'])) print $this->print_toc($_REQUEST['id']); + if($INPUT->has('id')) { + print $this->printToc($INPUT->str('id')); + } break; case 'index': - //print index - if(isset($_REQUEST['idx'])) print $this->print_index($_REQUEST['idx']); + //retrieval of data of the extra nodes for the indexmenu (if ajax loading set with max#m(#n) + if($INPUT->has('idx')) { + print $this->printIndex($INPUT->str('idx')); + } + break; + + case 'fancytree': + //data for new index build with Fancytree + $this->getDataFancyTree(); break; } + } + + /** + * Handles ajax requests for FancyTree + * + * @return void + */ + private function getDataFancyTree() { + global $INPUT; + + +// $idxm = new syntax_plugin_indexmenu_indexmenu(); +// $ns = $idxm->parseNs(rawurldecode($ns)); // why not assuming a 'key' is offered? + $ns = $INPUT->str('ns',''); + $ns = rtrim($ns,':'); //key of directory has extra : on the end + $level = -1; //opened levels. -1=all levels open + $max = 1; //levels to load by lazyloading. Before the default was 0. CHANGED to 1. + $skipFile = []; + $skipNs = []; + + if($INPUT->int('max') > 0) { + $max = $INPUT->int('max'); // max#n#m, if init: #n, otherwise #m + $level = $max; + } + if($INPUT->int('level',-10) >= -1) { + $level = $INPUT->int('level'); + } + $isInit = $INPUT->bool('init'); + + $currentPage = $INPUT->str('currentpage'); + if($isInit) { //TODO attention, depends on logic that js is only 1 if init + $subnss = $INPUT->arr('subnss'); + $debug1=var_export($subnss,true); + // if 'navbar' enabled add current ns to list + if($INPUT->bool('navbar')) { + $subnss[] = [getNS($currentPage)]; + } + $debug2=var_export($subnss,true); + // alternative, via javascript.. https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree.html#loadKeyPath + } else { + $subnss = $INPUT->str('subnss'); + $subnss = [[cleanID($subnss), 1]]; + } + + $skipf = $INPUT->str('skipfile'); // utf8_decodeFN($_REQUEST['skipfile']); + $skipFile[] = $this->getConf('skip_file'); + if(!empty($skipf)) { + $index = 0; + //prefix is '=' or '+' + if($skipf[1] == '+') { + $index = 1; + } + $skipFile[$index] = substr($skipf, 1); + } + $skipn = $INPUT->str('skipns'); //utf8_decodeFN($_REQUEST['skipns']); + $skipNs[] = $this->getConf('skip_index'); + if(!empty($skipn)) { + $index = 0; + //prefix is '=' or '+' + if($skipn[1] == '+') { + $index = 1; + } + $skipNs[$index] = substr($skipn, 1); + } + + $opts = array( + 'level' => $level, //only set for init, lazy requests equal to max + 'nons' => $INPUT->bool('nons'), //only needed for init + 'nopg' => $INPUT->bool('nopg'), + 'subnss' => $subnss, //init with complex array, only current ns if lazy + 'max' => $max, + 'js' => false, //DEPRECATED (for dTree: only init true, lazy requests false.) NOW not used, so false. + 'skipns' => $skipNs, //preprocessed to string, only part from syntax + 'skipfile' => $skipFile, //preprocessed to string, only part from syntax + 'headpage' => $this->getConf('headpage'), + 'hide_headpage' => $this->getConf('hide_headpage'), + ); + + $sort = [ + 'sort' => $INPUT->str('sort'), + 'msort' => $INPUT->str('msort'), + 'rsort' => $INPUT->bool('rsort'), + 'nsort' => $INPUT->bool('nsort'), + 'hsort' => $INPUT->bool('hsort') + ]; + + $search = new Search($sort); + $data = $search->search($ns, $opts); + $fancytreeData = $search->buildFancytreeData($data, $isInit, $currentPage); + + if($isInit) { + //for lazy loading are other items than children not supported. + $fancytreeData['opts'] = $opts; + $fancytreeData['sort'] = $sort; +// $fancytreeData['navbar'] = $INPUT->bool('navbar'); +// $fancytreeData['debug1'] = $debug1; +// $fancytreeData['debug2'] = $debug2; + + } else { + $fancytreeData[0]['opts'] = $opts; + $fancytreeData[0]['sort'] = $sort; + } + + header('Content-Type: application/json'); + echo json_encode($fancytreeData); } @@ -181,7 +306,9 @@ function _ajax_call(&$event, $param) { * @author Samuele Tognini * @author Gerrit Uitslag */ - private function _getlocalThemes() { + private function getlocalThemes() { + header('Content-Type: application/json'); + $themebase = 'lib/plugins/indexmenu/images'; $handle = @opendir(DOKU_INC.$themebase); @@ -200,21 +327,22 @@ private function _getlocalThemes() { closedir($handle); sort($themes); - return array( + echo json_encode([ 'themebase' => $themebase, 'themes' => $themes - ); - + ]); } /** * Print a toc preview * + * @param string $id + * @return string + * * @author Samuele Tognini * @author Andreas Gohr */ - function print_toc($id) { - require_once(DOKU_INC.'inc/parser/xhtml.php'); + private function printToc($id) { $id = cleanID($id); if(auth_quickaclcheck($id) < AUTH_READ) return ''; @@ -223,10 +351,10 @@ function print_toc($id) { if(count($toc) > 1) { //display ToC of two or more headings - $out = $this->render_toc($toc); + $out = $this->renderToc($toc); } else { //display page abstract - $out = $this->render_abstract($id, $meta); + $out = $this->renderAbstract($id, $meta); } return $out; } @@ -234,41 +362,51 @@ function print_toc($id) { /** * Return the TOC rendered to XHTML * + * @param $toc + * @return string + * * @author Andreas Gohr * @author Gerrit Uitslag */ - function render_toc($toc) { + private function renderToc($toc) { global $lang; - $out = '
'.DOKU_LF; + $out = '
'; $out .= $lang['toc']; - $out .= '
'.DOKU_LF; - $out .= '
'.DOKU_LF; - $out .= html_buildlist($toc, 'toc', array($this, '_indexmenu_list_toc'), 'html_li_default', true); - $out .= '
'.DOKU_LF; + $out .= '
'; + $out .= '
'; + $out .= html_buildlist($toc, 'toc', [$this, 'formatIndexmenuListTocItem'], null, true); + $out .= '
'; return $out; } /** * Return the page abstract rendered to XHTML + * + * @param $id + * @param array $meta by reference + * @return string */ - function render_abstract($id, &$meta) { - $out = '
'.DOKU_LF; + private function renderAbstract($id, $meta) { + $out = ''.DOKU_LF; + $out .= ''; + $out .= '
'; if($meta['description']['abstract']) { - $out .= '
'.DOKU_LF; + $out .= '
'; $out .= p_render('xhtml', p_get_instructions($meta['description']['abstract']), $info); - $out .= '
'.DOKU_LF.'
'.DOKU_LF; + $out .= '
'; } return $out; } /** * Callback for html_buildlist + * + * @param $item + * @return string */ - function _indexmenu_list_toc($item) { + public function formatIndexmenuListTocItem($item) { $id = cleanID($_REQUEST['id']); if(isset($item['hid'])) { @@ -287,45 +425,48 @@ function _indexmenu_list_toc($item) { /** * Print index nodes * + * @param $ns + * @return string + * + * @author Rene Hadler * @author Samuele Tognini * @author Andreas Gohr - * @author Rene Hadler */ - function print_index($ns) { - require_once(DOKU_PLUGIN.'indexmenu/syntax/indexmenu.php'); - global $conf; + private function printIndex($ns) { + global $conf, $INPUT; $idxm = new syntax_plugin_indexmenu_indexmenu(); - $ns = $idxm->_parse_ns(rawurldecode($ns)); + $ns = $idxm->parseNs(rawurldecode($ns)); $level = -1; $max = 0; $data = array(); $skipfile = array(); $skipns = array(); - if($_REQUEST['max'] > 0) { - $max = $_REQUEST['max']; + if($INPUT->int('max') > 0) { + $max = $INPUT->int('max'); $level = $max; } - $nss = ($_REQUEST['nss']) ? cleanID($_REQUEST['nss']) : ''; - $idxm->sort = $_REQUEST['sort']; - $idxm->msort = $_REQUEST['msort']; - $idxm->rsort = $_REQUEST['rsort']; - $idxm->nsort = $_REQUEST['nsort']; - $idxm->hsort = $_REQUEST['hsort']; + $nss = $INPUT->str('nss','', true); + $sort['sort'] = $INPUT->str('sort', '', true); + $sort['msort'] = $INPUT->str('msort', '', true); + $sort['rsort'] = $INPUT->bool('rsort', false, true); + $sort['nsort'] = $INPUT->bool('nsort', false, true); + $sort['hsort'] = $INPUT->bool('hsort', false, true); + $search = new Search($sort); $fsdir = "/".utf8_encodeFN(str_replace(':', '/', $ns)); - $skipf = utf8_decodeFN($_REQUEST['skipfile']); + $skipf = utf8_decodeFN($INPUT->str('skipfile')); $skipfile[] = $this->getConf('skip_file'); - if(isset($skipf)) { + if(!empty($skipf)) { $index = 0; if($skipf[1] == '+') { $index = 1; } $skipfile[$index] = substr($skipf, 1); } - $skipn = utf8_decodeFN($_REQUEST['skipns']); + $skipn = utf8_decodeFN($INPUT->str('skipns')); $skipns[] = $this->getConf('skip_index'); - if(isset($skipn)) { + if(!empty($skipn)) { $index = 0; if($skipn[1] == '+') { $index = 1; @@ -335,33 +476,72 @@ function print_index($ns) { $opts = array( 'level' => $level, - 'nons' => $_REQUEST['nons'], + 'nons' => $INPUT->bool('nons', false, true), 'nss' => array(array($nss, 1)), 'max' => $max, 'js' => false, - 'nopg' => $_REQUEST['nopg'], - 'skip_index' => $skipns, - 'skip_file' => $skipfile, + 'nopg' => $INPUT->bool('nopg', false, true), + 'skipns' => $skipns, + 'skipfile' => $skipfile, 'headpage' => $idxm->getConf('headpage'), 'hide_headpage' => $idxm->getConf('hide_headpage') ); - if($idxm->sort || $idxm->msort || $idxm->rsort || $idxm->hsort) { - $idxm->_search($data, $conf['datadir'], array($idxm, '_search_index'), $opts, $fsdir); + if($sort['sort'] || $sort['msort'] || $sort['rsort'] || $sort['hsort']) { + $search->customSearch($data, $conf['datadir'], array($search, 'searchIndexmenuItems'), $opts, $fsdir); } else { - search($data, $conf['datadir'], array($idxm, '_search_index'), $opts, $fsdir); + search($data, $conf['datadir'], array($search, 'searchIndexmenuItems'), $opts, $fsdir); } $out = ''; - if($_REQUEST['nojs']) { - require_once(DOKU_INC.'inc/html.php'); - $out_tmp = html_buildlist($data, 'idx', array($idxm, "_html_list_index"), "html_li_index"); + if($INPUT->int('nojs') === 1) { + $idx = new Index(); + $out_tmp = html_buildlist($data, 'idx', [$idxm, 'formatIndexmenuItem'], [$idx, 'tagListItem']); $out .= preg_replace('/
    (.*)<\/ul>/s', "$1", $out_tmp); } else { - $nodes = $idxm->_jsnodes($data, '', 0); + $nodes = $idxm->builddTreeNodes($data, '', false); $out = "ajxnodes = ["; $out .= rtrim($nodes[0], ","); $out .= "];"; } return $out; } -} \ No newline at end of file + + /** + * Add Js & Css after template is displayed + * + * @param Event $event + */ + public function addStylesForUsedThemes(Event $event) + { + global $ID; + + p_get_metadata($ID, 'indexmenu hasindexmenu'); + + if (($themes = p_get_metadata($ID, 'indexmenu usedthemes')) !== null) { //METADATA_RENDER_UNLIMITED + $themes = array_keys($themes); + } else { + $themes = []; + } + + //TODO works only on the main pages (where we can get its $ID) sidebars and other included pages we miss here +// foreach ($themes as $theme) { +// $event->data["link"][] = [ +// "type" => "text/css", +// "rel" => "stylesheet", +// "href" => DOKU_BASE . "lib/plugins/indexmenu/scripts/fancytree/skin-$theme/ui.fancytree.min.css" +// ]; +// } + +// $event->data["link"][] = [ +// "type" => "text/css", +// "rel" => "stylesheet", +// "href" => "//fonts.googleapis.com/icon?family=Material+Icons" +// ]; + +// $event->data["link"][] = [ +// "type" => "text/css", +// "rel" => "stylesheet", +// "href" => "//code.getmdl.io/1.3.0/material.indigo-pink.min.css" +// ]; + } +} diff --git a/admin.php b/admin.php index 6dfbe64..da13cf8 100644 --- a/admin.php +++ b/admin.php @@ -186,6 +186,7 @@ private function dolist($n) { * * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) * @author Samuele Tognini + */ private function install($n, $name) { $repo = $this->repos['url'][$n]; @@ -261,6 +262,7 @@ private function checktmpsubdir() { * @param string $theme * @param string $info * @return bool + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) * @author Samuele Tognini */ diff --git a/ajax.php b/ajax.php index c1ecb15..897d0c1 100644 --- a/ajax.php +++ b/ajax.php @@ -22,13 +22,15 @@ $ajax_indexmenu = new ajax_indexmenu_plugin; $ajax_indexmenu->render(); +/** + * Class ajax_indexmenu_plugin + */ class ajax_indexmenu_plugin { /** * Output * * @author Samuele Tognini */ - function render() { $req = $_REQUEST['req']; $succ = false; @@ -38,7 +40,7 @@ function render() { $repo = new repo_indexmenu_plugin; $succ = $repo->send_theme($_REQUEST['t']); } - if($succ) return true; + if($succ) return; header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: public, max-age=3600'); @@ -48,23 +50,14 @@ function render() { //list themes print $this->local_themes(); break; - /*case 'toc': - //print toc preview - if(isset($_REQUEST['id'])) print $this->print_toc($_REQUEST['id']); - break; - case 'index': - //print index - if(isset($_REQUEST['idx'])) print $this->print_index($_REQUEST['idx']); - break; */ } } /** * Print a list of local themes - * + * TODO: delete this funstion; copy of this function is already in action.php * @author Samuele Tognini */ - function local_themes() { $list = 'indexmenu,'.DOKU_URL.",lib/plugins/indexmenu/images,"; $data = array(); @@ -85,118 +78,4 @@ function local_themes() { $list .= implode(",", $data); return $list; } - - /** - * Print a toc preview - * - * @author Samuele Tognini - * @author Andreas Gohr - */ - function print_toc($id) { - require_once(DOKU_INC.'inc/parser/xhtml.php'); - $id = cleanID($id); - if(auth_quickaclcheck($id) < AUTH_READ) return ''; - $meta = p_get_metadata($id); - $toc = $meta['description']['tableofcontents']; - $out = '
    '.DOKU_LF; - if(count($toc) > 1) { - $out .= $this->render_toc($toc); - } else { - $out .= ''; - $out .= ($meta['title']) ? htmlspecialchars($meta['title']) : htmlspecialchars(noNS($id)); - $out .= ''.DOKU_LF; - if($meta['description']['abstract']) { - $out .= '
    '.DOKU_LF; - $out .= '
    '.DOKU_LF; - $out .= p_render('xhtml', p_get_instructions($meta['description']['abstract']), $info); - $out .= '
    '.DOKU_LF; - } - } - $out .= ''.DOKU_LF; - return $out; - } - - /** - * Return the TOC rendered to XHTML - * - * @author Andreas Gohr - */ - function render_toc($toc) { - global $lang; - $r = new Doku_Renderer_xhtml; - $r->toc = $toc; - - $out = $lang['toc']; - $out .= ''.DOKU_LF; - $out .= '
    '.DOKU_LF; - $out .= html_buildlist($r->toc, 'toc', array($this, '_tocitem')); - $out .= '
    '.DOKU_LF; - return $out; - } - - /** - * Callback for html_buildlist - */ - function _tocitem($item) { - $id = cleanID($_POST['id']); - return ''. - htmlspecialchars($item['title']).''; - } - - /** - * Print index nodes - * - * @author Samuele Tognini - * @author Andreas Gohr - * @author Rene Hadler - */ - function print_index($ns) { - require_once(DOKU_PLUGIN.'indexmenu/syntax/indexmenu.php'); - global $conf; - $idxm = new syntax_plugin_indexmenu_indexmenu(); - $ns=$idxm->_parse_ns(rawurldecode($ns)); - $level = -1; - $max = 0; - $data = array(); - $out = ''; - if($_REQUEST['max'] > 0) { - $max = $_REQUEST['max']; - $level = $max; - } - $nss = ($_REQUEST['nss']) ? cleanID($_REQUEST['nss']) : ''; - $idxm->sort = $_REQUEST['sort']; - $idxm->msort = $_REQUEST['msort']; - $idxm->rsort = $_REQUEST['rsort']; - $idxm->nsort = $_REQUEST['nsort']; - $idxm->hsort = $_REQUEST['hsort']; - $fsdir = "/".utf8_encodeFN(str_replace(':', '/', $ns)); - $opts = array( - 'level' => $level, - 'nons' => $_REQUEST['nons'], - 'nss' => array(array($nss, 1)), - 'max' => $max, - 'js' => false, - 'nopg' => $_REQUEST['nopg'], - 'skip_index' => $idxm->getConf('skip_index'), - 'skip_file' => $idxm->getConf('skip_file'), - 'headpage' => $idxm->getConf('headpage'), - 'hide_headpage' => $idxm->getConf('hide_headpage') - ); - if($idxm->sort || $idxm->msort || $idxm->rsort || $idxm->hsort) { - $idxm->_search($data, $conf['datadir'], array($idxm, '_search_index'), $opts, $fsdir); - } else { - search($data, $conf['datadir'], array($idxm, '_search_index'), $opts, $fsdir); - } - if($_REQUEST['nojs']) { - require_once(DOKU_INC.'inc/html.php'); - $out_tmp = html_buildlist($data, 'idx', array($idxm, "_html_list_index"), "html_li_index"); - $out .= preg_replace('/
      (.*)<\/ul>/s', "$1", $out_tmp); - } else { - $nodes = $idxm->_jsnodes($data, '', 0); - $out = "ajxnodes = ["; - $out .= rtrim($nodes[0], ","); - $out .= "];"; - } - return $out; - } } diff --git a/all.less b/all.less new file mode 100644 index 0000000..632d4c9 --- /dev/null +++ b/all.less @@ -0,0 +1,43 @@ +//The data-uri() links in skin-common.less break. Needs to be replaced by url(), DokuWiki can inline if needed + +//moved from skin-common.less to here to prevent wrong prefixing and renamed from spin to spin-fancytree +@keyframes spin-fancytree { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } +} + + + +//Mixins +// note: import of skin-common.less in the imported file below works only if skin-common.less is copied to EACH skin +// folder and referred from its ui.fancytree.less respectively. +.importSkin(@skin-foldername) { + &.@{skin-foldername} { + @import "scripts/fancytree/@{skin-foldername}/ui.fancytree.less"; + //overwrite default variable: @fancy-image-prefix: "./skin-win8/"; the current less compressor does not update paths + @fancy-image-prefix: "/lib/plugins/indexmenu/scripts/fancytree/@{skin-foldername}/"; + } +} + +//wrap everything by plugin class to ensure its dominates default dokuwiki paddings etc. +.indexmenu_js2 { + //workaround needed for LESS processor of DokuWiki + .setBgImageUrl(@url) when not (@fancy-use-sprites) {} + .useSprite(@x, @y) when not(@fancy-use-sprites) {} + + .importSkin(skin-awesome); + .importSkin(skin-bootstrap); + .importSkin(skin-bootstrap-n); + .importSkin(skin-lion); + .importSkin(skin-material); + .importSkin(skin-mdi); + .importSkin(skin-vista); + .importSkin(skin-win7); + .importSkin(skin-win8); + .importSkin(skin-xp); + .importSkin(skin-typicons); +} diff --git a/script.js b/script.js index 239f8e1..c561c11 100644 --- a/script.js +++ b/script.js @@ -9,6 +9,366 @@ var indexmenu_contextmenu = {'all': []}; /* DOKUWIKI:include scripts/contextmenu.local.js */ +/* DOKUWIKI:include scripts/fancytree/jquery.fancytree-all.min.js */ +// function logEvent(event, data, msg){ +// // var args = Array.isArray(args) ? args.join(", ") : +// msg = msg ? ": " + msg : ""; +// jQuery.ui.fancytree.info("Event('" + event.type + "', node=" + data.node + ")" + msg); +// } +jQuery(function(){ // on page load + // Create the tree inside the
      element. + const predefinedPresets = { + 'bootstrap': { //works with template bootstrap3 or by manually adding resources to icon plugin assets + 'preset': 'bootstrap3', + 'map': {} + }, + 'bootstrap-n': { //works with template bootstrap3 or ..etc + 'preset': 'bootstrap3', + 'map': {} + }, + 'awesome': { //works with icons-plugin, settings: enable plugin»icons»loadFontAwesome + 'preset': 'awesome4', //plugin icons does include only awesome4, not awesome5. + 'map': {} + }, + 'material': { // add Material Icons font stylesheet to header with TPL_METAHEADER_OUTPUT in action component + 'preset': 'material', + 'map': {} + }, + 'mdi': { //works with icons-plugin, settings: enable plugin»icons»loadMaterialDesignIcons + + 'preset': '', + 'map': { + _addClass: "mdi", + checkbox: "mdi-checkbox-blank-outline", + checkboxSelected: "mdi-check-box-outline", + checkboxUnknown: "mdi-checkbox-intermediate fancytree-helper-indeterminate-cb", + dragHelper: "mdi-play", + dropMarker: "mdi-skip-forward", + error: "mdi-warning", + expanderClosed: "mdi-chevron-right", + expanderLazy: "mdi-chevron-right", + expanderOpen: "mdi-chevron-down", + // We may prevent wobbling rotations on FF by creating a separate sub element: + loading: "mdi-refresh", + nodata: "mdi-information-outline", + noExpander: "", + radio: "mdi-radiobox-blank", // "fa-circle-o" + radioSelected: "mdi-radiobox-marked", + // Default node icons. + // (Use tree.options.icon callback to define custom icons based on node data) + doc: "mdi-file-outline", + docOpen: "mdi-file-outline", + folder: "mdi-folder", + folderOpen: "mdi-folder-open", + } + }, + 'typicons': { //works with icons-plugin, settings: enable plugin»icons»loadTypicons + 'preset': '', + 'map': { + _addClass: "typcn", + checkbox: "typcn-media-stop-outline", + checkboxSelected: "typcn-input-checked", + checkboxUnknown: "typcn-media-stop-outline fancytree-helper-indeterminate-cb", + dragHelper: "typcn-media-play-outline", + dropMarker: "typcn-media-fast-forward-outline", + error: "typcn-warning", + expanderClosed: "typcn-media-play", + expanderLazy: "typcn-media-play", + expanderOpen: "typcn-arrow-sorted-down", + // We may prevent wobbling rotations on FF by creating a separate sub element: + loading: "typcn-arrow-sync", + nodata: "typcn-info-large", + noExpander: "", + radio: "typcn-media-record-outline", // "fa-circle-o" + radioSelected: "typcn-media-record", + // Default node icons. + // (Use tree.options.icon callback to define custom icons based on node data) + doc: "typcn-document", + docOpen: "typcn-document", + folder: "typcn-folder", + folderOpen: "typcn-folder-open", + } + } + + }; + // userDefinedPresets can be defined in conf/userscript.js + const presets = {...predefinedPresets, ...(typeof userDefinedPresets === 'undefined' ? [] : userDefinedPresets)}; + + jQuery(".indexmenu_js2").each(function(){ + let $tree = jQuery(this), + id = $tree.attr('id'); + var options = $tree.data('options'); +console.log(options); + + $tree.fancytree({ + extensions: presets[options.opts.theme] ? ["glyph"] : [], + glyph: { + preset: presets[options.opts.theme] ? presets[options.opts.theme].preset : '', + map: presets[options.opts.theme] ? presets[options.opts.theme].map : {} + }, + //minExpandLevel: 2, // number of levels already expanded, and not unexpandable. + clickFolderMode: 3, // expand with single click instead of dblclick + //autoCollapse: true, //closes other opened nodes, so only one node is opened + // autoScroll: true, // for keyboard.. --opening folders becomes jumpy + autoActivate: false, // we use scheduleAction(). Otherwise, looping in combination with clicking + activeVisible: false, + // tooltip: function(event, data) { + // return data.node.title; + // }, + escapeTitles: false, + tooltip: true, + rtl: jQuery('html[dir=rtl]').length, + focus: function(event, data) { + var node = data.node; + // Auto-activate focused node after 1 second (practical for use with keyboard) + if(node.key){ + node.scheduleAction("activate", 1000); + } + }, + blur: function(event, data) { + data.node.scheduleAction("cancel"); + }, + // click: function(event, data) { //just for logging info(testing) + // logEvent(event, data, ", targetType=" + data.targetType); + // // return false to prevent default behavior (i.e. activation, ...) + // //return false; + // }, + activate: function(event, data){ + var node = data.node, + orgEvent = data.originalEvent; + + //prevent looping + if(node.key === JSINFO.id) { + //node is equal to current page + return + } + if(!node.folder) { + url = DOKU_BASE + node.key + } else if(node.data.hns === false) { + return false; + } else { + url = DOKU_BASE + node.data.hns + } + console.log(url); + if(url){ + //window.open(node.data.href, (orgEvent.ctrlKey || orgEvent.metaKey) ? "_blank" /*node.data.target*/ : node.data.target); + window.location.href=url; + } + }, + init: function(event, data) { + data.tree.reactivate(); + }, + enhanceTitle: function(event, data) { + let url, node = data.node; + // console.log('enhanceTitle'); + // console.log(data.node); + // console.log(data.$title); + if(!node.folder) { + url = DOKU_BASE + node.key + } else if(node.data.hns === false) { + return; + } else { + url = DOKU_BASE + node.data.hns + } + data.$title.html("" + node.title + ""); + }, + source: { + url: DOKU_BASE + 'lib/exe/ajax.php', + data: { + ns: options.ns, + call: 'indexmenu', + req: 'fancytree', + + level: options.opts.level, //only init + nons: options.opts.nons ? 1 : 0, //only init; without ns, no lower levels possible + nopg: options.opts.nopg ? 1 : 0, + subnss: options.opts.subnss, //subns to open. Only on init array, later just current ns string + navbar: options.opts.navbar ? 1 : 0, //only init: open tree at current page + currentpage: JSINFO.id, + max: options.opts.max, //#n of max#n#m + //js: 1,//options.opts.js, //only init true, later false + skipns: options.opts.skipns, + skipfile: options.opts.skipfile, + sort: options.sort.sort ? options.sort.sort : 0, //'t', 'd', false TODO is false handled correctly? + msort: options.sort.msort ? options.sort.msort : 0, //'indexmenu_n', or metadata 'key subkey' TODO is empty handled correctly? + rsort: options.sort.rsort ? 1 : 0, + nsort: options.sort.nsort ? 1 : 0, + hsort: options.sort.hsort ? 1 : 0, + + init: 1 + } + }, + lazyLoad: function(event, data) { + var node = data.node; + // Issue an Ajax request to load child nodes + data.result = { + url: DOKU_BASE + 'lib/exe/ajax.php', //TODO reminder: after adding node.key to subnss and maxajax loading is incomplete for ns3 + data: { + ns: node.key, // ns with trailing : + call: 'indexmenu', + req: 'fancytree', + + level: 1, //level opened nodes, for follow up ajax requests only next level, so:1 + //nons: options.opts.nons ? 1 : 0, //todo: sets text false + nopg: options.opts.nopg ? 1 : 0, + subnss: '',//node.key,//options.opts.subnss, //TODO only string of current ns, that should be opened (use this only for navbar!) + currentpage: JSINFO.id, + max: options.opts.maxajax, //#m of max#n#m + //js: 0, //options.opts.js, //original: only true needed if init + skipns: options.opts.skipns, + skipfile: options.opts.skipfile, + sort: options.sort.sort ? options.sort.sort : 0, + msort: options.sort.msort ? options.sort.msort : 0, + rsort: options.sort.rsort ? 1 : 0, + nsort: options.sort.nsort ? 1 : 0, + hsort: options.sort.hsort ? 1 : 0, + + init: 0 + } + } + } + }); + + //hide the fallback nojs indexmenu + jQuery('#nojs_' + id.substring(6)).css("display", "none"); + + + // Note: Loading and initialization may be asynchronous, so the nodes may not be accessible yet. + + // On page load, activate node if node.data.href matches the url#href + let tree = jQuery.ui.fancytree.getTree("#" + id), + path = window.parent && window.parent.location.pathname; +console.log(path); +console.log('test'); + if(path) { + let arr = path.split('/'); // not reliable with config:useslash? + let last = arr[arr.length-1] || arr[arr.length-2]; + console.log(arr); + console.log(last); + + // tree.activateKey(last); + // var node1=tree.getNodeByKey(last); + // console.log(node1); + // node1.setActive(); + // also possible: + // $.ui.fancytree.getTree("#tree").getNodeByKey("id4.3.2").setActive(); + + // tree.visit(function(n) { + // console.log(n.key); + // console.log(n); + // if( n.key && n.key === last ) { + // n.setActive(); //if not using iframes, this creates a loops in combination with activate above + // return false; // done: break traversal + // } + // }); + } +// console.log(tree); +// console.log("test"); +// jQuery.contextMenu({ +// selector: "span.fancytree-title", +// items: { +// // "cut": {name: "Cut", icon: "cut", +// // callback: function(key, opt){ +// // var node = jQuery.ui.fancytree.getNode(opt.$trigger); +// // alert("Clicked on " + key + " on " + node); +// // } +// // }, +// "page": {name: "Page", icon: "", disabled: true }, +// "sep1": "----", +// "revs": {name: "Revisions", icon: "ui-icon-arrowreturn-1-w", disabled: false }, +// "toc": {name: "ToC preview", icon: "ui-icon-bookmark", disabled: false }, +// "edit": {name: "Edit", icon: "edit", disabled: false }, +// "hpage": {name: "Headpage", icon: "add", disabled: false}, +// "spage": {name: "Start page", icon: "add", disabled: false}, +// "cpage": {name: "Custom page...", icon: "add", disabled: false}, +// "acls": {name: "Acls", icon: "ui-icon-locked", disabled: false}, +// "purge": {name: "Purge cache", icon: "loading", disabled: false}, +// "html": {name: "Export as HTML", icon: "ui-icon-document", disabled: false}, +// "text": {name: "Export as text", icon: "ui-icon-note", disabled: false}, +// "sep2": "----", +// "ns": {name: "Namespace", icon: "", disabled: true}, +// "sep3": "----", +// "search": {name: "Search...", icon: "ui-icon-search", disabled: false}, +// "npage": {name: "New page...", icon: "add", disabled: false}, +// "nshpage": {name: "Headpage here", icon: "add", disabled: false}, +// "nsacls": {name: "Acls", icon: "ui-icon-locked", disabled: false} +// }, +// callback: function(itemKey, opt) { +// var node = jQuery.ui.fancytree.getNode(opt.$trigger); +// alert("select " + itemKey + " on " + node); +// } +// }); + + // $tree.contextmenu({ + // delegate: "span.fancytree-title", + // autoFocus: true, + // // menu: "#options", + // menu: [ + // {title: "Page", cmd: 'pg'}, + // {title: "----", cmd: 'pg'}, + // {title: "Revisions", cmd: "revs", uiIcon: "ui-icon-arrowreturn-1-w"}, + // {title: "ToC preview", cmd: "toc", uiIcon: "ui-icon-bookmark"}, + // {title: "Edit", cmd: "edit", uiIcon: "ui-icon-pencil", disabled: false }, + // {title: "Headpage", cmd: "hpage", uiIcon: "ui-icon-plus"}, + // {title: "Start page", cmd: "spage", uiIcon: "ui-icon-plus"}, + // {title: "Custom page...", cmd: "cpage", uiIcon: "ui-icon-plus"}, + // {title: "Acls", cmd: "acls", uiIcon: "ui-icon-locked", disabled: true }, + // {title: "Purge cache", cmd: "purge", uiIcon: "ui-icon-arrowrefresh-1-e"}, + // {title: "Export as HTML", cmd: "html", uiIcon: "ui-icon-document"}, + // {title: "Export as text", cmd: "text", uiIcon: "ui-icon-note"}, + // {title: "Namespace", cmd:'ns'}, + // {title: "----", cmd:'ns'}, + // {title: "Search...", cmd: "search", uiIcon: "ui-icon-search"}, + // {title: "New page...", cmd: "npage", uiIcon: "ui-icon-plus"},// children:[] + // {title: "Headpage here", cmd: "nshpage", uiIcon: "ui-icon-plus"}, + // {title: "Acls", cmd: "nsacls", uiIcon: "ui-icon-locked"} + // ], + // beforeOpen: function(event, ui) { + // var node = jQuery.ui.fancytree.getNode(ui.target); + // // Modify menu entries depending on node status + // $tree.contextmenu("enableEntry", "toc", node.isFolder()); + // // Show/hide single entries + // $tree.contextmenu("showEntry", "pg", !node.isFolder()); + // $tree.contextmenu("showEntry", "revs", !node.isFolder()); + // $tree.contextmenu("showEntry", "toc", !node.isFolder()); + // $tree.contextmenu("showEntry", "edit", !node.isFolder()); + // $tree.contextmenu("showEntry", "hpage", !node.isFolder()); + // $tree.contextmenu("showEntry", "spage", !node.isFolder()); + // $tree.contextmenu("showEntry", "cpage", !node.isFolder()); + // $tree.contextmenu("showEntry", "acls", !node.isFolder()); + // $tree.contextmenu("showEntry", "purge", !node.isFolder()); + // $tree.contextmenu("showEntry", "html", !node.isFolder()); + // $tree.contextmenu("showEntry", "text", !node.isFolder()); + // + // $tree.contextmenu("showEntry", "ns", node.isFolder()); + // $tree.contextmenu("showEntry", "search", node.isFolder()); + // $tree.contextmenu("showEntry", "npage", node.isFolder()); + // $tree.contextmenu("showEntry", "nshpage", node.isFolder()); + // $tree.contextmenu("showEntry", "nsacls", node.isFolder()); + // + // // Activate node on right-click + // node.setActive(); + // // Disable tree keyboard handling + // ui.menu.prevKeyboard = node.tree.options.keyboard; + // node.tree.options.keyboard = false; + // }, + // close: function(event, ui) { + // // Restore tree keyboard handling + // // console.log("close", event, ui, this) + // // Note: ui is passed since v1.15.0 + // var node = jQuery.ui.fancytree.getNode(ui.target); + // node.tree.options.keyboard = ui.menu.prevKeyboard; + // node.setFocus(); + // }, + // select: function(event, ui) { + // var node = jQuery.ui.fancytree.getNode(ui.target); + // alert("select " + ui.cmd + " on " + node); + // } + // }); + }); +}); + + /** * Add button action for the indexmenu wizard button * @@ -28,7 +388,7 @@ function addBtnActionIndexmenu($btn, props, edid) { // try to add button to toolbar -if (window.toolbar != undefined) { +if (window.toolbar !== undefined) { window.toolbar[window.toolbar.length] = { "type": "Indexmenu", "title": "Insert the Indexmenu tree", @@ -49,12 +409,12 @@ var IndexmenuUtils = { * @returns {string} extension gif, png or jpg */ determineExtension: function (themedir) { - var extension = "gif"; - var posext = themedir.lastIndexOf("."); + let extension = "gif"; + let posext = themedir.lastIndexOf("."); if (posext > -1) { posext++; - var ext = themedir.substring(posext, themedir.length).toLowerCase(); - if ((ext == "png") || (ext == "jpg")) { + let ext = themedir.substring(posext, themedir.length).toLowerCase(); + if ((ext === "png") || (ext === "jpg")) { extension = ext; } } diff --git a/scripts/contextmenu.js b/scripts/contextmenu.js index 37be715..338da15 100644 --- a/scripts/contextmenu.js +++ b/scripts/contextmenu.js @@ -66,6 +66,11 @@ * indexmenu_contextmenu['all']['pg']['view'].splice(1, 0, ['Input new page', '"javascript: IndexmenuContextmenu.reqpage(\'"+index.config.urlbase+"\',\'"+index.config.sepchar+"\',\'"+node.dokuid+"\');"']); */ +/* global LANG */ +/* global DOKU_BASE */ +/* global JSINFO */ + + // IMPORTANT: DON'T MODIFY THIS FILE, BUT EDIT contextmenu.local.js PLEASE! // THIS FILE IS OVERWRITTEN WHEN PLUGIN IS UPDATED @@ -76,12 +81,12 @@ indexmenu_contextmenu['all']['pg'] = { 'view': [ [''+LANG.plugins.indexmenu.page+''], [LANG.plugins.indexmenu.revs, 'IndexmenuContextmenu.getid(index.config.urlbase,id)+"do=revisions"'], - [LANG.plugins.indexmenu.tocpreview, '"javascript: IndexmenuContextmenu.createTocMenu(\'call=indexmenu&req=toc&id="+id+"\',\'picker_"+index.obj+"\',\'s"+index.obj+node.id+"\');"'] + [LANG.plugins.indexmenu.tocpreview, '"javascript: IndexmenuContextmenu.createTocMenu(\'call=indexmenu&req=toc&id="+id+"\',\'picker_"+index.treeName+"\',\'s"+index.treeName+node.id+"\');"'] ], //Menu items in edit mode, when previewing 'edit': [ [''+LANG.plugins.indexmenu.editmode+''], - [LANG.plugins.indexmenu.insertdwlink, '"javascript: IndexmenuContextmenu.insertTags(\'"+id+"\',\'"+index.config.sepchar+"\');"+index.obj+".divdisplay(\'r\',0);"', LANG.plugins.indexmenu.insertdwlinktooltip] + [LANG.plugins.indexmenu.insertdwlink, '"javascript: IndexmenuContextmenu.insertTags(\'"+id+"\',\'"+index.config.sepchar+"\');"+index.treeName+".divdisplay(\'r\',0);"', LANG.plugins.indexmenu.insertdwlinktooltip] ] }; @@ -284,11 +289,12 @@ var IndexmenuContextmenu = { }, /** - * Concatenates contextmenu configuration arrays + * Fills the contextmenu by creating entries from the given configuration arrays and concatenating these + * to the #r picker * - * @param amenu - * @param index - * @param n + * @param {any[]} amenu (part of) the configuration array + * @param {dTree} index the indexmenu object + * @param {int} n node id */ arrconcat: function (amenu, index, n) { var html, id, item, a, li; @@ -311,7 +317,7 @@ var IndexmenuContextmenu = { return true; } item = document.createElement('li'); - var $cmenu = jQuery('#r' + index.obj); + var $cmenu = jQuery('#r' + index.treeName); if (cmenuentry[1]) { if (cmenuentry[1] instanceof Array) { html = document.createElement('ul'); @@ -336,9 +342,9 @@ var IndexmenuContextmenu = { }, /** + * Absolute positioning of the div at place of mouseclick * - * - * @param obj + * @param obj div element * @param e */ mouseposition: function (obj, e) { @@ -358,24 +364,25 @@ var IndexmenuContextmenu = { }, /** + * Check mouse button onmousedown event, only for middle and right mouse button contextmenu is shown * - * - * @param n - * @param obj - * @param e + * @param {int} n node id + * @param {string|dTree} obj the unique name of a dTree object + * @param {event} e */ checkcontextm: function (n, obj, e) { e = e || event; - if ((e.which == 3 || e.button == 2) || (window.opera && e.which == 1 && e.ctrlKey)) { + // mouse clicks: which 3 === right, button 2 === right button + if ((e.which === 3 || e.button === 2) || (window.opera && e.which === 1 && e.ctrlKey)) { obj.contextmenu(n, e); IndexmenuContextmenu.stopevt(e); } }, /** + * Prevent default oncontextmenu event * - * - * @param e + * @param {event} e * @returns {boolean} */ stopevt: function (e) { diff --git a/scripts/contextmenu/jquery.ui-contextmenu.js b/scripts/contextmenu/jquery.ui-contextmenu.js new file mode 100644 index 0000000..a8fcf73 --- /dev/null +++ b/scripts/contextmenu/jquery.ui-contextmenu.js @@ -0,0 +1,634 @@ +/******************************************************************************* + * jquery.ui-contextmenu.js plugin. + * + * jQuery plugin that provides a context menu (based on the jQueryUI menu widget). + * + * @see https://github.com/mar10/jquery-ui-contextmenu + * + * Copyright (c) 2013-2018, Martin Wendt (http://wwWendt.de). Licensed MIT. + */ + +(function( factory ) { + "use strict"; + if ( typeof define === "function" && define.amd ) { + // AMD. Register as an anonymous module. + define([ "jquery", "jquery-ui/ui/widgets/menu" ], factory ); + } else { + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + +"use strict"; + +var supportSelectstart = "onselectstart" in document.createElement("div"), + match = $.ui.menu.version.match(/^(\d)\.(\d+)/), + uiVersion = { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10) + }, + isLTE110 = ( uiVersion.major < 2 && uiVersion.minor <= 10 ), + isLTE111 = ( uiVersion.major < 2 && uiVersion.minor <= 11 ); + +$.widget("moogle.contextmenu", { + version: "@VERSION", + options: { + addClass: "ui-contextmenu", // Add this class to the outer
        + closeOnWindowBlur: true, // Close menu when window loses focus + appendTo: "body", // Set keyboard focus to first entry on open + autoFocus: false, // Set keyboard focus to first entry on open + autoTrigger: true, // open menu on browser's `contextmenu` event + delegate: null, // selector + hide: { effect: "fadeOut", duration: "fast" }, + ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents + menu: null, // selector or jQuery pointing to
          , or a definition hash + position: null, // popup positon + preventContextMenuForPopup: false, // prevent opening the browser's system + // context menu on menu entries + preventSelect: false, // disable text selection of target + show: { effect: "slideDown", duration: "fast" }, + taphold: false, // open menu on taphold events (requires external plugins) + uiMenuOptions: {}, // Additional options, used when UI Menu is created + // Events: + beforeOpen: $.noop, // menu about to open; return `false` to prevent opening + blur: $.noop, // menu option lost focus + close: $.noop, // menu was closed + create: $.noop, // menu was initialized + createMenu: $.noop, // menu was initialized (original UI Menu) + focus: $.noop, // menu option got focus + open: $.noop, // menu was opened + select: $.noop // menu option was selected; return `false` to prevent closing + }, + /** Constructor */ + _create: function() { + var cssText, eventNames, targetId, + opts = this.options; + + this.$headStyle = null; + this.$menu = null; + this.menuIsTemp = false; + this.currentTarget = null; + this.extraData = {}; + this.previousFocus = null; + + if (opts.delegate == null) { + $.error("ui-contextmenu: Missing required option `delegate`."); + } + if (opts.preventSelect) { + // Create a global style for all potential menu targets + // If the contextmenu was bound to `document`, we apply the + // selector relative to the tag instead + targetId = ($(this.element).is(document) ? $("body") + : this.element).uniqueId().attr("id"); + cssText = "#" + targetId + " " + opts.delegate + " { " + + "-webkit-user-select: none; " + + "-khtml-user-select: none; " + + "-moz-user-select: none; " + + "-ms-user-select: none; " + + "user-select: none; " + + "}"; + this.$headStyle = $("