选项卡页
目标
选项卡用于显示已经打开过的页面.在这些页面之间切换.
制作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>
功能特点
- 加载新页面:缓存当前页面后,加载新页面,同时增加一个选项卡.如果选项卡标题有相同的,则在选项卡标题加上(n)
- 选项卡容器上加上主题类,可使用不同颜色主题.变化部位在于活动选项卡颜色和底边框颜色
- 导航按钮:选项卡超出可视范围后,使用前进后退按钮滚动选项卡框
- 点击选项卡时,如果靠近选项卡框的两端,则调整该选项卡到中间位置
- 关闭所有选项卡.关闭除活动选项卡外的.定位当前活动选项卡,当其不在可视范围内时.
- 当前活动页面不缓存,当页面从非活动转活动时,其对应缓存会删除
- 缓存页面使用document.createDocumentFragment,加入其中的DOM会从当前文档中脱离.用以替代经典的iframe方法,
- 经过测试,一个填写过的表单页面在放入文档片段对象之后,再取出来时,其状态不变.
使用
// 调用方式 // 配置 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(); } })
.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; }