选项卡页

目标

选项卡用于显示已经打开过的页面.在这些页面之间切换.

制作js插件类,实现基础的选项卡页功能.选项卡的显示,关闭,移动,选项卡页的缓存.

使用document.createDocumentFragment缓存已打开页面,替代iframe方案

图示1

图示2

html结构

由选项卡容器(.cachetabs)和显示容器两部分,主要功能实现在选项卡,显示容器只是加载页面.

选项卡容器html结构如下.由按钮和选项卡导航区域组成,按钮含前进,后退,关闭按钮组.选项卡框是个带横向滚动条的div

 1 <div class="cachetabs" id="cachetabs1">
 2   <a class="cachetabs-left"></a>
 3   <nav class="cachetabs-navbox"><div class="cachetabs-nav"></div></nav>
 4   <a class="cachetabs-right"></a>
 5   <span class="cachetabs-menutitle">功能</span>
 6   <div class="cachetabs-menugroup">
 7     <span class="cachetabs-goto-active">定位当前页</span>
 8     <span class="cachetabs-close-all">关闭全部</span>
 9     <span class="cachetabs-close-other">关闭其它</span>
10   </div>
11 </div>

功能特点

  1. 加载新页面:缓存当前页面后,加载新页面,同时增加一个选项卡.如果选项卡标题有相同的,则在选项卡标题加上(n)
  2. 选项卡容器上加上主题类,可使用不同颜色主题.变化部位在于活动选项卡颜色和底边框颜色
  3. 导航按钮:选项卡超出可视范围后,使用前进后退按钮滚动选项卡框
  4. 点击选项卡时,如果靠近选项卡框的两端,则调整该选项卡到中间位置
  5. 关闭所有选项卡.关闭除活动选项卡外的.定位当前活动选项卡,当其不在可视范围内时.
  6. 当前活动页面不缓存,当页面从非活动转活动时,其对应缓存会删除
  7. 缓存页面使用document.createDocumentFragment,加入其中的DOM会从当前文档中脱离.用以替代经典的iframe方法,
  8. 经过测试,一个填写过的表单页面在放入文档片段对象之后,再取出来时,其状态不变.

使用

// 调用方式
// 配置
let tabscfg = {
  // 显示容器的ID
  screenId: 'cachetabs_mainbox',
  // 选项卡容器的ID
  cachetabsId: 'cachetabs1'
};
// 实例化
let cachetabs = new $.cacheTabs(tabscfg);
// 载入页面 html:页面dom,menutitle:选项卡标题
cachetabs.load(html, tabtitle)

类与样式

// 缓存组件
// 此缓存页面插件多用于AJAX载入片段页时.是一个显示新页面或者已缓存页面的构架.是一个iframe的替代解决方案.
// 主要作用是,在一个容器中操作缓存和显示文档.每当要显示一个文档时,先将容器中当前的文档缓存到片段中,然后
// 再显示新文档.这个被缓存的文档它的状态不变.将再次显示它时,从缓存中调出来.
// 需要引用: JQ, JsExtFun.js
$.extend({
    // 创建对象 let cachetabs = new $.cacheTabs(config);
    // {screenId:'显示内容的容器ID',cachetabsId:'选项卡容器ID'}
    cacheTabs: function (config)
    {
        /*=================*
         * init config
         *=================*/
        let self = this;
        if (!config) throw '必须传入配置对象';
        // cfg 
        let cfg = {};
        // 内容显示框JQ对象
        cfg.screenJQ = $('#' + config.screenId);
        // 选项卡框JQ对象
        cfg.tabsJQ = $('#' + config.cachetabsId);
        // 活动选项卡类名
        cfg.activeCls = 'active';
        // 选项卡与缓存键关联属性名
        cfg.cacheKey = 'cacheId';

        /*====================*
         * 方法 public
         *====================*/
        // 载入页面:缓存当前容器中的页面.将新页面显示在容器中.增加一个新选项卡
        self.load = function (doms, title, closeE)
        {
            // 选项卡容器
            let navJQ = cfg.tabsJQ.find('.cachetabs-nav');
            // 如果已经存在相同title的页面,将其命名为title(n),n>=1
            if (navJQ.find('.cachetabs-tab').length > 0)
            {
                title = titleExist(title, navJQ);
            }
            // 选项卡为空时,当前无页面,无需缓存.
            if (navJQ.find('.cachetabs-tab').length > 0)
            {
                // 缓存当前DOM
                let cacheId = domToCache();
                // 找到当前活动的选项卡,去掉活动状态,添加缓存id,关联选项卡与缓存
                navJQ.find('.' + cfg.activeCls).removeClass(cfg.activeCls).attr(cfg.cacheKey, cacheId);
            }

            // 新增页面的选项卡
            let tab = '<label class="cachetabs-tab {0}" title="{1}">{1}<a class="cachetabs-tabclose" title="关闭">×</a></label>';
            tab = String.Format(tab, cfg.activeCls, title);

            // 显示容器加载新页
            cfg.screenJQ.html(doms);
            // 选项卡页加入新选项卡
            navJQ.append(tab);
            // 绑定新tab的点击事件与关闭事件
            bindEventForLabelBtn(navJQ.find('.' + cfg.activeCls));
            bindEventForCloseBtn(navJQ.find('.' + cfg.activeCls), closeE);
            // 选项卡框滚动条移动到最后
            navScroller(1);
        }
       
        /*====================*
         * 方法 private
         *====================*/
        // 选项卡重复标题检查,如果已经存在相同title的,将其命名为title(n),n>=1
        let titleExist = function (title, navJQ)
        {
            let i = 1;
            let copytitle = title;
            while (true)
            {
                if (navJQ.find('.cachetabs-tab[title="' + copytitle + '"]').length > 0)
                {
                    let copyindex = copytitle.lastIndexOf('(');
                    if (copyindex > 0)
                        copytitle = copytitle.substring(0, copyindex);
                    copytitle = String.Format('{0}({1})', copytitle, i);
                    i++;
                    continue;
                }
                break;
            }
            return copytitle;
        }
        // 将当前页面添加到缓存 ,将对应选项卡
        let domToCache = function ()
        {
            // 取出当前容器中的页面节点
            let activeDoms = cfg.screenJQ.contents();
            // 添加进缓存
            let cacheId = store.add(activeDoms);
            //console.log('当前页面放入缓存区成功!cacheId=' + cacheId + '内容:' + activeDoms);
            return cacheId;
        }
        // 将缓存页面载入到显示容器中
        let cacheToDom = function (cacheId)
        {
            let doms = store.get(cacheId);
            if (doms == null)
            {
                //console.log('缓存页面cacheId无效,cacheId:' + cacheId);
                return;
            }
            //console.log('缓存页面已经载入,cacheId:' + cacheId);
            cfg.screenJQ.html(doms);
        }
        // 调整选项卡框的滚动条值,使用选项卡显示在合适的位置上
        // len:滚动距离,>0 : 右滚此距离, <0 : 左滚, 0 : 滚动到最左, 1 : 到最右,
        //              'left': 左滚默认距离, 'right': 右滚默认距离
        let navScroller = function (len, tabJQ)
        {
            let navJQ = cfg.tabsJQ.find('.cachetabs-nav');

            // 滚动条位置
            let sPosition = navJQ.scrollLeft();
            // nav宽度
            let w = navJQ.width();
            // nav文档长度
            let swidth = navJQ[0].scrollWidth;
            // 需要滚动的新位置
            let toPosition = 0;
            //
            if (len == 0)
                toPosition = 0;
            else if (len == 1)
                toPosition = swidth;
            else if (len == 'left')
                toPosition = sPosition - (w / 4);
            else if (len == 'right')
                toPosition = sPosition + (w / 4);
            else
                toPosition = sPosition + len;
            // 移动滚动条, 此处无需判是否滚动到头或者尾.如果传入的滚动位置无效,则会自动设为0或最大
            navJQ.scrollLeft(toPosition);
            //console.log('滚动位置: ' + toPosition);
            //console.log('文档长度: ' + navJQ[0].scrollWidth);
        }
        // 调整选项卡框的滚动条值,当指定选项卡靠近选项卡框左边或右边时,使其处于中间位置
        let navScrollerByTab = function (tabJQ)
        {
            let navJQ = cfg.tabsJQ.find('.cachetabs-nav');
            // 界限值100px,大致是一个按钮的宽度
            let tagLen = 100;
            // 滚动条位置
            let sPosition = navJQ.scrollLeft();
            // nav宽度
            let w = navJQ.width();
            // nav文档长度
            let swidth = navJQ[0].scrollWidth;
            // 离选项卡框左起位置
            let tabPosition = tabJQ.position().left;
            //
            //console.log('滚动条位置 ' + sPosition + ' 文档宽度 ' + w + ' nav文档长度 ' + swidth + ' 离选项卡框的位置 ' + tabPosition);
            if (tabPosition < tagLen || (tabPosition + 2*tagLen) > w)
            {
                navJQ.scrollLeft(sPosition + (tabPosition - w / 2));
                //console.log('判定移动距离:' + (sPosition + (tabPosition - w / 2)));
            }
        }
        /*=============================*
         * 事件绑定
         *=============================*/
        // 选项卡关闭按钮: tabJQ:选项卡JQ对象,onClosing:关闭时事件.返回false取消关闭
        let bindEventForCloseBtn = function (tabJQ, onClosing)
        {
            // 点击标签选项卡上的关闭按钮时,关闭当前页面,清除其缓存,载入上次的页面到容器中
            tabJQ.find('.cachetabs-tabclose').on('click', function (event)
            {
                event.stopPropagation();
                if (typeof onClosing == 'function')
                {
                    if (onClosing() == false)
                        return;
                }

                // 当前页面:则删除当前页面,加载缓存中最后一个页面到显示容器中
                if (tabJQ.hasClass(cfg.activeCls))
                {
                    let lastkey = store.KeyIndex.Last();
                    // 如果没有缓存页了,说明关闭的是最后一个选项卡.此时删除页面即可
                    if (lastkey == null)
                    {
                        cfg.screenJQ.empty();
                    }
                    else
                    {
                        cacheToDom(lastkey);
                        // 激活缓存对应选项卡,去掉关联属性,删除该缓存.
                        let navJQ = cfg.tabsJQ.find('.cachetabs-nav');
                        navJQ.find(String.Format('.cachetabs-tab[{0}={1}]', cfg.cacheKey, lastkey))
                            .addClass(cfg.activeCls).removeAttr(cfg.cacheKey);
                        store.remove(lastkey);
                    }
                } else
                {
                    let cacheId = tabJQ.attr(cfg.cacheKey);
                    // 非当前页面:直接清除缓存
                    store.remove(cacheId);
                }
                // 删除选项卡
                tabJQ.remove();
            })
        }
        let bindEventForInit = function ()
        {
            bindEventForLeftRightBtn();
            bindEventForGoToActiveTab();
            bindEventForCloseAll();
            bindEventForCloseAllWithOutActive();
        }
        // 选项卡点击:选项卡之间的切换
        let bindEventForLabelBtn = function (tabJQ)
        {
            // 点击标签选项卡时,缓存当前容器中的页面,对应缓存页面加载到容器中
            tabJQ.on('click', function ()
            {
                // 点击选项卡时,位置会相应调整,确保点击的选项卡完全显示在父级的可见区域.
                navScrollerByTab(tabJQ);
                // 点击的是活动页面,退出
                if ($(this).hasClass(cfg.activeCls))
                    return;

                // 缓存当前DOM
                let cacheId_current = domToCache();
                // 找到当前活动的选项卡,去掉活动状态,添加缓存id,关联选项卡与缓存
                $(this).parent().find('.' + cfg.activeCls).removeClass(cfg.activeCls).attr(cfg.cacheKey, cacheId_current);

                // 激活点击的选项卡,获取其缓存页加载到显示容器
                let cacheId = $(this).attr(cfg.cacheKey);
                cacheToDom(cacheId);
                // 删除其关联属性,缓存
                $(this).addClass(cfg.activeCls).removeAttr(cfg.cacheKey);
                store.remove(cacheId);
            })
        }
        // 选项卡前进,后退 按钮事件绑定
        let bindEventForLeftRightBtn = function ()
        {
            // 选项卡页向左滚动,调整选项卡框的scroll值
            cfg.tabsJQ.find('.cachetabs-left').on('click', function ()
            {
                navScroller('left');
            })

            // 选项卡页向右滚动按钮,调整选项卡框的scroll值
            cfg.tabsJQ.find('.cachetabs-right').on('click', function ()
            {
                navScroller('right');
            })
        }
        // 定位当前选项卡
        let bindEventForGoToActiveTab = function ()
        {
            cfg.tabsJQ.find('.cachetabs-goto-active').on('click', function ()
            {
                let activeTab = cfg.tabsJQ.find('.' + cfg.activeCls);
                if (activeTab.length == 0) return;
                navScrollerByTab(activeTab);
            })
        }
        // 关闭所有选项卡
        let bindEventForCloseAll = function ()
        {
            cfg.tabsJQ.find('.cachetabs-close-all').on('click', function ()
            {
                // 删除选项卡,删除缓存,清空显示容器
                let navJQ = cfg.tabsJQ.find('.cachetabs-nav');
                navJQ.empty();
                store.clear();
                cfg.screenJQ.empty();
            })
        }
        // 关闭除当前外所有选项卡
        let bindEventForCloseAllWithOutActive = function ()
        {
            cfg.tabsJQ.find('.cachetabs-close-other').on('click', function ()
            {
                // 删除选项卡除活动的外,删除缓存.(活动页无缓存)
                let navJQ = cfg.tabsJQ.find('.cachetabs-nav');
                navJQ.find('.cachetabs-tab:not(.'+ cfg.activeCls+')').remove();
                store.clear();
            })
        }
        /*==============================*
         * 缓存处理类
         *==============================*/
        let store = new function ()
        {
            let self = this;
            // 缓存数据
            self.Dict = {};
            // 缓存数据的键索引
            self.KeyIndex = [];
            // 添加DOM片段到缓存,然后返回缓存ID. doms:dom片段,不能是JQ对象
            self.add = function (doms)
            {
                // 缓存索引范围 0~1023
                let keycount = Object.keys(self.Dict).length;
                if (keycount > 1023) return;
                //
                let newCacheId = '_' + Math.NextInt(keycount, 1023);
                while (true)
                {
                    if (self.Dict.hasOwnProperty(newCacheId))
                        newCacheId = '_' + Math.NextInt(keycount, 1023);
                    else
                        break;
                }
                if (!doms)
                {
                    self.Dict[newCacheId] = null;
                } else
                {
                    // 建立新的文档片断对象
                    let fragdom = document.createDocumentFragment();
                    //console.log(fragdom);
                    //fragdom.appendChild(doms);
                    $(fragdom).append(doms);
                    self.Dict[newCacheId] = fragdom;
                }
                // 缓存ID插入排序列表
                self.KeyIndex.push(newCacheId);
                //console.log('添加了缓存,键:' + newCacheId);
                //console.log('缓存字典长度' + Object.keys(self.Dict).length);
                //console.log('缓存键列表' + self.KeyIndex);
                return newCacheId;
            }
            // 删除缓存 成功则返回true,cacheId无效返回false
            self.remove = function (cacheId)
            {
                if (self.Dict.hasOwnProperty(cacheId))
                {
                    delete self.Dict[cacheId];
                    self.KeyIndex.Remove(cacheId);
                    //console.log('删除了缓存' + cacheId);
                    //console.log('缓存字典长度' + Object.keys(self.Dict).length);
                    //console.log('缓存键列表' + self.KeyIndex);
                    return true;
                }
                //console.log('删除缓存失败,不存在的缓存id:' + cacheId);
                return false;
            }
            // 清空缓存
            self.clear = function ()
            {
                self.Dict = {};
                self.KeyIndex = [];
                //console.log('删除了全部缓存');
                //console.log('缓存字典长度' + Object.keys(self.Dict).length);
                //console.log('缓存键列表' + self.KeyIndex);
            }
            // 根据ID取缓存
            self.get = function (cacheId)
            {

                if (self.Dict.hasOwnProperty(cacheId))
                {
                    //console.log('取出了缓存,键:' + cacheId);
                    return self.Dict[cacheId];
                }
                //console.log('取出缓存无效,键不存在,键:' + cacheId);
                return null;
            }
        }

        /*==============================*
         * 初始化后绑定基础事件
         *==============================*/
        bindEventForInit();
    }
})
js
.cachetabs {
  display: flex;
  position: relative;
  height: 42px;
  line-height: 40px;
  border-bottom: 2px solid #007bff;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.cachetabs-left, .cachetabs-right {
  flex: 0 1 36px;
  border-left: 1px solid #ced4da;
  border-right: 1px solid #ced4da;
  cursor: pointer;
  background-color: #fff;
}

.cachetabs-right {
  text-align: right;
}

  .cachetabs-left:before, .cachetabs-right:before {
    content: '';
    vertical-align: middle;
  }

.cachetabs-left:before {
  display: inline-block;
  width: 0;
  height: 0;
  border: 12px solid transparent;
  border-right-color: #6c757d;
}

.cachetabs-right:before {
  display: inline-block;
  width: 0;
  height: 0;
  border: 12px solid transparent;
  border-left-color: #6c757d;
}

.cachetabs-left:hover, .cachetabs-right:hover, .cachetabs-tab:hover {
  background-color: #e9ecef;
}

.cachetabs-left:active, .cachetabs-right:active, .cachetabs-tab:active {
  background-color: #dee2e6;
}

.cachetabs-menutitle {
  flex-basis: 56px;
  text-align: center;
  background-color: #fff;
}

  .cachetabs-menutitle:after {
    content: '';
    display: inline-block;
    width: 0;
    height: 0;
    border: 5px solid transparent;
    border-top-color: #6c757d;
    margin-left: 2px;
    vertical-align: middle;
  }

  .cachetabs-menutitle:hover {
    background-color: #e9ecef;
  }

    .cachetabs-menutitle:hover ~ .cachetabs-menugroup {
      display: block;
    }

.cachetabs-menugroup {
  display: none;
  position: absolute;
  top: 40px;
  right: 0;
  width: 120px;
  color: #adb5bd;
  text-align: center;
  background-color: #fff;
  border-left: 2px solid #007bff;
  border-bottom: 2px solid #007bff;
  cursor: pointer;
}

  .cachetabs-menugroup:hover {
    display: block;
  }

.cachetabs-close-all, .cachetabs-close-other, .cachetabs-goto-active {
  display: block;
}

.cachetabs-goto-active {
  border-bottom: 1px solid #e9ecef;
}

  .cachetabs-close-all:hover, .cachetabs-close-other:hover, .cachetabs-goto-active:hover {
    color: #495057;
    background-color: #e9ecef;
  }

.cachetabs-navbox {
  flex: 1 1 0;
  width: 0;
  height: 40px;
  overflow: hidden;
  background-color: #f8f9fa;
}

.cachetabs-nav {
  position: relative;
  margin-right: 100px;
  white-space: nowrap;
  overflow-x: auto;
}

.cachetabs-tab {
  display: inline-block;
  padding: 0 15px;
  height: 40px;
  border-right: 1px solid #ced4da;
  font-size: 14px;
  cursor: pointer;
}

  .cachetabs-tab.active {
    color: #fff;
    background-color: #007bff;
  }

.cachetabs-tabclose {
  display: inline-block;
  width: 16px;
  height: 16px;
  line-height: 16px;
  color: #ced4da;
  text-align: center;
  margin-left: 4px;
}

  .cachetabs-tabclose:hover {
    border-radius: 50% 50%;
    color: #fff;
    background-color: #dc3545;
    text-decoration: none;
  }

.cachetabs.gray {
  border-bottom: 2px solid #6c757d;
}

  .cachetabs.gray .cachetabs-tab.active {
    background-color: #6c757d;
  }

  .cachetabs.gray .cachetabs-menugroup {
    border-left: 2px solid #6c757d;
    border-bottom: 2px solid #6c757d;
  }

.cachetabs.green {
  border-bottom: 2px solid #28a745;
}

  .cachetabs.green .cachetabs-tab.active {
    background-color: #28a745;
  }

  .cachetabs.green .cachetabs-menugroup {
    border-left: 2px solid #28a745;
    border-bottom: 2px solid #28a745;
  }

.cachetabs.red {
  border-bottom: 2px solid #dc3545;
}

  .cachetabs.red .cachetabs-tab.active {
    background-color: #dc3545;
  }

  .cachetabs.red .cachetabs-menugroup {
    border-left: 2px solid #dc3545;
    border-bottom: 2px solid #dc3545;
  }

.cachetabs.yellow {
  border-bottom: 2px solid #ffc107;
}

  .cachetabs.yellow .cachetabs-tab.active {
    background-color: #ffc107;
  }

  .cachetabs.yellow .cachetabs-menugroup {
    border-left: 2px solid #ffc107;
    border-bottom: 2px solid #ffc107;
  }
css

 

posted @ 2018-08-29 11:43  mirrorspace  阅读(437)  评论(0编辑  收藏  举报