From 0a58e3c1bb451c5f5e0d49ed8906fbd2f0214107 Mon Sep 17 00:00:00 2001 From: hongweipeng <961365124@qq.com> Date: Fri, 1 Apr 2016 11:39:04 +0800 Subject: [PATCH] init --- MenuTree/Plugin.php | 220 ++++++++++++++++++++++++++++++++++++++++++ MenuTree/dropdown.js | 165 +++++++++++++++++++++++++++++++ MenuTree/menutree.css | 108 +++++++++++++++++++++ README.md | 26 +++++ 4 files changed, 519 insertions(+) create mode 100644 MenuTree/Plugin.php create mode 100644 MenuTree/dropdown.js create mode 100644 MenuTree/menutree.css create mode 100644 README.md diff --git a/MenuTree/Plugin.php b/MenuTree/Plugin.php new file mode 100644 index 0000000..55441f6 --- /dev/null +++ b/MenuTree/Plugin.php @@ -0,0 +1,220 @@ +<?php +/** + * 文章目录树 + * + * @package MenuTree + * @author hongweipeng + * @version 0.6.1 + * @link https://www.hongweipeng.com + */ +class MenuTree_Plugin implements Typecho_Plugin_Interface { + /** + * 索引ID + */ + public static $id = 1; + + /** + * 目录树 + */ + public static $tree = array(); + + /** + * 激活插件方法,如果激活失败,直接抛出异常 + * + * @access public + * @return void + * @throws Typecho_Plugin_Exception + */ + public static function activate() { + Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array(__CLASS__, 'parse'); + Typecho_Plugin::factory('Widget_Archive')->header = array(__CLASS__, 'header'); + Typecho_Plugin::factory('Widget_Archive')->footer = array(__CLASS__, 'footer'); + } + + /** + * 禁用插件方法,如果禁用失败,直接抛出异常 + * + * @static + * @access public + * @return void + * @throws Typecho_Plugin_Exception + */ + public static function deactivate(){} + + /** + * 获取插件配置面板 + * + * @access public + * @param Typecho_Widget_Helper_Form $form 配置面板 + * @return void + */ + public static function config(Typecho_Widget_Helper_Form $form){ + + $jq_import = new Typecho_Widget_Helper_Form_Element_Radio('jq_import', array( + 0 => _t('不引入'), + 1 => _t('引入') + ), 1, _t('是否引入jQuery'), _t('此插件需要jQuery,如已有选择不引入避免引入多余jQuery')); + $form->addInput($jq_import->addRule('enum', _t('必须选择一个模式'), array(0, 1))); + + + } + + /** + * 个人用户的配置面板 + * + * @access public + * @param Typecho_Widget_Helper_Form $form + * @return void + */ + public static function personalConfig(Typecho_Widget_Helper_Form $form){} + + /** + * 插件实现方法 + * + * @access public + * @return void + */ + public static function render() { + + } + + /** + * 解析 + * + * @access public + * @param array $matches 解析值 + * @return string + */ + public static function parseCallback( $match ) { + $parent = &self::$tree; + + $html = $match[0]; + $n = $match[1]; + $menu = array( + 'num' => $n, + 'title' => trim( strip_tags( $html ) ), + 'id' => 'menu_index_' . self::$id, + 'sub' => array() + ); + $current = array(); + if( $parent ) { + $current = &$parent[ count( $parent ) - 1 ]; + } + // 根 + if( ! $parent || ( isset( $current['num'] ) && $n <= $current['num'] ) ) { + $parent[] = $menu; + } else { + while( is_array( $current[ 'sub' ] ) ) { + // 父子关系 + if( $current['num'] == $n - 1 ) { + $current[ 'sub' ][] = $menu; + break; + } + // 后代关系,并存在子菜单 + elseif( $current['num'] < $n && $current[ 'sub' ] ) { + $current = &$current['sub'][ count( $current['sub'] ) - 1 ]; + } + // 后代关系,不存在子菜单 + else { + for( $i = 0; $i < $n - $current['num']; $i++ ) { + $current['sub'][] = array( + 'num' => $current['num'] + 1, + 'sub' => array() + ); + $current = &$current['sub'][0]; + } + $current['sub'][] = $menu; + break; + } + } + } + self::$id++; + return "<span id=\"{$menu['id']}\" name=\"{$menu['id']}\"></span>" . $html; + } + + /** + * 构建目录树,生成索引 + * + * @access public + * @return string + */ + public static function buildMenuHtml( $tree, $include = true ) { + $menuHtml = ''; + foreach( $tree as $menu ) { + if( ! isset( $menu['id'] ) && $menu['sub'] ) { + $menuHtml .= self::buildMenuHtml( $menu['sub'], false ); + } elseif( $menu['sub'] ) { + $menuHtml .= "<li><a data-scroll href=\"#{$menu['id']}\" title=\"{$menu['title']}\">{$menu['title']}</a>" . self::buildMenuHtml( $menu['sub'] ) . "</li>"; + } else { + $menuHtml .= "<li><a data-scroll href=\"#{$menu['id']}\" title=\"{$menu['title']}\">{$menu['title']}</a></li>"; + } + } + if( $include ) { + $menuHtml = '<ul>' . $menuHtml . '</ul>'; + } + return $menuHtml; + } + + /** + * 判断是否是内容页,避免主页加载插件 + */ + public static function is_content() { + static $is_content = null; + if($is_content === null) { + $widget = Typecho_Widget::widget('Widget_Archive'); + $is_content = !($widget->is('index') || $widget->is('search') || $widget->is('date') || $widget->is('category')); + } + return $is_content; + } + /** + * 插件实现方法 + * + * @access public + * @return string + */ + public static function parse( $html, $widget, $lastResult ) { + $html = empty( $lastResult ) ? $html : $lastResult; + if (!self::is_content()) { + return $html; + } + $html = preg_replace_callback( '/<h([1-6])[^>]*>.*?<\/h\1>/s', array( 'MenuTree_Plugin', 'parseCallback' ), $html ); + return $html; + } + + /** + *为header添加css文件 + *@return void + */ + public static function header() { + if (!self::is_content()) { + return; + } + $cssUrl = Helper::options()->pluginUrl . '/MenuTree/menutree.css'; + echo '<link rel="stylesheet" type="text/css" href="' . $cssUrl . '" />'; + } + + /** + *为footer添加js文件 + *@return void + */ + public static function footer() { + if (!self::is_content()) { + return; + } + if (Helper::options()->plugin('MenuTree')->jq_import) { + echo '<script src="//cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>'; + } + + $html = '<div class="in-page-preview-buttons in-page-preview-buttons-full-reader"><ul><li title="内容目录 Ctrl+Alt+O"id="preview-toc-button"class="in-page-button dropdown"><svg data-toggle="dropdown"class="dropdown-toggle icon-list" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 22 22" style="enable-background:new 0 0 22 22;" xml:space="preserve"><path style="fill-rule:evenodd;clip-rule:evenodd;" d="M22,0L0,7.5l11,3.459L14.5,22L22,0z M2.512,7.534L20.5,1.5l-6,17.5 l-2.75-8.75L2.512,7.534z"/></svg><div class="dropdown-menu theme pull-right theme-white"id="toc-list"><h3>内容目录</h3><hr><div class="table-of-contents"><div class="toc"><ul><li>'. self::buildMenuHtml( self::$tree ) .'</li></ul></div></div></div></li></ul></div>'; + $js = Helper::options()->pluginUrl . '/MenuTree/dropdown.js'; + echo <<<HTML + <script src="{$js}"></script> + <script type="text/javascript"> + jQuery('body').append('$html'); + </script> +HTML; + self::$id = 1; + self::$tree = array(); + + } +} diff --git a/MenuTree/dropdown.js b/MenuTree/dropdown.js new file mode 100644 index 0000000..0fa87c7 --- /dev/null +++ b/MenuTree/dropdown.js @@ -0,0 +1,165 @@ +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.6 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.6' + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) + }) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $(document.createElement('div')) + .addClass('dropdown-backdrop') + .insertAfter($(this)) + .on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this + .trigger('focus') + .attr('aria-expanded', 'true') + + $parent + .toggleClass('open') + .trigger($.Event('shown.bs.dropdown', relatedTarget)) + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive && e.which != 27 || isActive && e.which == 27) { + if (e.which == 27) $parent.find(toggle).trigger('focus') + return $this.trigger('click') + } + + var desc = ' li:not(.disabled):visible a' + var $items = $parent.find('.dropdown-menu' + desc) + + if (!$items.length) return + + var index = $items.index(e.target) + + if (e.which == 38 && index > 0) index-- // up + if (e.which == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).trigger('focus') + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.dropdown + + $.fn.dropdown = Plugin + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) + .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) + +}(jQuery); diff --git a/MenuTree/menutree.css b/MenuTree/menutree.css new file mode 100644 index 0000000..f1803d7 --- /dev/null +++ b/MenuTree/menutree.css @@ -0,0 +1,108 @@ +.in-page-preview-buttons-full-reader { + right:50px; + top:-15px; + border-radius: 50%; + width: 48px; + height: 48px; + background: rgba(255, 255, 255, 0.2); + box-shadow: 0 1px 8px 1.5px rgba(0, 0, 0, 0.35), 0 20px 70px 8px rgba(0, 0, 0, 0.25); +} + +.in-page-editor-buttons,.in-page-preview-buttons { + margin-top:100px; + position:fixed; + z-index:10; +} + +.in-page-button { + /*margin-right:15px;*/ + width:20px; + height:20px; + display:inline-block; + list-style:none; + cursor:pointer; + font-size:17px; +} +.in-page-button>svg { + width:25px; + height:25px; + display:inline-block; + padding-left: 12px; + padding-top: 12px; +} +.icon-list:before{ + display: inline-block; + text-decoration: inherit; +} +.icon-list { +} +.in-page-preview-buttons ul { + color: #2c3e50; + padding-left: 0; +} +#toc-list { + background-clip:padding-box; + border-radius:4px; + float:left; + font-size:14px; + list-style:none outside none; + margin:2px 0 0; + min-width:160px; + position:absolute; + padding:5px 0 20px; + top:100%; + z-index:1000; + box-shadow: 0 1px 8px 1.5px rgba(0, 0, 0, 0.35), 0 20px 70px 8px rgba(0, 0, 0, 0.25); +} +#toc-list .toc ul { + margin-left:20px; + padding-left: 0; +} +#toc-list .toc>ul { + margin:0 +} +#toc-list a:hover { + background:0 0; + color:#005580; + text-decoration:underline; +} +#toc-list a { + color:#08c; + text-decoration:none; + display:inline; +} +#toc-list h3 { + margin:10px 0; + padding-left:15px; +} +#toc-list hr { + margin:10px 0; +} +.dropdown-menu { + display: none; +} +.dropdown-menu.pull-right { + left: auto; + right: 0; +} +.theme-white { + background-color: #f9f9f5; + /*background-color: rgba(0, 0, 0, 0.5);*/ + color: #2c3e50; +} +.pull-right { + float: right; +} +.table-of-contents { + overflow-x:hidden; + overflow-y:auto; + width:330px; + max-height:400px; + padding:5px 0; +} +.toc ul { + list-style-type:none; +} +.open > .dropdown-menu { + display: block; +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c49a7f --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +## 起步 + +悬浮式文章目录树,定在右侧。 + +## 使用方法 + +第一步:下载本插件,放在 `usr/plugins/` 目录中; +第二步:激活插件; + +## 预览 + +![20160401112707.png][1] + +github开源地址:[https://github.com/hongweipeng/MenuTree_for_typecho][2] + +## 与我联系: + +作者:hongweipeng +主页:[https://www.hongweipeng.com/][3] +或者通过 Emai: hongweichen8888@sina.com +有任何问题也可评论留言 + + + [1]: https://www.hongweipeng.com/usr/uploads/2016/04/1824863673.png + [2]: https://github.com/hongweipeng/MenuTree_for_typecho + [3]: https://www.hongweipeng.com/ \ No newline at end of file