This commit is contained in:
hongweipeng 2016-04-01 11:39:04 +08:00
commit 0a58e3c1bb
4 changed files with 519 additions and 0 deletions

220
MenuTree/Plugin.php Normal file
View File

@ -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();
}
}

165
MenuTree/dropdown.js Normal file
View File

@ -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);

108
MenuTree/menutree.css Normal file
View File

@ -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;
}

26
README.md Normal file
View File

@ -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/