feat: 升级 2.10 核心 (#2477)

This commit is contained in:
贤心 2025-03-10 14:30:36 +08:00 committed by GitHub
commit 7075fddfeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 7665 additions and 5059 deletions

View File

@ -0,0 +1,21 @@
<pre class="layui-code" lay-options="{preview: true, text: {preview: '综合用法'}, layout: ['preview', 'code'], tools: ['full']}">
<textarea>
{{- d.include("/MOD_NAME/examples/demo.md") }}
</textarea>
</pre>
<h3 id="demo-NAME1" class="ws-anchor ws-bold">示例1</h3>
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], tools: ['full']}">
<textarea>
{{- d.include("/MOD_NAME/examples/ex1.md") }}
</textarea>
</pre>
<h3 id="demo-NAME2" class="ws-anchor ws-bold">示例2</h3>
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], tools: ['full']}">
<textarea>
{{- d.include("/MOD_NAME/examples/ex2.md") }}
</textarea>
</pre>

View File

@ -1,5 +1,3 @@
<pre class="layui-code" lay-options="{preview: true, text: {preview: '综合用法'}, layout: ['preview', 'code'], tools: ['full']}">
<textarea>
AAA
<!-- import layui -->
@ -7,25 +5,6 @@ AAA
layui.use(function(){
var MOD_NAME = layui.MOD_NAME;
});
</script>
</textarea>
</pre>
<h3 id="demo-NAME" class="ws-anchor ws-bold">示例标题</h3>
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], tools: ['full']}">
<textarea>
AAA
<!-- import layui -->
<script>
layui.use(function(){
var MOD_NAME = layui.MOD_NAME;
});
</script>
</textarea>
</pre>

View File

@ -2,10 +2,10 @@
title: 某某组件 MOD_NAME
toc: true
---
# 某某组件
> 某某组件 `MOD_NAME`
> 某某组件 `MOD_NAME`
<h2 id="examples" lay-toc="{hot: true}" style="margin-bottom: 0;">示例</h2>
@ -27,8 +27,8 @@ toc: true
- 参数 `options` : 基础属性配置项。[#详见属性](#options)
<h2 id="options" lay-toc="{level: 2, hot: true}">属性</h2>
<h3 id="options" lay-toc="{level: 2, hot: true}">属性</h3>
<div>
{{- d.include("/MOD_NAME/detail/options.md") }}
</div>
</div>

34
docs/.layui/prompt.txt Normal file
View File

@ -0,0 +1,34 @@
> 自动生成组件文档提示词
# 角色
你是一位顶级的前端开发专家,能够高效、准确地为 JavaScript 组件代码生成对应的接口文档,并且严格遵循给定的模板规则。
## 组件
本次生成的组件名称为: input (统一简称为 MOD_NAME)
## 任务
按照 `/.layui` 目录给定的模板,为 `/src/modules/MOD_NAME.js` 生成完整的文档,并保存在 `/docs/MOD_NAME/` 目录。文档要采用 `HTML+Markdown+laytpl` 混合编写,其中 laytpl 为视图引擎(类似于 ejs可为文档引入子模版`{{- d.include("/MOD_NAME/detail/demo.md") }}`)。
### 文档模板介绍
- `index.md`: 组件文档主文件包含示例、API、属性等完整内容。
- `detail/`: 目录存放文档子模板。一般在 index.md 内容过大时可将内容碎片放置在该目录中index.md 只需引用即可。
- `detail/demo.md`: 组件示例主模板,在 `index.md` 中引入。
- `detail/options.md`: 组件 `render()` 方法接受的参数配置项。
- `examples/`: 目录存放组件示例文件,在 `detail/demo.md` 中引入。
### 文档内容要求
- `index.md` 主文档内容主要包含以下层级:
```markdown
## 示例
## API
### 渲染
### 属性
## 事件(如果有的话)
```
其中API 中列举的所有方法进行介绍,如果方法传入的参数(如 opts是一个选项需按照表格的方式展示
```markdown
| opts | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| 内容 | 内容 | 内容 | 内容 |
```
- `detail/options.md` 需按照当前已有的 HTML + Markdown 模板混合编写。

View File

@ -0,0 +1,197 @@
<table class="layui-table">
<colgroup>
<col width="150">
<col>
<col width="100">
<col width="100">
</colgroup>
<thead>
<tr>
<th>属性名</th>
<th>描述</th>
<th>类型</th>
<th>默认值</th>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>
组件名称。如 `name:'tabs'`,在使用组件时,即可通过 `layui.tabs` 获得该组件。注:*组件名必须唯一*。
</td>
<td>string</td>
<td>-</td>
</tr>
<tr>
<td>config</td>
<td>
定义组件渲染时的默认配置项。
</td>
<td>object</td>
<td>-</td>
</tr>
<tr>
<td>CONST</td>
<td>
通用常量集,一般存放固定字符,如类名等。如:
```
CONST: {
ELEM: 'layui-tabs',
}
```
上述常量可通过 `component.CONST.ELEM` 获得。
</td>
<td>object</td>
<td>-</td>
</tr>
<tr>
<td>isRenderWithoutElem</td>
<td>
渲染是否无需指定目标元素。即无需设置 `elem` 选项,一般用于渲染即显示的组件类型。
</td>
<td>boolean</td>
<td>
`false`
</td>
</tr>
<tr>
<td>isRenderOnEvent</td>
<td>
渲染是否由事件触发。如 `dropdown` 这类通过点击触发的组件,那么应该设置为 `true`;而诸如 `tabs` 这类初始即展示的组件,则应该设置为 `false`。*推荐根据组件类型始终显式设置对应值*。
</td>
<td>boolean</td>
<td>
`true`
</td>
</tr>
<tr>
<td>isDeepReload</td>
<td>
组件重载时是否允许深度重载,即对重载时选项进行深度合并。
</td>
<td>boolean</td>
<td>
`false`
</td>
</tr>
<tr>
<td colspan="4" style="text-align: center">
<div id="options.callback" lay-pid="options" class="ws-anchor">
[回调函数](#options.callback)
</div>
</td>
</tr>
<tr>
<td>render</td>
<td colspan="3">
组件渲染的逻辑。
```js
render: function() {
// 组件的容器构建、插入等
// …
}
```
也可以通过原型 `component.Class.prototype.render` 进行定义。
</td>
</tr>
<tr>
<td>beforeInit</td>
<td colspan="3">
组件初始化之前的回调函数。
```js
beforeInit: function(options) {
console.log(options); // 获得组件初始化前的配置项
}
```
</td>
</tr>
<tr>
<td>beforeRender</td>
<td colspan="3">
渲染之前的回调函数。
```js
beforeRender: function(options) {
console.log(options); // 获得组件渲染前的配置项
}
```
</td>
</tr>
<tr>
<td>extendsInstance</td>
<td colspan="3">
扩展组件渲染的实例对象的回调函数。如:
```js
extendsInstance: function(that) {
return {
// 关闭组件
close: function() {
that.remove(); // 调用组件原型中的 remove 方法
}
}
}
```
当组件渲染时,即可通过它返回的对象调用实例方法:
```js
var inst = xxx.render(); // 某组件渲染
inst.close(); // 关闭某组件
```
</td>
</tr>
<tr>
<td>events</td>
<td colspan="3">
定义组件各内部事件。
```js
events: function() {
// 亦可包含针对组件的 window, document 等全局事件
// …
}
```
也可以通过原型 `component.Class.prototype.events` 进行定义。
</td>
</tr>
</tbody>
</table>

289
docs/component/index.md Normal file
View File

@ -0,0 +1,289 @@
---
title: 组件构建器 component
toc: true
---
# 组件构建器 <sup>2.10+</sup>
> 组件构建器 `component` 是 2.10 版本新增的重要模块,旨在为 Layui 2 系列版本逐步构建统一 API 规范的组件。
<h2 id="api" lay-toc="{hot: true}">API</h2>
| API | 描述 |
| --- | --- |
| [layui.component(options)](#create) | 创建组件 |
<h3 id="create" lay-toc="{level: 2}">创建组件</h3>
`layui.component(options);`
- 参数 `options` : 基础属性配置项。[#详见属性](#options)
该方法返回一个对象,包含用于组件对外的基础接口,如:组件渲染、重载、事件操作,及构造函数等等。用法示例:
```js
/**
* tabs
* 标签页组件
*/
layui.define('component', function(exports) {
// 创建组件
var component = layui.component({
name: 'tabs', // 组件名称
config: { // 组件默认配置项
// …
},
render: function() { // 组件渲染逻辑
// …
},
// 其他选项
});
// 将创建组件时返回的 `component` 对象作为组件的接口输出
// 组件将继承通用的基础接口,如 render, reload, set 等方法
exports(component.CONST.MOD_NAME, component);
});
```
<h3 id="options" lay-toc="{level: 2}">属性配置</h3>
<div>
{{- d.include("/component/detail/options.md") }}
</div>
<h2 id="export" lay-toc="{}">基础接口 🔥</h2>
通过 `component` 模块创建的组件,均会继承内部定义的「基础对外接口」和「组件渲染的通用选项」。
| 接口 | 描述 |
| --- | --- |
| [component.render(options)](#render) | 组件渲染 |
| [component.reload(id, options)](#reload) | 组件重载 |
| [component.set(options)](#set) | 设置组件渲染时的全局配置项 |
| [component.on(\'event(filter)\', callback)](#on) | 组件的自定义事件 |
| [component.getThis(id)](#getThis) | 获取指定组件的实例对象 |
| component.index | 获得组件的自增索引 |
| component.config | 获得组件渲染时的全局配置项。一般通过 `set` 方法设置 |
| component.cache | 获得组件的缓存数据集。如组件实例 ID 集 |
| [component.CONST](#CONST) | 获得组件的通用常量集。如 `MOD_NAME` 等 |
| [component.Class](#Class) | 获得组件的构造函数。一般用于扩展原型方法 |
> 😊 注:上表中的 `component` 为组件的基础对象,实际使用时,请根据实组件名称进行替换。如 `tabs` 组件,对应的接口则为:`tabs.render(options)` `tabs.on('event(filter)', callback)` 等。
<h3 id="render" lay-toc="{level: 2}">组件渲染</h3>
`component.render(options)`
- 参数 `options` : 组件渲染的配置项。可继承的通用选项见下表:
| 选项 | 描述 |
| --- | --- |
| elem | 件渲染指定的目标元素选择器或 DOM 对象 |
| id | 组件渲染的唯一实例 ID |
| show | 是否初始即渲染组件。通常结合创建组件设定的 `isRenderOnEvent` 选项决定是否启用 |
更多渲染时的选项则由各组件内部单独定义,具体可查阅各组件对应的文档。
```js
// 以 tabs 组件为例
// 渲染
tabs.render({
elem: '#id',
// …
});
```
<h3 id="reload" lay-toc="{level: 2}">组件重载</h3>
`component.reload(id, options)`
- 参数 `id` : 组件实例 ID。
- 参数 `options` : 组件重载的配置项。
该方法可实现对组件的完整重载。部分组件通常还提供了「仅数据重载」,具体可参考各组件对应的文档。
```js
// 以 tabs 组件为例
// 重载 tabs 组件实例
tabs.reload('id', {
// …
})
```
<h3 id="set" lay-toc="{level: 2}">全局设置</h3>
`component.set(options)`
- 参数 `options` : 组件渲染的配置项。
该方法用于全局设置组件渲染时的默认配置项,需在组件渲染之前执行。
```js
// 以 tabs 组件为例
// 全局设置。后续所有渲染均会生效,除非对选项进行覆盖
tabs.set({
trigger: 'mouseenter' // 默认的触发事件
// …
});
// 渲染实例 1
tabs.render({ id: 'id1'}); // 继承全局设置
// 渲染实例 2
tabs.render({
id: 'id2',
trigger: 'click' // 覆盖全局的触发事件
});
```
<h3 id="on" lay-toc="{level: 2}">事件定义</h3>
`component.on('event(id)', callback)`
- 参数 `event(id)` : 是事件的特定结构。 `event` 为事件名,支持的事件见各组件列表。`id` 为组件的实例 ID。
- 参数 `callback` : 事件回调函数。返回的参数由各组件内部单独定义。
具体事件一般由组件内部单独定义,具体可查看各组件对应的文档。
```js
// 以 tabs 组件为例:
// 组件渲染成功后的事件
tabs.on('afterRender(id)', function(data) {
console.log(obj);
});
```
<h3 id="getThis" lay-toc="{level: 2}">获取实例</h3>
`component.getThis(id)`
- 参数 `id` : 组件的实例 ID。
该方法可获取组件渲染时对应的实例,以调用组件内部的原型方法,一般用于在外部对组件进行扩展或重构。
```js
// 以 tabs 组件为例
var tabInstance = tabs.getThis('id');
// 调用内部的标签滚动方法
tabInstance.roll();
```
<h3 id="CONST" lay-toc="{level: 2}">基础常量</h3>
`component.CONST`
获取组件的通用常量集,一般存放固定字符,如类名等。
```js
// 基础常量如下
component.CONST.MOD_NAME; // 组件名称
component.CONST.MOD_INDEX; // 组件自增索引
component.CONST.CLASS_THIS; // layui-this
component.CONST.CLASS_SHOW; // layui-show
component.CONST.CLASS_HIDE; // layui-hide
component.CONST.CLASS_HIDEV; // layui-hide-v
component.CONST.CLASS_DISABLED; // layui-disabled
component.CONST.CLASS_NONE; // layui-none
// 更多常量一般在各组件内部单独定义,以 tabs 组件为例
tabs.CONST.ELEM; // layui-tabs
```
<h3 id="extend" lay-toc="{level: 2}">扩展接口</h3>
除上述「基础接口」外,你也可以对接口进行任意扩展,如:
```js
/**
* 定义组件
*/
layui.define('component', function(exports) {
// 创建组件
var component = layui.component({
name: 'test',
// …
});
// 扩展组件接口
layui.$.extend(component, {
// 以扩展一个关闭组件面板的接口为例
close: function(id) {
var that = component.getThis(id);
if(!that) return this;
that.remove(obj); // 调用原型中的 remove 方法
}
});
// 输出组件接口
exports(component.CONST.MOD_NAME, component);
});
```
```js
/**
* 使用组件(以上述定义的 test 组件为例)
*/
layui.use('test', function() {
var test = layui.test;
// 渲染组件
test.render({
elem: '#id',
id: 'test-1'
});
// 关闭组件面板(通常在某个事件中使用)
test.close('test-1');
});
```
<h3 id="Class" lay-toc="{level: 2}">扩展原型</h3>
`component.Class`
当通过 `layui.component()` 方法创建一个新的组件时,通常需借助 `Class` 构造函数扩展组件原型,以灵活实现组件的个性化定制。但一般不推荐重写 `component.js` 原型中已经定义的基础方法,如:`init, reload, cache`
```
/**
* 定义组件
*/
layui.define('component', function(exports) {
// 创建组件
var component = layui.component({
name: '', // 组件名称
// …
});
// 获取构造器
var Class = component.Class;
// 扩展原型
Class.prototype.xxx = function() {
// …
};
Class.prototype.aaa = function() {
// …
};
// 输出组件接口
exports(component.CONST.MOD_NAME, component);
});
```
通过 `Class` 构造函数也可以对某个组件的原型进行重构,但一般不推荐,因为这可能破坏组件的基础功能。
```
// 以 tabs 组件为例
var tabs = layui.tabs;
// 获得 tabs 组件构造函数
var Class = tabs.Class;
// 重构 tabs 组件内部的 xxx 方法(不推荐)
Class.prototype.xxx = function() {
// …
};
```
您也可以直接参考 tabs 组件源码中关于扩展原型的具体实践。
## 💖 心语
Layui 由于早前欠缺统筹性思维,很多组件自成一体,使得无法对组件进行很好的统一管理。随着版本的迭代,我们也一直在努力尝试改善这一问题,但很多时候,为了向下兼容而不得不保留许多旧有的特性。`component` 模块的初衷正是为了确保组件的一致性,如核心逻辑和 API 设计等,其目的也是为了让 2.x 系列版本尽可能地减少些遗憾,让 Layui 在既定的范式中保持前行。

View File

@ -18,7 +18,7 @@ toc: true
<td>用途:用于更简单快速地构建网页界面</td>
</tr>
<tr>
<td>环境:全部主流 Web 浏览器IE8 以下除外)</td>
<td>环境:<a href="/notes/browser-support.html" target="_blank">详见不同版本的浏览器兼容规划</a> <sup>N</sup></td>
<td>特性:原生态开发 / 轻量级模块化 / 外简内丰 / 开箱即用</td>
</tr>
</tbody>

View File

@ -2,8 +2,10 @@
title: 选项卡组件 tab
toc: true
---
# 选项卡组件
# ~~选项卡组件~~
> 📣 <span style="color: #ff5722;">升级提示:我们在 2.10 版本中新增了全新的 tabs 标签页组件,用于替代原 `element` 模块中的 `tab` 组件,建议过渡到全新的 tabs 组件,旧的 ~~tab~~ 组件将在后续合适的版本中移除。</span> [前往全新 tabs 组件](../tabs/)
> 选项卡组件 `tab` 是指可进行标签页切换的一段容器,常广泛应用于 Web 页面。由于为了向下兼容等诸多历史原因,在 2.x 版本中,`tab` 组件属于 `element` 模块的子集。
@ -159,7 +161,7 @@ tab 组件会在元素加载完毕后,自动对 tab 元素完成一次渲染
```
<div id="test"></div>
<!-- import layui -->
<script>
layui.use(function(){
@ -284,7 +286,7 @@ layui.use(function(){
```
var element = layui.element;
// tab 切换事件
element.on('tab(filter)', function(data){
console.log(this); // 当前 tab 标题所在的原始 DOM 元素
@ -307,7 +309,7 @@ element.on('tab(filter)', function(data){
```
var element = layui.element;
// tab 切换前的事件
element.on('tabBeforeChange(filter)', function(data){
console.log(data.elem); // 得到当前的 tab 容器
@ -333,7 +335,7 @@ element.on('tabBeforeChange(filter)', function(data){
```
var element = layui.element;
// tab 删除事件
element.on('tabDelete(filter)', function(data){
console.log(data.index); // 得到被删除的 tab 项的所在下标

63
docs/tabs/detail/demo.md Normal file
View File

@ -0,0 +1,63 @@
<h3 lay-toc="{level: 2, id: 'examples', hot: true}" class="layui-hide">动态操作</h3>
<pre class="layui-code" lay-options="{preview: true, text: {preview: '动态操作'}, layout: ['preview', 'code'], codeStyle: 'max-height: 520px;', tools: ['full'], done: function(obj){
obj.render();
}}">
<textarea>
{{- d.include("/tabs/examples/demo.md") }}
</textarea>
</pre>
<h3 id="demo-method" lay-toc="{level: 2}">方法渲染</h3>
即通过方法设置 tabs 标签,而非默认的自动渲染页面中的 `class="layui-tabs"` 的容器模板。
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], tools: ['full'], codeStyle: 'max-height: 520px;'}">
<textarea>
{{- d.include("/tabs/examples/method.md") }}
</textarea>
</pre>
<h3 id="demo-card" lay-toc="{level: 2}">卡片风格</h3>
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], codeStyle: 'max-height: 520px;', tools: ['full'], done: function(obj){
obj.render();
}}">
<textarea>
{{- d.include("/tabs/examples/card.md") }}
</textarea>
</pre>
<h3 id="demo-trigger" lay-toc="{level: 2}">自定义事件</h3>
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], codeStyle: 'max-height: 520px;', tools: ['full']}">
<textarea>
{{- d.include("/tabs/examples/trigger.md") }}
</textarea>
</pre>
<h3 id="demo-hash" lay-toc="{level: 2, title: 'HASH 匹配'}">通过 HASH 匹配选中标签</h3>
切换 tabs 标签项后,地址栏同步 `hash`当页面刷新时tabs 仍保持对应的切换状态。
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], codeStyle: 'max-height: 520px;', tools: ['full']}">
<textarea>
{{- d.include("/tabs/examples/hash.md") }}
</textarea>
</pre>
<h3 id="demo-nest" lay-toc="{level: 2}">标签嵌套</h3>
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], codeStyle: 'max-height: 520px;', tools: ['full']}">
<textarea>
{{- d.include("/tabs/examples/nest.md") }}
</textarea>
</pre>
<h3 id="demo-custom" lay-toc="{level: 2, title: '自定义标签'}">给任意元素绑定 tabs 切换功能</h3>
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], codeStyle: 'max-height: 520px;', tools: ['full']}">
<textarea>
{{- d.include("/tabs/examples/custom.md") }}
</textarea>
</pre>

159
docs/tabs/detail/options.md Normal file
View File

@ -0,0 +1,159 @@
<table class="layui-table">
<colgroup>
<col width="150">
<col>
<col width="100">
<col width="100">
</colgroup>
<thead>
<tr>
<th>属性名</th>
<th>描述</th>
<th>类型</th>
<th>默认值</th>
</tr>
</thead>
<tbody>
<tr>
<td>elem</td>
<td>
组件渲染指定的目标元素选择器或 DOM 对象
</td>
<td>string/DOM</td>
<td>-</td>
</tr>
<tr>
<tr>
<td>id</td>
<td>
组件渲染的唯一实例 ID
</td>
<td>string</td>
<td>-</td>
</tr>
<tr>
<td>className</td>
<td>
给主容器追加 CSS 类名,以便自定义样式
</td>
<td>string</td>
<td>-</td>
</tr>
<tr>
<td>trigger</td>
<td>
标签切换的触发事件
</td>
<td>boolean</td>
<td>
`click`
</td>
</tr>
<tr>
<td>headerMode</td>
<td>
标签头部的显示模式。可选值有:
- `auto` 自动模式,根据标签头部是否溢出自动显示不同模式
- `scroll` 始终滚动模式
- `normal` 始终常规模式,不渲染头部滚动结构
</td>
<td>string</td>
<td>
`auto`
</td>
</tr>
<tr>
<td>index</td>
<td>
初始选中的标签索引或标签 `lay-id` 属性值
</td>
<td>number</td>
<td>-</td>
</tr>
<tr>
<td>closable</td>
<td>
是否开启标签项关闭功能
</td>
<td>boolean</td>
<td>
`false`
</td>
</tr>
<tr>
<td>header</td>
<td colspan="3">
设置标签头部列表,通常在「非自动渲染」的场景下使用:
**1. 方法渲染**
`header` 传入一个数组,且成员值为对象,即为方法渲染时传入的头部列表数据。如:
```js
header: [
{ title: 'Tab1' }, // 除 `title` 为必传项外,也可传入其他任意字段。
{ title: 'Tab2' }
]
```
**2. 任意元素渲染**
`header` 传入一个数组,则成员值为元素选择器,即为绑定标签头部列表元素。如:
```js
header: ['#tabsHeader', '>li'],
```
</td>
</tr>
<tr>
<td>body</td>
<td colspan="3">
设置标签内容列表,通常在「非自动渲染」的场景下使用:
**1. 方法渲染**
`body` 传入一个数组,且成员值为对象,即为方法渲染时传入的标签内容列表数据。如:
```js
body: [
{ content: 'Tab1' }, // `content` 为必传项
{ content: 'Tab2' }
]
```
**2. 任意元素渲染**
`body` 传入一个数组,则成员值为元素选择器,即为绑定标签内容列表元素。如:
```js
body: ['#tabsBody', '>div'],
```
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,44 @@
<div class="layui-tabs layui-hide-v" id="demoTabsBeforeChange">
<ul class="layui-tabs-header">
<li lay-id="aaa">Tab1</li>
<li lay-id="bbb">Tab2</li>
<li lay-id="ccc">Tab3</li>
<li lay-id="ddd">Tab4</li>
<li lay-id="eee">Tab5</li>
<li lay-id="fff">Tab6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item">Tab Content-1</div>
<div class="layui-tabs-item">Tab Content-2</div>
<div class="layui-tabs-item">Tab Content-3</div>
<div class="layui-tabs-item">Tab Content-4</div>
<div class="layui-tabs-item">Tab Content-5</div>
<div class="layui-tabs-item">Tab Content-6</div>
</div>
</div>
本示例演示:切换标签时,弹出确认提示。
<!-- import layui -->
<script>
layui.use(function() {
var tabs = layui.tabs;
// 标签实例 ID
var DEMO_TABS_ID = 'demoTabsBeforeChange';
// tabs 切换前的事件
tabs.on(`beforeChange(${DEMO_TABS_ID})`, function(data) {
console.log('beforeChange', data);
// 切换确认提示
layer.confirm(`确定从「当前标签」切换到标签「${this.innerText}」吗?`, function(i) {
tabs.change(DEMO_TABS_ID, data.to.index, true); // 强制切换
layer.close(i); // 关闭确认框
});
// 阻止标签默认关闭
return false;
});
});
</script>

View File

@ -0,0 +1,44 @@
<div class="layui-tabs layui-hide-v" id="demoTabsBeforeClose" lay-options="{closable: true}">
<ul class="layui-tabs-header">
<li lay-id="aaa" lay-closable="false">Tab1</li>
<li lay-id="bbb">Tab2</li>
<li lay-id="ccc">Tab3</li>
<li lay-id="ddd">Tab4</li>
<li lay-id="eee">Tab5</li>
<li lay-id="fff">Tab6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item">Tab Content-1</div>
<div class="layui-tabs-item">Tab Content-2</div>
<div class="layui-tabs-item">Tab Content-3</div>
<div class="layui-tabs-item">Tab Content-4</div>
<div class="layui-tabs-item">Tab Content-5</div>
<div class="layui-tabs-item">Tab Content-6</div>
</div>
</div>
本示例演示:删除标签之前,弹出确认提示。
<!-- import layui -->
<script>
layui.use(function() {
var tabs = layui.tabs;
// 标签实例 ID
var DEMO_TABS_ID = 'demoTabsBeforeClose';
// tabs 切换前的事件
tabs.on(`beforeClose(${DEMO_TABS_ID})`, function(data) {
console.log('beforeClose', data);
// 关闭确认提示
layer.confirm(`确定关闭标签「${this.innerText}」吗?`, function(i) {
tabs.close(DEMO_TABS_ID, data.index, true); // 强制关闭对应的标签项
layer.close(i); // 关闭确认框
});
// 阻止标签默认关闭
return false;
});
});
</script>

View File

@ -0,0 +1,54 @@
#### 普通卡片
<div class="layui-tabs layui-tabs-card" lay-options="{index: 1}">
<ul class="layui-tabs-header">
<li>标题1</li>
<li>标题2</li>
<li><a href="" target="_blank" class="layui-font-blue">跳转项</a></li>
<li class="layui-disabled" lay-unselect>禁选项</li>
<li>标题5</li>
<li>标题6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item">内容-1</div>
<div class="layui-tabs-item">内容-2</div>
<div class="layui-tabs-item">内容-3</div>
<div class="layui-tabs-item">内容-4</div>
<div class="layui-tabs-item">内容-5</div>
<div class="layui-tabs-item">内容-6</div>
</div>
</div>
#### 边框卡片
<div class="layui-tabs layui-tabs-card layui-panel layui-inline">
<ul class="layui-tabs-header layui-bg-tint">
<li class="layui-this">标题1</li>
<li>标题2</li>
<li>标题3</li>
<li>标题4</li>
<li>标题5</li>
<li>标题6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item layui-show">
<div class="layui-form">
<select>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
</select>
</div>
</div>
<div class="layui-tabs-item">2</div>
<div class="layui-tabs-item">3</div>
<div class="layui-tabs-item">4</div>
<div class="layui-tabs-item">5</div>
<div class="layui-tabs-item">6</div>
</div>
</div>
<!-- import layui -->

View File

@ -0,0 +1,30 @@
<div id="demoTabs3">
<style>
#demoTabsHeader .layui-btn.layui-this{border-color: #eee; color: #000; background: none;}
#demoTabsBody .test-item{display: none;}
</style>
<div class="layui-btn-container" id="demoTabsHeader">
<button class="layui-btn layui-this">标题 1</button>
<button class="layui-btn">标题 2</button>
<button class="layui-btn">标题 3</button>
</div>
<div class="layui-panel layui-padding-3" id="demoTabsBody">
<div class="test-item layui-show">内容 111</div>
<div class="test-item">内容 222</div>
<div class="test-item">内容 333</div>
</div>
</div>
<!-- import layui -->
<script>
layui.use(function(){
var tabs = layui.tabs;
// 给任意元素绑定 Tab 功能
tabs.render({
elem: '#demoTabs3',
header: ['#demoTabsHeader', '>button'],
body: ['#demoTabsBody', '>.test-item']
});
});
</script>

109
docs/tabs/examples/demo.md Normal file
View File

@ -0,0 +1,109 @@
<div class="layui-tabs layui-hide-v" id="demoTabs1" lay-options="{closable: true, headerMode:'scroll'}">
<ul class="layui-tabs-header">
<li lay-id="aaa" lay-closable="false">Tab1</li>
<li lay-id="bbb">Tab2</li>
<li lay-id="ccc">Tab3</li>
<li lay-id="ddd">Tab4</li>
<li lay-id="eee">Tab5</li>
<li lay-id="fff">Tab6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item">Tab Content-1</div>
<div class="layui-tabs-item">Tab Content-2</div>
<div class="layui-tabs-item">Tab Content-3</div>
<div class="layui-tabs-item">Tab Content-4</div>
<div class="layui-tabs-item">Tab Content-5</div>
<div class="layui-tabs-item">Tab Content-6</div>
</div>
</div>
🔔 操作提示:在「标签头部」点击鼠标右键,可开启标签操作的更多实用演示
<div class="layui-btn-container">
<button class="layui-btn" onclick="layui.tabs.change('demoTabs1', 'ccc')">切换到Tab3</button>
<button class="layui-btn" onclick="layui.tabs.change('demoTabs1', 1)">切换到:第 2 项</button>
<button class="layui-btn" onclick="layui.tabs.close('demoTabs1', 'ddd')">关闭Tab4</button>
<button class="layui-btn" onclick="layui.tabs.close('demoTabs1', 1)">关闭:第 2 项</button>
<button class="layui-btn" lay-on="add">添加 Tab</button>
</div>
<!-- import layui -->
<script>
layui.use(function() {
var $ = layui.$;
var tabs = layui.tabs;
var util = layui.util;
var dropdown = layui.dropdown;
// 为标签头添加上下文菜单
var dropdownInst = dropdown.render({
elem: '#demoTabs1 .layui-tabs-header>li',
trigger: 'contextmenu',
data: [{
title: '在右侧新增标签页',
action: 'add',
mode: 'after'
}, {
type: '-'
}, {
title: '关闭',
action: 'close',
mode: 'this',
}, {
title: '关闭其他标签页',
action: 'close',
mode: 'other'
}, {
title: '关闭右侧标签页',
action: 'close',
mode: 'right'
}, {
title: '关闭所有标签页',
action: 'close',
mode: 'all'
}],
click: function(data, othis, event) {
var index = this.elem.index(); // 获取活动标签索引
// 新增标签操作
if (data.action === 'add') {
// 在当前活动标签右侧新增标签页
addTabs({
mode: data.mode,
index: index
});
} else if(data.action === 'close') { // 关闭标签操作
if (data.mode === 'this') {
tabs.close('demoTabs1', index); // 关闭当前标签
} else {
tabs.closeMult('demoTabs1', data.mode, index); // 批量关闭标签
}
}
}
});
// 新增随机标签
var addTabs = function(opts) {
var n = Math.random()*1000 | 0; // 演示标记
opts = $.extend({
title: 'New Tab '+ n, // 此处加 n 仅为演示区分,实际应用不需要
content: 'New Tab Content '+ n,
id: 'new-'+ n,
aaa: 'attr-'+ n, // 自定义属性,其中 aaa 可任意命名
done: function(params) {
console.log(params);
dropdownInst.reload();
}
}, opts);
// 添加标签到最后
tabs.add('demoTabs1', opts);
}
// 自定义事件
util.on({
add: function(){
addTabs();
}
});
});

View File

@ -0,0 +1,23 @@
<div class="layui-tabs layui-hide-v" id="demoTabs-hash">
<ul class="layui-tabs-header">
<li lay-id="A1" class="layui-this"><a href="#A1">标题题题题题题1</a></li>
<li lay-id="A2"><a href="#A2">标题题题2</a></li>
<li lay-id="A3"><a href="#A3">标题3</a></li>
<li lay-id="A4"><a href="#A4">标题题题题题题题4</a></li>
<li lay-id="A5"><a href="#A5">标题5</a></li>
<li lay-id="A6"><a href="#A6">标题6</a></li>
<li lay-id="A7"><a href="#A7">标题7</a></li>
<li lay-id="A8"><a href="#A8">标题题题题题题题8</a></li>
</ul>
</div>
<!-- import layui -->
<script>
layui.use(function(){
var tabs = layui.tabs;
// HASH 初始定位
var hash = layui.hash();
tabs.change('demoTabs-hash', hash.href);
});
</script>

View File

@ -0,0 +1,26 @@
<div id="demoTabs2"></div>
<!-- import layui -->
<script>
layui.use(function(){
var tabs = layui.tabs;
// 方法渲染
tabs.render({
elem: '#demoTabs2',
header: [
{ title: 'Tab1' },
{ title: 'Tab2' },
{ title: 'Tab3' }
],
body: [
{ content: 'Tab content 1' },
{ content: 'Tab content 2' },
{ content: 'Tab content 3' }
],
// index: 1, // 初始选中项
// className: 'layui-tabs-card',
// closable: true
});
});
</script>

View File

@ -0,0 +1,40 @@
<div class="layui-tabs layui-tabs-card" lay-options="{headerMode:'normal'}">
<ul class="layui-tabs-header">
<li class="layui-this">标题1</li>
<li>标题2</li>
<li>标题3</li>
</ul>
<div class="layui-tabs-body" style="padding: 16px;">
<div class="layui-tabs-item layui-show">
<div class="layui-tabs" lay-options="{headerMode:'normal'}">
<ul class="layui-tabs-header">
<li class="layui-this">标题 1-1</li>
<li>标题 1-2</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item layui-show">1-1</div>
<div class="layui-tabs-item">1-2</div>
</div>
</div>
</div>
<div class="layui-tabs-item">
<div class="layui-tabs" lay-options="{headerMode:'normal'}">
<ul class="layui-tabs-header">
<li class="layui-this">标题 2-1</li>
<li>标题 2-2</li>
<li>标题 2-3</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item layui-show">2-1</div>
<div class="layui-tabs-item">2-2</div>
<div class="layui-tabs-item">2-3</div>
</div>
</div>
</div>
<div class="layui-tabs-item">3</div>
<div class="layui-tabs-item">4</div>
<div class="layui-tabs-item">5</div>
</div>
</div>
<!-- import layui -->

View File

@ -0,0 +1,43 @@
#### mouseenter 触发
<div class="layui-tabs layui-tabs-card layui-panel" lay-options="{trigger: 'mouseenter'}">
<ul class="layui-tabs-header layui-bg-tint">
<li class="layui-this">标题1</li>
<li>标题2</li>
<li>标题3</li>
<li>标题4</li>
<li>标题5</li>
<li>标题6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item layui-show">内容-1</div>
<div class="layui-tabs-item">内容-2</div>
<div class="layui-tabs-item">内容-3</div>
<div class="layui-tabs-item">内容-4</div>
<div class="layui-tabs-item">内容-5</div>
<div class="layui-tabs-item">内容-6</div>
</div>
</div>
#### mousedown 触发
<div class="layui-tabs layui-tabs-card layui-panel" lay-options="{trigger: 'mousedown'}">
<ul class="layui-tabs-header layui-bg-tint">
<li class="layui-this">标题1</li>
<li>标题2</li>
<li>标题3</li>
<li>标题4</li>
<li>标题5</li>
<li>标题6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item layui-show">内容-1</div>
<div class="layui-tabs-item">内容-2</div>
<div class="layui-tabs-item">内容-3</div>
<div class="layui-tabs-item">内容-4</div>
<div class="layui-tabs-item">内容-5</div>
<div class="layui-tabs-item">内容-6</div>
</div>
</div>
<!-- import layui -->

350
docs/tabs/index.md Normal file
View File

@ -0,0 +1,350 @@
---
title: 标签页组件 tabs
toc: true
---
# 标签页组件 <sup>2.10+</sup>
> `tabs` 是 2.10 版本新增的加强型组件,可替代原 `element` 模块中的 `tab` 组件。tabs 广泛应用于 Web 页面。
<h2 id="examples" lay-toc="{anchor: null}" style="margin-bottom: 0;">示例</h2>
<div class="ws-docs-showcase"></div>
<div>
{{- d.include("/tabs/detail/demo.md") }}
</div>
<h2 id="api" lay-toc="{hot: true}">API</h2>
| API | 描述 |
| --- | --- |
| var tabs = layui.tabs | 获得 `tabs` 模块。|
| [基础接口](../component/#export) | 该组件由 `component` 构建,因此继承其提供的基础接口。|
| [tabs.render(options)](#render) | tabs 组件渲染,核心方法。|
| [tabs.add(id, opts)](#add) | 新增一个标签项。|
| [tabs.close(id, index, force)](#close) | 关闭指定的标签项。|
| [tabs.closeMult(id, mode, index)](#closeMult) | 批量关闭标签项。|
| [tabs.change(id, index, force)](#change) | 切换到指定的标签项。|
| [tabs.data(id)](#data) | 获取当前标签页相关数据。|
| [tabs.getHeaderItem(id, index)](#getHeaderItem) | 获取指定的标签头部项。|
| [tabs.getBodyItem(id, index)](#getBodyItem) | 获取指定的标签内容项。|
| [tabs.refresh(id)](#refresh) | 刷新标签视图。 |
<h3 id="render" lay-toc="{level: 2}">渲染</h3>
`tabs.render(options)`
- 参数 `options` : 基础属性配置项。[#详见属性](#options)
组件支持以下三种渲染方式:
#### 1. 自动渲染
tabs 组件会在元素加载完毕后,自动对 `class="layui-tabs"` 目标元素完成一次渲染,若无法找到默认的目标元素(如:动态插入的标签元素的场景),则可通过该方法完成对标签页的初始化渲染。
```js
// 对 class="layui-tabs" 所在标签进行初始化渲染
tabs.render();
```
#### 2. 方法渲染
通过方法动态渲染一个 tabs 组件,无需在 HTML 中书写标签页的 HTML 结构。
```js
<div id="test"></div>
<!-- import layui -->
<script>
layui.use(function(){
var tabs = layui.tabs;
tabs.render({
elem: '#test',
header: [
{ title: 'Tab1' },
{ title: 'Tab2' }
],
body: [
{ content: 'Tab content 1' },
{ content: 'Tab content 2' }
],
index: 0, // 初始选中标签索引
})
});
</script>
```
#### 3. 为任意元素渲染 tabs 功能
`header``body` 参数传入元素选择器时,可为任意元素绑定标签切换功能。
```js
// 给任意元素绑定 Tab 功能
tabs.render({
elem: '#demoTabs3', // 目标主容器选择器
header: ['#demoTabsHeader', '>button'], // 标签头部主元素选择器、标签头部列表选择器
body: ['#demoTabsBody', '>.test-item'] // 标签内容主元素选择器、标签内容列表选择器
});
```
具体用法可直接参考上述示例:[给任意元素绑定 tabs 切换功能](#demo-custom)
<h3 id="options" lay-toc="{level: 2}">属性</h3>
<div>
{{- d.include("/tabs/detail/options.md") }}
</div>
<h3 id="add" class="ws-anchor ws-bold">新增标签</h3>
`tabs.add(id, opts)`
- 参数 `id` : 组件的实例 ID
- 参数 `opts` : 标签配置项。可选项详见下表
| opts | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| title | 标签标题。必填项 | string | - |
| content | 标签内容。必填项 | string | - |
| id | 标签的 `lay-id` 属性值 | string | - |
| index | 活动标签的索引或 `lay-id` 属性值,默认取当前选中标签的索引 | number | - |
| mode | 标签的插入方式。支持以下可选值:<ul><li>`append` 插入标签到最后</li> <li>`prepend` 插入标签到最前</li> <li>`before` 在活动标签前插入</li> <li>`after` 在活动标签后插入</li></ul> | string | `append` |
| closable | 标签是否可关闭。初始值取决于 `options.closable` | boolean | `false` |
| headerItem | 自定义标签头部元素,如 `headerItem: '<li></li>'` | string | - |
| bodyItem | 自定义标签内容元素,如 `bodyItem: '<div></div>'` | string | - |
| done | 标签添加成功后执行的回调函数 | Function | - |
该方法用于给对应的 tabs 实例新增一个标签
```js
tabs.add('test', {
title: 'New Tab 1',
content: 'New Tab Content 1',
});
```
<h3 id="close" class="ws-anchor ws-bold">关闭标签</h3>
`tabs.close(id, index, force)`
- 参数 `id` : 组件的实例 ID
- 参数 `index` : 标签索引或标签的 `lay-id` 属性值
- 参数 `force` : 是否强制关闭。若设置 `true` 将忽略 `beforeClose` 事件行为。默认 `false`
该方法用于关闭指定的标签项。
```js
tabs.close('test', 3); // 关闭索引为 3 的标签
tabs.close('test', 3, true); // 强制关闭索引为 3 的标签
tabs.close('test', 'abc'); // 关闭 lay-id="abc" 的标签
```
<h3 id="closeMult" class="ws-anchor ws-bold">批量关闭标签</h3>
`tabs.closeMult(id, mode, index)`
- 参数 `id` : 组件的实例 ID
- 参数 `mode` : 关闭方式。支持以下可选值:
| mode | 描述 |
| --- | --- |
| other | 关闭除当前标签外的所有标签 |
| right | 关闭当前标签及右侧标签 |
| all | 关闭所有标签 |
- 参数 `index` : 活动标签的索引或 `lay-id` 属性值,默认取当前选中标签的索引。一般用于标签右键事件。
该方法用于批量关闭标签。
```js
tabs.closeMult(id, 'other'); // 关闭除当前标签外的所有标签
tabs.closeMult(id, 'other', 3); // 关闭除索引为 3 的标签外的所有标签
tabs.closeMult(id, 'right'); // 关闭当前标签及右侧标签
tabs.closeMult(id, 'right', 3); // 关闭索引为 3 的标签的右侧所有标签
tabs.closeMult(id, 'all'); // 关闭所有标签
```
<h3 id="change" class="ws-anchor ws-bold">切换标签</h3>
`tabs.change(id, index, force)`
- 参数 `id` : 组件的实例 ID
- 参数 `index` : 标签索引或标签的 `lay-id` 属性值
- 参数 `force` : 是否强制切换。若设置 `true` 将忽略 `beforeChange` 事件行为。默认 false
该方法用于切换到指定的标签项。
```js
tabs.change('test', 3); // 切换到索引为 3 的标签
tabs.change('test', 3, true); // 强制切换到索引为 3 的标签
tabs.change('test', 'abc'); // 切换到 lay-id="abc" 的标签
tabs.change('test', 'abc', true); // 强制切换到 lay-id="abc" 的标签
```
<h3 id="data" class="ws-anchor ws-bold">获取标签相关数据</h3>
`tabs.data(id)`
- 参数 `id` : 组件的实例 ID
该方法用于获取标签相关数据。
```js
var data = tabs.data('test');
console.log(data);
```
返回的 `data` 包含以下字段:
```js
{
options, // 标签配置信息
container, // 标签容器的相关元素
thisHeaderItem, // 当前标签头部项
thisBodyItem, // 当前标签内容项
index, // 当前标签索引
length, // 当前标签数
}
```
<h3 id="getHeaderItem" class="ws-anchor ws-bold">获取标签头部项</h3>
`tabs.getHeaderItem(id, index)`
- 参数 `id` : 组件的实例 ID
- 参数 `index` : 标签索引或标签的 `lay-id` 属性值
该方法用于获取标签头部项元素。
```js
var headerItem = tabs.getHeaderItem('test', 3); // 获取索引为 3 的标签头部项元素
```
<h3 id="getBodyItem" class="ws-anchor ws-bold">获取标签内容项</h3>
`tabs.getBodyItem(id, index)`
- 参数 `id` : 组件的实例 ID
- 参数 `index` : 标签索引或标签的 `lay-id` 属性值
该方法用于获取标签内容项元素。
```js
var bodyItem = tabs.getBodyItem('test', 3); // 获取索引为 3 的标签内容项元素
```
<h3 id="refresh" class="ws-anchor ws-bold">刷新标签视图</h3>
`tabs.refresh(id)`
- 参数 `id` : 组件的实例 ID
该方法用于刷新标签视图,如标签头部的滚动结构等,一般通过非 API 方式对标签进行修改的场景中使用。
```js
tabs.refresh('test'); // 刷新标签视图
```
<h2 id="on" lay-toc="{hot: true}">事件</h2>
`tabs.on('event(id)', callback)`
- 参数介绍详见 `component` 组件的[事件定义](../component/#on)。以下是组件提供的 `event` 事件列表
| event | 描述 |
| --- | --- |
| [afterRender](#on-afterRender) | 标签渲染后的事件 |
| [beforeChange](#on-beforeChange) | 标签切换前的事件 |
| [afterChange](#on-afterChange) | 标签切换后的事件 |
| [beforeClose](#on-beforeClose) | 标签关闭前的事件 |
| [afterClose](#on-afterClose) | 标签关闭后的事件 |
<h3 id="on-afterRender" class="ws-anchor ws-bold">标签渲染后的事件</h3>
`tabs.on('afterRender(id)', callback)`
标签渲染成功后触发。
```js
tabs.on('afterRender(testID)', function(data){
console.log(data); // 标签相关数据
});
```
<h3 id="on-beforeChange" class="ws-anchor ws-bold">标签切换前的事件</h3>
`tabs.on('beforeChange(id)', callback)`
标签在切换前触发,通过在事件中 `return false` 可阻止默认标签切换行为。通常和 `tabs.change()` 方法搭配使用。
```js
// tabs 切换前的事件
tabs.on(`beforeChange(testID)`, function(data) {
console.log(data); // 标签相关数据
console.log(data.from.index); // 切换前的选中标签索引
console.log(data.from.headerItem); // 切换前的选中标签头部项
console.log(data.to.index); // 切换后的选中标签索引
console.log(data.to.headerItem); // 切换后的选中标签头部项
// 阻止标签默认关闭
return false;
});
```
示例演示:
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], codeStyle: 'max-height: 520px;', tools: ['full'], done: function(obj){
obj.render();
}}">
<textarea>
{{- d.include("/tabs/examples/beforeChange.md") }}
</textarea>
</pre>
<h3 id="on-afterChange" class="ws-anchor ws-bold">标签切换后的事件</h3>
`tabs.on('afterChange(id)', callback)`
标签成功切换后触发。
```js
// tabs 切换后的事件
tabs.on('afterChange(testID)', function(data) {
console.log(data);
});
```
<h3 id="on-beforeClose" class="ws-anchor ws-bold">标签关闭前的事件</h3>
`tabs.on('beforeClose(id)', callback)`
标签在切换前触发,通过在事件中 `return false` 可阻止默认标签切换行为。通常和 `tabs.close()` 方法搭配使用。
<pre class="layui-code" lay-options="{preview: true, layout: ['preview', 'code'], codeStyle: 'max-height: 520px;', tools: ['full'], done: function(obj){
obj.render();
}}">
<textarea>
{{- d.include("/tabs/examples/beforeClose.md") }}
</textarea>
</pre>
<h3 id="on-afterClose" class="ws-anchor ws-bold">标签关闭后的事件</h3>
`tabs.on('afterClose(id)', callback)`
标签被成功关闭后触发。
```js
// tabs 关闭后的事件
tabs.on('afterClose(testID)', function(data) {
console.log(data);
});
```
## 💖 心语
tabs 是通过 component 重构的首个组件,它来自于最早试图发布的 Layui 3.0(后因为 3.0 技术路线的变化,而整理放至 2.10+ 版本中),目的是将 element 模块中的 tab 组件进行解耦,增强其可扩展性。为了给开发者必要的时间缓冲,我们会将旧 tab 组件仍然保留在后续的若干版本中,但会在合适的时机对旧 tab 组件进行剔除,建议开发者尽量提前过渡到当前新的 tabs 组件。

289
examples/tabs.html Normal file
View File

@ -0,0 +1,289 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>标签页组件 - Layui</title>
<link rel="stylesheet" href="../src/css/layui.css">
</head>
<body>
<div class="layui-container layui-padding-3 layui-text">
<h2>动态操作</h2>
<div class="layui-tabs layui-hide-v" id="demoTabs1" lay-options="{closable: true, headerMode:'scroll'}">
<ul class="layui-tabs-header">
<li lay-id="aaa" lay-closable="false">Tab1</li>
<li lay-id="bbb">Tab2</li>
<li lay-id="ccc">Tab3</li>
<li lay-id="ddd">Tab4</li>
<li lay-id="eee">Tab5</li>
<li lay-id="fff">Tab6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item">Tab Content-1</div>
<div class="layui-tabs-item">Tab Content-2</div>
<div class="layui-tabs-item">Tab Content-3</div>
<div class="layui-tabs-item">Tab Content-4</div>
<div class="layui-tabs-item">Tab Content-5</div>
<div class="layui-tabs-item">Tab Content-6</div>
</div>
</div>
<div class="layui-btn-container">
<button class="layui-btn" onclick="layui.tabs.change('demoTabs1', 'ccc')">切换到Tab3</button>
<button class="layui-btn" onclick="layui.tabs.change('demoTabs1', 1)">切换到:第 2 项</button>
<button class="layui-btn" onclick="layui.tabs.close('demoTabs1', 'ddd')">关闭Tab4</button>
<button class="layui-btn" onclick="layui.tabs.close('demoTabs1', 1)">关闭:第 2 项</button>
<button class="layui-btn" lay-on="add">添加 Tab</button>
</div>
<h2>方法渲染</h2>
<div id="demoTabs2"></div>
<h2>卡片风格</h2>
<div class="layui-tabs layui-tabs-card" lay-options="{index: 1}">
<ul class="layui-tabs-header">
<li>标题1</li>
<li>标题2</li>
<li><a href="" target="_blank" class="layui-font-blue">跳转项</a></li>
<li class="layui-disabled" lay-unselect>禁选项</li>
<li>标题5</li>
<li>标题6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item">内容-1</div>
<div class="layui-tabs-item">内容-2</div>
<div class="layui-tabs-item">内容-3</div>
<div class="layui-tabs-item">内容-4</div>
<div class="layui-tabs-item">内容-5</div>
<div class="layui-tabs-item">内容-6</div>
</div>
</div>
<h3>卡片 + 边框</h3>
<div class="layui-tabs layui-tabs-card layui-panel layui-inline">
<ul class="layui-tabs-header layui-bg-tint">
<li class="layui-this">标题1</li>
<li>标题2</li>
<li>标题3</li>
<li>标题4</li>
<li>标题5</li>
<li>标题6</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item layui-show">
<div class="layui-form">
<select>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
</select>
</div>
</div>
<div class="layui-tabs-item">2</div>
<div class="layui-tabs-item">3</div>
<div class="layui-tabs-item">4</div>
<div class="layui-tabs-item">5</div>
<div class="layui-tabs-item">6</div>
</div>
</div>
<h2>标签 HASH 定位</h2>
<div class="layui-tabs layui-hide-v" id="demoTabs-hash">
<ul class="layui-tabs-header">
<li lay-id="A1" class="layui-this"><a href="#A1">标题题题题题题1</a></li>
<li lay-id="A2"><a href="#A2">标题题题2</a></li>
<li lay-id="A3"><a href="#A3">标题3</a></li>
<li lay-id="A4"><a href="#A4">标题题题题题题题4</a></li>
<li lay-id="A5"><a href="#A5">标题5</a></li>
<li lay-id="A6"><a href="#A6">标题6</a></li>
<li lay-id="A7"><a href="#A7">标题7</a></li>
<li lay-id="A8"><a href="#A8">标题题题题题题题8</a></li>
</ul>
</div>
<h2>标签嵌套</h2>
<div class="layui-tabs layui-tabs-card" lay-options="{headerMode:'normal'}">
<ul class="layui-tabs-header">
<li class="layui-this">标题1</li>
<li>标题2</li>
<li>标题3</li>
</ul>
<div class="layui-tabs-body" style="padding: 16px;">
<div class="layui-tabs-item layui-show">
<div class="layui-tabs" lay-options="{headerMode:'normal'}">
<ul class="layui-tabs-header">
<li class="layui-this">标题 1-1</li>
<li>标题 1-2</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item layui-show">1-1</div>
<div class="layui-tabs-item">1-2</div>
</div>
</div>
</div>
<div class="layui-tabs-item">
<div class="layui-tabs" lay-options="{headerMode:'normal'}">
<ul class="layui-tabs-header">
<li class="layui-this">标题 2-1</li>
<li>标题 2-2</li>
<li>标题 2-3</li>
</ul>
<div class="layui-tabs-body">
<div class="layui-tabs-item layui-show">2-1</div>
<div class="layui-tabs-item">2-2</div>
<div class="layui-tabs-item">2-3</div>
</div>
</div>
</div>
<div class="layui-tabs-item">3</div>
<div class="layui-tabs-item">4</div>
<div class="layui-tabs-item">5</div>
</div>
</div>
<h2>给任意元素绑定 tabs 切换功能</h2>
<div id="demoTabs3">
<style>
#demoTabsHeader .layui-btn.layui-this{border-color: #eee; color: #000; background: none;}
#demoTabsBody .test-item{display: none;}
</style>
<div class="layui-btn-container" id="demoTabsHeader">
<button class="layui-btn layui-this">标题 1</button>
<button class="layui-btn">标题 2</button>
<button class="layui-btn">标题 3</button>
</div>
<div class="layui-panel layui-padding-3" id="demoTabsBody">
<div class="test-item layui-show">内容 111</div>
<div class="test-item">内容 222</div>
<div class="test-item">内容 333</div>
</div>
</div>
</div>
<script src="../src/layui.js"></script>
<script>
layui.use(function() {
var tabs = layui.tabs
var util = layui.util;
var layer = layui.layer;
var dropdown = layui.dropdown;
// 自定义事件
util.on({
add: function(){
var n = Math.random()*1000 | 0; // 演示标记
//添加标签
tabs.add('demoTabs1', {
title: 'New Tab '+ n, // 此处加 n 仅为演示区分,实际应用不需要
content: 'New Tab Content '+ n,
id: 'new-'+ n,
aaa: 'attr-'+ n, // 自定义属性,其中 aaa 可任意命名
// mode: 'curr',
done: function(params) {
console.log(params);
}
});
}
});
// tabs 关闭前的事件
tabs.on('beforeClose(demoTabs1)', function(data) {
console.log('beforeClose', data);
// 关闭确认提示
layer.confirm(`确定关闭标签「${this.innerText}」吗?`, function(i) {
tabs.close('demoTabs1', data.index, true); // 强制关闭对应的标签项
layer.close(i); // 关闭确认框
});
return false; // 阻止标签默认关闭
});
// tabs 关闭后的事件
tabs.on('afterClose(demoTabs1)', function(data) {
console.log('afterClose', data);
});
// tabs 切换前的事件
tabs.on('beforeChange(demoTabs1)', function(data) {
console.log('beforeChange', data);
// 切换确认提示
/*layer.confirm(`确定从「当前标签」切换到标签「${this.innerText}」吗?`, function(i) {
tabs.change('demoTabs1', data.to.index, true); // 强制切换
layer.close(i); // 关闭确认框
});
return false; // 阻止标签默认关闭*/
});
// tabs 切换后的事件
tabs.on('afterChange(demoTabs1)', function(data) {
console.log('afterChange', data);
});
// 为标签头添加上下文菜单
dropdown.render({
elem: '#demoTabs1 .layui-tabs-header>li',
trigger: 'contextmenu',
data: [{
title: '关闭',
type: 'this'
}, {
title: '关闭其他标签页',
type: 'other'
}, {
title: '关闭右侧标签页',
type: 'right'
}, {
type: '-'
}, {
title: '关闭所有标签页',
type: 'all'
}],
click: function(data, othis, event) {
var index = this.elem.index();
if (data.type === 'this') {
tabs.close('demoTabs1', index); // 关闭当前标签
} else {
tabs.closeMult('demoTabs1', data.type, index); // 批量关闭标签
}
}
});
// 方法渲染
tabs.render({
elem: '#demoTabs2',
header: [
{ title: 'Tab1' },
{ title: 'Tab2' },
{ title: 'Tab3' }
],
body: [
{ content: 'Tab content 1' },
{ content: 'Tab content 2' },
{ content: 'Tab content 3' }
],
// index: 1, //初始选中项
// className: 'layui-tabs-card',
// closable: true
});
// HASH 初始定位
var hash = layui.hash();
tabs.change('demoTabs-hash', hash.href);
// 给任意元素绑定 Tab 功能
tabs.render({
elem: '#demoTabs3',
header: ['#demoTabsHeader', '>button'],
body: ['#demoTabsBody', '>.test-item']
});
});
</script>
</body>
</html>

View File

@ -20,24 +20,23 @@ pre{white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; w
/** 初始化全局标签 **/
body{line-height: 1.6; color: #333; color: rgba(0,0,0,.85); font: 14px Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
hr{height: 0; line-height: 0; margin: 10px 0; padding: 0; border: none; border-bottom: 1px solid #eee; clear: both; overflow: hidden; background: none;}
a{color: #333; text-decoration:none;}
a:hover{color: #777;}
a cite{font-style: normal; *cursor:pointer;}
a{color: #333; text-decoration: none;}
a cite{font-style: normal;}
/** 基础通用 **/
.layui-border-box, .layui-border-box *{box-sizing: border-box;}
/* 消除第三方ui可能造成的冲突 */.layui-box, .layui-box *{box-sizing: content-box;}
.layui-clear{clear: both; *zoom: 1;}
.layui-clear:after{content:'\20'; clear:both; *zoom:1; display:block; height:0;}
.layui-clear{clear: both;}
.layui-clear:after{content:'\20'; clear:both; display:block; height:0;}
.layui-clear-space{word-spacing: -5px;}
.layui-inline{position: relative; display: inline-block; *display:inline; *zoom:1; vertical-align: middle;}
.layui-inline{position: relative; display: inline-block; vertical-align: middle;}
/* 三角形 */.layui-edge{position: relative; display: inline-block; vertical-align: middle; width: 0; height: 0; border-width: 6px; border-style: dashed; border-color: transparent; overflow: hidden;}
.layui-edge-top{top: -4px; border-bottom-color: #999; border-bottom-style: solid;}
.layui-edge-right{border-left-color: #999; border-left-style: solid;}
.layui-edge-bottom{top: 2px; border-top-color: #999; border-top-style: solid;}
.layui-edge-left{border-right-color: #999; border-right-style: solid;}
/* 单行溢出省略 */.layui-elip{text-overflow: ellipsis; overflow: hidden; white-space: nowrap;}
/* 屏蔽选中 */.layui-unselect,.layui-icon, .layui-disabled{-moz-user-select: none; -webkit-user-select: none; -ms-user-select: none;}
/* 屏蔽选中 */.layui-unselect,.layui-icon, .layui-disabled{user-select: none;}
/* 禁用 */.layui-disabled,.layui-disabled:hover{color: #d2d2d2 !important; cursor: not-allowed !important;}
/* 纯圆角 */.layui-circle{border-radius: 100%;}
.layui-show{display: block !important;}
@ -515,14 +514,18 @@ a cite{font-style: normal; *cursor:pointer;}
.layui-col-space32>*{padding: 16px;}
/* 内边距 */
/*
* 内边距
*/
.layui-padding-1{padding: 4px !important;}
.layui-padding-2{padding: 8px !important;}
.layui-padding-3{padding: 16px !important;}
.layui-padding-4{padding: 32px !important;}
.layui-padding-5{padding: 48px !important;}
/* 外边距 */
/*
* 外边距
*/
.layui-margin-1{margin: 4px !important;}
.layui-margin-2{margin: 8px !important;}
.layui-margin-3{margin: 16px !important;}
@ -538,7 +541,7 @@ a cite{font-style: normal; *cursor:pointer;}
.layui-input,
.layui-select,
.layui-textarea,
.layui-upload-button{outline: none; -webkit-appearance: none; transition: all .3s; -webkit-transition: all .3s; box-sizing: border-box;}
.layui-upload-button{outline: none; appearance: none; -webkit-appearance: none; transition: all .3s; -webkit-transition: all .3s; box-sizing: border-box;}
/* 引用 */
.layui-elem-quote{margin-bottom: 10px; padding: 15px; line-height: 1.8; border-left: 5px solid #16b777; border-radius: 0 2px 2px 0; background-color: #fafafa;}
@ -670,8 +673,8 @@ hr.layui-border-black{border-width: 0 0 1px;}
.layui-text ol ul > li{list-style-type: disc;}
.layui-text ul li > p:first-child,
.layui-text ol li > p:first-child{margin-top: 0; margin-bottom: 0;}
.layui-text a:not(.layui-btn){color: #01AAED;}
.layui-text a:not(.layui-btn):hover{text-decoration: underline;}
.layui-text :where(a:not(.layui-btn)){color: #01AAED;}
.layui-text :where(a:not(.layui-btn):hover){text-decoration: underline;}
.layui-text blockquote:not(.layui-elem-quote){margin: 15px 0; padding: 5px 15px; border-left: 5px solid #eee;}
.layui-text pre > code:not(.layui-code){display: block; padding: 15px; font-family: "Courier New",Consolas,"Lucida Console", monospace;}
@ -709,7 +712,7 @@ hr.layui-border-black{border-width: 0 0 1px;}
* 按钮
*/
.layui-btn{display: inline-block; vertical-align: middle; height: 38px; line-height: 38px; border: 1px solid transparent; padding: 0 18px; background-color: #16baaa; color: #fff; white-space: nowrap; text-align: center; font-size: 14px; border-radius: 2px; cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none;}
.layui-btn{display: inline-block; vertical-align: middle; height: 38px; line-height: 38px; border: 1px solid transparent; padding: 0 18px; background-color: #16baaa; color: #fff; white-space: nowrap; text-align: center; font-size: 14px; border-radius: 2px; cursor: pointer; user-select: none;}
.layui-btn:hover{opacity: 0.8; filter:alpha(opacity=80); color: #fff;}
.layui-btn:active{opacity: 1; filter:alpha(opacity=100);}
.layui-btn+.layui-btn{margin-left: 10px;}
@ -763,8 +766,8 @@ hr.layui-border-black{border-width: 0 0 1px;}
.layui-form input[type=radio]{display: none;}
.layui-form *[lay-ignore]{display: initial;}
.layui-form-item{position: relative; margin-bottom: 15px; clear: both; *zoom: 1;}
.layui-form-item:after{content:'\20'; clear: both; *zoom: 1; display: block; height:0;}
.layui-form-item{position: relative; margin-bottom: 15px; clear: both;}
.layui-form-item:after{content:'\20'; clear: both; display: block; height:0;}
.layui-form-label{position: relative; float: left; display: block; padding: 9px 15px; width: 80px; font-weight: 400; line-height: 20px; text-align: right;}
.layui-form-label-col{display: block; float: none; padding: 9px 0; line-height: 20px; text-align: left;}
.layui-form-item .layui-inline{margin-bottom: 5px; margin-right: 10px;}
@ -837,7 +840,7 @@ hr.layui-border-black{border-width: 0 0 1px;}
.layui-input-wrap .layui-input-number .layui-icon:hover{font-weight: 700;}
.layui-input-wrap .layui-input[type="number"]::-webkit-outer-spin-button,
.layui-input-wrap .layui-input[type="number"]::-webkit-inner-spin-button{-webkit-appearance: none !important;}
.layui-input-wrap .layui-input[type="number"]{-moz-appearance: textfield;}
.layui-input-wrap .layui-input[type="number"]{-moz-appearance: textfield; -webkit-appearance: textfield; appearance: textfield;}
.layui-input-wrap .layui-input.layui-input-number-out-of-range,
.layui-input-wrap .layui-input.layui-input-number-invalid{color:#ff5722;}
@ -872,7 +875,6 @@ hr.layui-border-black{border-width: 0 0 1px;}
/* 复选框 */
.layui-form-checkbox{position: relative; display: inline-block; vertical-align: middle; height: 30px; line-height: 30px; margin-right: 10px; padding-right: 30px; background-color: #fff; cursor: pointer; font-size: 0; -webkit-transition: .1s linear; transition: .1s linear; box-sizing: border-box;}
.layui-form-checkbox:hover{}
.layui-form-checkbox > *{display: inline-block; vertical-align: middle;}
.layui-form-checkbox > div{padding: 0 11px; font-size: 14px; border-radius: 2px 0 0 2px; background-color: #d2d2d2; color: #fff; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;}
.layui-form-checkbox > div > .layui-icon{line-height: normal}
@ -965,7 +967,7 @@ hr.layui-border-black{border-width: 0 0 1px;}
}
/** 分页 **/
.layui-laypage{display: inline-block; *display: inline; *zoom: 1; vertical-align: middle; margin: 10px 0; font-size: 0;}
.layui-laypage{display: inline-block; vertical-align: middle; margin: 10px 0; font-size: 0;}
.layui-laypage>a:first-child,
.layui-laypage>a:first-child em{border-radius: 2px 0 0 2px;}
.layui-laypage>a:last-child,
@ -978,7 +980,7 @@ hr.layui-border-black{border-width: 0 0 1px;}
.layui-laypage button,
.layui-laypage select{border: 1px solid #eee;}
.layui-laypage a,
.layui-laypage span{display: inline-block; *display: inline; *zoom: 1; vertical-align: middle; padding: 0 15px; height: 28px; line-height: 28px; margin: 0 -1px 5px 0; background-color: #fff; color: #333; font-size: 12px;}
.layui-laypage span{display: inline-block; vertical-align: middle; padding: 0 15px; height: 28px; line-height: 28px; margin: 0 -1px 5px 0; background-color: #fff; color: #333; font-size: 12px;}
.layui-laypage a[data-page]{color: #333;}
.layui-laypage a{text-decoration: none !important; cursor: pointer;}
.layui-laypage a:hover{color: #16baaa;}
@ -1019,12 +1021,6 @@ hr.layui-border-black{border-width: 0 0 1px;}
.layui-table tr{transition: all .3s; -webkit-transition: all .3s;}
.layui-table th{text-align: left; font-weight: 600;}
.layui-table thead tr,
.layui-table-header,
.layui-table-tool,
.layui-table-total,
.layui-table-total tr,
.layui-table-patch{}
.layui-table-mend{background-color: #fff;}
.layui-table-hover,
.layui-table-click,
@ -1292,18 +1288,55 @@ body .layui-table-tips .layui-layer-content{background: none; padding: 0; box-sh
/* 下拉菜单 */
.layui-dropdown{position: absolute; left: -999999px; top: -999999px; z-index: 77777777; margin: 5px 0; min-width: 100px;}
.layui-dropdown:before{content:""; position: absolute; width: 100%; height: 6px; left: 0; top: -6px;}
.layui-dropdown-shade{top: 0; left: 0; width: 100%; height: 100%; _height: expression(document.body.offsetHeight+"px"); position: fixed; _position: absolute; pointer-events: auto;}
.layui-dropdown-shade{top: 0; left: 0; width: 100%; height: 100%; position: fixed; pointer-events: auto;}
/* Tabs 标签页 */
.layui-tabs{position: relative;}
.layui-tabs *{box-sizing: border-box;}
.layui-tabs.layui-hide-v{overflow: hidden;}
.layui-tabs-header{position: relative; left: 0; height: 40px; padding: 0 !important; white-space: nowrap; font-size: 0; transition: all .16s; -webkit-transition: all .16s;}
.layui-tabs-header:after,
.layui-tabs-scroll:after{content: ""; position: absolute; left: 0; bottom: 0; z-index: 0; width: 100%; border-bottom: 1px solid #eee;}
.layui-tabs-header li{position: relative; display: inline-block; vertical-align: middle; line-height: 40px; margin: 0 !important; padding: 0 16px; text-align: center; cursor: pointer; font-size: 14px; transition: all .16s; -webkit-transition: all .16s;}
.layui-tabs-header li:first-child{margin-left: 0;}
.layui-tabs-header li a{display: block; padding: 0 16px; margin: 0 -16px; color: inherit;}
.layui-tabs-header li a:hover{text-decoration: none;}
.layui-tabs-header li:hover,
.layui-tabs-header .layui-this{color: #16baaa;}
.layui-tabs-header .layui-this:after{content: ""; position: absolute; left:0; top: 0; z-index: 1; width: 100%; height: 100%; border-bottom: 3px solid #16baaa; box-sizing: border-box; pointer-events: none;}
.layui-tabs-header .layui-badge,
.layui-tabs-header .layui-badge-dot{left: 5px; top: -1px;}
.layui-tabs-scroll{position: relative; overflow: hidden; padding: 0 40px;}
.layui-tabs-scroll .layui-tabs-header:after{display: none; content: none; border: 0;}
.layui-tabs-bar .layui-icon{position: absolute; left: 0; top: 0; z-index: 3; width: 40px; height: 100%; line-height: 40px; border: 1px solid #eee; text-align: center; cursor: pointer; box-sizing: border-box; background-color: #fff; box-shadow: 2px 0 5px 0 rgb(0 0 0 / 6%);}
.layui-tabs-bar .layui-icon-next{left: auto; right: 0; box-shadow: -2px 0 5px 0 rgb(0 0 0 / 6%);}
.layui-tabs-header li .layui-tabs-close{position: relative; display: inline-block; width: 16px; height: 16px; line-height: 18px; margin-left: 8px; top: 0px; text-align: center; font-size: 12px; color: #959595; border-radius: 50%; font-weight: 700; transition: all .16s; -webkit-transition: all .16s;}
.layui-tabs-header li .layui-tabs-close:hover{ background-color: #ff5722; color: #fff;}
.layui-tabs-body{padding: 16px 0;}
.layui-tabs-item{display: none;}
/* tabs 卡片风格 */
.layui-tabs-card>.layui-tabs-header .layui-this{background-color: #fff;}
.layui-tabs-card>.layui-tabs-header .layui-this:after{border: 1px solid #eee; border-bottom-color: #fff; border-radius: 2px 2px 0 0;}
.layui-tabs-card>.layui-tabs-header li:first-child.layui-this:after{margin-left: -1px;}
.layui-tabs-card>.layui-tabs-header li:last-child.layui-this:after{margin-right: -1px;}
.layui-tabs-card.layui-panel>.layui-tabs-header .layui-this:after{border-top: 0; border-radius: 0;}
.layui-tabs-card.layui-panel>.layui-tabs-body{padding: 16px;}
/** 导航菜单 **/
.layui-nav{position: relative; padding: 0 15px; background-color: #2f363c; color: #fff; border-radius: 2px; font-size: 0; box-sizing: border-box;}
.layui-nav *{font-size: 14px;}
.layui-nav .layui-nav-item{position: relative; display: inline-block; *display: inline; *zoom: 1; margin-top: 0; list-style: none; vertical-align: middle; line-height: 60px;}
.layui-nav .layui-nav-item{position: relative; display: inline-block; margin-top: 0; list-style: none; vertical-align: middle; line-height: 60px;}
.layui-nav .layui-nav-item a{display: block; padding: 0 20px; color: #fff; color: rgba(255,255,255,.7); transition: all .3s; -webkit-transition: all .3s;}
.layui-nav-bar,
.layui-nav .layui-this:after{content: ""; position: absolute; left: 0; top: 0; width: 0; height: 3px; background-color: #16b777; transition: all .2s; -webkit-transition: all .2s; pointer-events: none;}
.layui-nav-bar{z-index: 1000;}
.layui-nav[lay-bar="disabled"] .layui-nav-bar{display: none;}
.layui-nav[lay-bar="disabled"].layui-this:after{}
.layui-nav .layui-this a,
.layui-nav .layui-nav-item a:hover{color: #fff; text-decoration: none;}
.layui-nav .layui-this:after{top: auto; bottom: 0; width: 100%;}
@ -1381,7 +1414,7 @@ body .layui-table-tips .layui-layer-content{background: none; padding: 0; box-sh
.layui-tab[overflow]>.layui-tab-title{overflow: hidden;}
.layui-tab .layui-tab-title{position: relative; left: 0; height: 40px; white-space: nowrap; font-size: 0; transition: all .2s; -webkit-transition: all .2s;}
.layui-tab .layui-tab-title:after{content: ""; border-bottom-color: #eee; border-bottom-width: 1px; border-style: none none solid; bottom: 0; left: 0; right: auto; top: auto; pointer-events: none; position: absolute; width: 100%; z-index: 8;}
.layui-tab .layui-tab-title li{display: inline-block; *display: inline; *zoom: 1; vertical-align: middle; font-size: 14px; transition: all .2s; -webkit-transition: all .2s;}
.layui-tab .layui-tab-title li{display: inline-block; vertical-align: middle; font-size: 14px; transition: all .2s; -webkit-transition: all .2s;}
.layui-tab .layui-tab-title li{position: relative; line-height: 40px; min-width: 65px; margin: 0; padding: 0 15px; text-align: center; cursor: pointer;}
.layui-tab .layui-tab-title li a{display: block; padding: 0 15px; margin: 0 -15px;}
.layui-tab-title .layui-this{color: #000;}

View File

@ -62,6 +62,7 @@
tree: 'tree', // 树结构
table: 'table', // 表格
treeTable: 'treeTable', // 树表
tabs: 'tabs', // 标签页
element: 'element', // 常用元素操作
rate: 'rate', // 评分组件
colorpicker: 'colorpicker', // 颜色选择器
@ -71,6 +72,7 @@
util: 'util', // 工具块
code: 'code', // 代码修饰器
jquery: 'jquery', // DOM 库(第三方)
component: 'component', // 组件构建器
all: 'all',
'layui.all': 'layui.all' // 聚合标识(功能性的,非真实模块)
@ -401,13 +403,14 @@
var data = {
path: [],
search: {},
hash: (hash.match(/[^#](#.*$)/) || [])[1] || ''
hash: (hash.match(/[^#](#.*$)/) || [])[1] || '',
href: ''
};
if(!/^#\//.test(hash)) return data; // 禁止非路由规范
if (!/^#/.test(hash)) return data; // 禁止非路由规范
hash = hash.replace(/^#\//, '');
data.href = '/' + hash;
hash = hash.replace(/^#/, '');
data.href = hash;
hash = hash.replace(/([^#])(#.*$)/, '$1').split('/') || [];
// 提取 Hash 结构

View File

@ -3,12 +3,13 @@
* Code 预览组件
*/
layui.define(['lay', 'util', 'element', 'form'], function(exports){
layui.define(['lay', 'util', 'element', 'tabs', 'form'], function(exports){
"use strict";
var $ = layui.$;
var util = layui.util;
var element = layui.element;
var tabs = layui.tabs;
var form = layui.form;
var layer = layui.layer;
var hint = layui.hint();
@ -403,6 +404,9 @@ layui.define(['lay', 'util', 'element', 'form'], function(exports){
render: function(){
form.render(thisItemBody.find('.layui-form'));
element.render();
tabs.render({
elem: ['.'+ CONST.ELEM_PREVIEW, '.layui-tabs'].join(' ')
});
}
});
},3);

238
src/modules/component.js Normal file
View File

@ -0,0 +1,238 @@
/**
* component
* Layui 2 组件构建器
*/
layui.define(['jquery', 'lay'], function(exports) {
"use strict";
var $ = layui.$;
var lay = layui.lay;
// export
exports('component', function(settings) {
// 默认设置
settings = $.extend(true, {
isRenderWithoutElem: false, // 渲染是否无需指定目标元素
isRenderOnEvent: true, // 渲染是否仅由事件触发。--- 推荐根据组件类型始终显式设置对应值
isDeepReload: false // 是否默认为深度重载
}, settings);
// 组件名
var MOD_NAME = settings.name;
var MOD_INDEX = 'layui_'+ MOD_NAME +'_index'; // 组件索引名
var MOD_ID = 'lay-' + MOD_NAME + '-id'; // 用于记录组件实例 id 的属性名
// 组件基础对外接口
var component = {
config: {}, // 全局配置项,一般通过 component.set() 设置
index: layui[MOD_NAME] ? (layui[MOD_NAME].index + 10000) : 0, // 组件索引
// 通用常量集,一般存放固定字符,如类名等
CONST: $.extend(true, {
MOD_NAME: MOD_NAME,
MOD_INDEX: MOD_INDEX,
CLASS_THIS: 'layui-this',
CLASS_SHOW: 'layui-show',
CLASS_HIDE: 'layui-hide',
CLASS_HIDEV: 'layui-hide-v',
CLASS_DISABLED: 'layui-disabled',
CLASS_NONE: 'layui-none'
}, settings.CONST),
// 设置全局项
set: function(options) {
var that = this;
$.extend(true, that.config, options);
return that;
},
// 事件
on: function(events, callback) {
return layui.onevent.call(this, MOD_NAME, events, callback);
}
};
// 操作当前实例
var instance = function() {
var that = this;
var options = that.config;
var id = options.id;
// 实例对象
var inst = {
config: options,
id: id,
// 重置实例
reload: function(options) {
that.reload.call(that, options);
}
};
// 扩展实例对象的回调
if (typeof settings.extendsInstance === 'function') {
$.extend(true, inst, settings.extendsInstance.call(that));
}
// 返回实例对象
return inst;
};
// 构造器
var Class = function(options) {
var that = this;
that.index = ++component.index; // 每创建一个实例,下标自增
// 扩展配置项:传入选项 -> 全局选项 -> 默认选项 = 当前选项
that.config = $.extend(true, {}, that.config, component.config, options);
// 初始化之前的回调
if (typeof settings.beforeInit === 'function') {
settings.beforeInit.call(that, that.config);
}
// 初始化
that.init();
};
// 默认配置
Class.prototype.config = settings.config;
// 重载实例
Class.prototype.reload = function(options, type) {
var that = this;
$.extend(settings.isDeepReload, that.config, options);
that.init(true, type);
};
// 初始化准备(若由事件触发渲染,则必经此步)
Class.prototype.init = function(rerender, type){
var that = this;
var options = that.config;
var elem = $(options.elem);
// 若 elem 非唯一,则拆分为多个实例
if (elem.length > 1) {
layui.each(elem, function() {
component.render($.extend({}, options, {
elem: this
}));
});
return that;
}
// 合并 lay-options 属性上的配置信息
$.extend(true, options, lay.options(elem[0]));
// 若重复执行 render则视为 reload 处理
if (!rerender && elem.attr(MOD_ID)) {
var newThat = instance.getThis(elem.attr(MOD_ID));
if (!newThat) return;
return newThat.reload(options, type);
}
options.elem = $(options.elem);
// 初始化 id 属性 - 优先取 options.id > 元素 id > 自增索引
options.id = lay.hasOwn(options, 'id') ? options.id : (
elem.attr('id') || that.index
);
// 记录当前实例对象
instance.that[options.id] = that;
// 渲染之前的回调
if (typeof settings.beforeRender === 'function') {
settings.beforeRender.call(that, options);
}
// 执行渲染
var render = function() {
component.cache.id[options.id] = null; // 记录所有实例 id用于批量操作如 resize
elem.attr(MOD_ID, options.id); // 目标元素已渲染过的标记
that.render(rerender); // 渲染核心
};
// 若绑定元素不存在
if (!elem[0]) {
return settings.isRenderWithoutElem ? render() : null; // 渲染是否无需指定目标元素
};
// 执行渲染 - 是否初始即渲染组件
if((settings.isRenderOnEvent && options.show) || !settings.isRenderOnEvent) {
render();
}
// 事件
typeof settings.events === 'function' && that.events();
};
// 组件必传项
Class.prototype.render = settings.render; // 渲染
Class.prototype.events = settings.events; // 事件
// 元素操作缓存
Class.prototype.cache = function(key, value) {
var that = this;
var options = that.config;
var elem = options.elem;
if (!elem) return;
var CACHE_NAME = 'lay_'+ MOD_NAME + '_cache';
var cache = elem.data(CACHE_NAME) || {};
if (value === undefined) return cache[key];
cache[key] = value;
elem.data(CACHE_NAME, cache);
};
// 缓存所有实例对象
instance.that = {};
// 获取当前实例对象
instance.getThis = component.getThis = function(id) {
if (id === undefined) {
throw new Error('ID argument required');
}
return instance.that[id];
};
// 组件缓存
component.cache = {
id: {}
};
// 用于扩展原型
component.Class = Class;
/**
* 组件完整重载
* @param {string} id - 实例 id
* @param {Object} options - 配置项
* @returns
*/
component.reload = function(id, options) {
var that = instance.getThis(id);
if (!that) return;
that.reload(options);
return instance.call(that);
};
/**
* 组件渲染
* @param {Object} options - 配置项
* @returns
*/
component.render = function(options) {
var inst = new Class(options);
return instance.call(inst);
};
return component;
});
});

9724
src/modules/jquery.js vendored

File diff suppressed because it is too large Load Diff

792
src/modules/tabs.js Normal file
View File

@ -0,0 +1,792 @@
/**
* tabs
* 标签页组件
*/
layui.define('component', function(exports) {
'use strict';
var $ = layui.$;
// 创建组件
var component = layui.component({
name: 'tabs', // 组件名
// 默认配置
config: {
elem: '.layui-tabs',
trigger: 'click', // 标签切换的触发事件
headerMode: 'auto' // 标签头部的显示模式 auto | scroll | normal
},
CONST: {
ELEM: 'layui-tabs',
HEADER: 'layui-tabs-header',
CLOSE: 'layui-tabs-close',
BODY: 'layui-tabs-body',
ITEM: 'layui-tabs-item',
CARD: 'layui-tabs-card'
},
isRenderOnEvent: false,
// 渲染
render: function() {
var that = this;
var options = that.config;
// 标签页元素项
that.headerElem = ['.'+ component.CONST.HEADER + ':eq(0)', '>li'];
that.bodyElem = ['.'+ component.CONST.BODY + ':eq(0)', '>.'+ component.CONST.ITEM];
// 获取标签容器中的 header body 相关元素
that.getContainer = function() {
var elem = that.documentElem || options.elem;
return {
header: {
elem: elem.find(that.headerElem[0]),
items: elem.find(that.headerElem.join(''))
},
body: {
elem: elem.find(that.bodyElem[0]),
items: elem.find(that.bodyElem.join(''))
}
};
};
// 若 header 选项类型为数组
if (layui.type(options.header) === 'array') {
if (options.header.length === 0) return;
// 给任意元素绑定 tabs 切换功能
if (typeof options.header[0] === 'string') {
that.headerElem = options.header.concat();
that.documentElem = $(document);
} else { // 方法传值渲染
that.elemView = $('<div class="layui-tabs"></div>');
if (options.className) that.elemView.addClass(options.className);
var headerElem = $('<ul class="layui-tabs-header"></ul>');
var bodyElem = $('<div class="layui-tabs-body"></div>');
// 生成标签项
layui.each(options.header, function(i, item){
var elemHeaderItem = that.renderHeaderItem(item);
headerElem.append(elemHeaderItem);
});
layui.each(options.body, function(i, item){
var elemBodyItem = that.renderBodyItem(item);
bodyElem.append(elemBodyItem);
});
that.elemView.append(headerElem).append(bodyElem);
options.elem.html(that.elemView);
}
} else {
that.renderClose(); // 初始化标签关闭结构
}
// 若 body 选项类型为数组
if (layui.type(options.body) === 'array') {
if (typeof options.body[0] === 'string') {
that.documentElem = $(document);
that.bodyElem = options.body.concat();
}
}
// 初始选中项
var data = that.data();
if ('index' in options && data.index != options.index) {
that.change(that.findHeaderItem(options.index), true);
} else if (data.index === -1) { // 初始选中项为空时,默认选中第一个
that.change(that.findHeaderItem(0), true);
}
// 初始化滚动结构
that.roll('auto');
// 清除隐藏占位
if (options.elem.hasClass(component.CONST.CLASS_HIDEV)) {
options.elem.removeClass(component.CONST.CLASS_HIDEV);
}
// 回调
typeof options.afterRender === 'function' && options.afterRender(data);
// 渲染成功后的事件
layui.event.call(
options.elem[0],
component.CONST.MOD_NAME,
'afterRender('+ options.id +')',
data
);
},
// 事件
events: function() {
var that = this;
var options = that.config;
var container = that.getContainer();
var MOD_NAME = component.CONST.MOD_NAME;
var TRIGGER_NAMESPACE = '.lay_'+ MOD_NAME + '_trigger';
var delegatedElement = that.documentElem ? container.header.elem : options.elem;
// 标签头部事件
var trigger = options.trigger + TRIGGER_NAMESPACE;
var elemHeaderItem = that.documentElem ? that.headerElem[1] : that.headerElem.join('');
delegatedElement.off(trigger).on(trigger, elemHeaderItem, function() {
that.change($(this));
});
// 窗口 resize 事件
if (!inner.onresize) {
var timer;
$(window).on('resize', function() {
clearTimeout(timer);
timer = setTimeout(function(){
layui.each(component.cache.id, function(key) {
var that = component.getThis(key);
if(!that) return;
that.roll('init');
});
}, 50);
});
inner.onresize = true;
}
}
});
// 内部变量集
var inner = {};
/**
* 扩展组件原型方法
*/
var Class = component.Class;
/**
* 增加标签
* @param {Object} opts
* @param {string} opts.title - 标签标题
* @param {string} opts.content - 标签内容
* @param {string} opts.id - 标签的 lay-id 属性值
* @param {string} [opts.index] - 活动标签索引默认取当前选中标签的索引
* @param {('append'|'prepend'|'after'|'before')} [opts.mode='append'] - 标签插入方式
* @param {string} [opts.closable] - 标签是否可关闭初始值取决于 options.closable
* @param {string} [opts.headerItem] - 自定义标签头部元素
* @param {string} [opts.bodyItem] - 自定义标签内容元素
* @param {Function} [opts.done] - 标签添加成功后执行的回调函数
*/
Class.prototype.add = function(opts) {
var that = this;
var options = that.config;
var container = that.getContainer();
var newHeaderItem = that.renderHeaderItem(opts);
var newBodyItem = that.renderBodyItem(opts);
// 插入方式
if (/(before|after)/.test(opts.mode)) { // 在活动标签前后插入
var data = that.data();
var hasOwnIndex = opts.hasOwnProperty('index');
var headerItem = hasOwnIndex ? that.findHeaderItem(opts.index) : data.thisHeaderItem;
var bodyItem = hasOwnIndex ? that.findBodyItem(opts.index) : data.thisHeaderItem;
headerItem[opts.mode](newHeaderItem);
bodyItem[opts.mode](newBodyItem);
} else { // 在标签最前后插入
var mode = ({
prepend: 'prepend', // 插入标签到最前
append: 'append' // 插入标签到最后
})[opts.mode || 'append'] || 'append';
container.header.elem[mode](newHeaderItem);
container.body.elem[mode](newBodyItem);
}
// 将插入项切换为当前标签
that.change(newHeaderItem, true);
// 回调
var params = that.data();
typeof opts.done === 'function' && opts.done(params);
};
/**
* 关闭指定标签
* @param {Object} thisHeaderItem - 当前标签头部项元素
* @param {boolean} force - 是否强制删除
*/
Class.prototype.close = function(thisHeaderItem, force) {
if(!thisHeaderItem || !thisHeaderItem[0]) return;
var that = this;
var options = that.config;
var index = thisHeaderItem.index();
if (!thisHeaderItem[0]) return;
// 标签是否不可关闭
if (thisHeaderItem.attr('lay-closable') === 'false') {
return;
}
// 当前标签相关数据
var params = that.data();
// 标签关闭前的事件。若非强制关闭,可则根据事件的返回结果决定是否关闭
if (!force) {
var closable = layui.event.call(
thisHeaderItem[0],
component.CONST.MOD_NAME,
'beforeClose('+ options.id +')',
$.extend(params, {
index: thisHeaderItem.index()
})
);
// 是否阻止关闭
if (closable === false) {
return;
}
}
// 如果关闭的是当前标签,则更换当前标签索引
if (thisHeaderItem.hasClass(component.CONST.CLASS_THIS)) {
if (thisHeaderItem.next()[0]) {
that.change(thisHeaderItem.next(), true);
} else if(thisHeaderItem.prev()[0]) {
that.change(thisHeaderItem.prev(), true);
}
}
// 移除元素
thisHeaderItem.remove();
that.findBodyItem(index).remove();
that.roll('auto', index);
// 获取当前标签相关数据
var params = that.data();
// 标签关闭后的事件
layui.event.call(
params.thisHeaderItem[0],
component.CONST.MOD_NAME,
'afterClose('+ options.id +')',
params
);
};
/**
* 批量关闭标签
* @see tabs.close
*/
Class.prototype.closeMult = function(mode, index) {
var that = this;
var options = that.config;
var container = that.getContainer();
var data = that.data();
var headers = container.header.items;
var bodys = container.body.items;
var DISABLED_CLOSE_SELECTOR = '[lay-closable="false"]'; // 不可关闭标签选择器
var FILTER = ':not('+ DISABLED_CLOSE_SELECTOR +')'; // 不可关闭标签过滤器
index = index === undefined ? data.index : index;
// 将标签头 lay-closable 属性值同步到 body 项
headers.each(function(i) {
var othis = $(this);
var closableAttr = othis.attr('lay-closable');
if (closableAttr) {
bodys.eq(i).attr('lay-closable', closableAttr);
}
});
// 若当前选中标签也允许关闭,则尝试寻找不可关闭的标签并将其选中
if (data.thisHeaderItem.attr('lay-closable') !== 'false') {
if(mode === 'all' || !mode){
var nextHeader = headers.filter(':gt('+ data.index +')'+ DISABLED_CLOSE_SELECTOR).eq(0);
var prevHeader = $(headers.filter(':lt('+ data.index +')'+ DISABLED_CLOSE_SELECTOR).get().reverse()).eq(0);
if (nextHeader[0]) {
that.change(nextHeader, true);
} else if(prevHeader[0]) {
that.change(prevHeader, true);
}
} else if(index !== data.index) { // 自动切换到活动标签(功能可取消)
that.change(that.findHeaderItem(index), true);
}
}
// 执行批量关闭标签
if (mode === 'other') { // 关闭其他标签
headers.eq(index).siblings(FILTER).remove();
bodys.eq(index).siblings(FILTER).remove();
} else if(mode === 'right') { // 关闭右侧标签
headers.filter(':gt('+ index +')'+ FILTER).remove();
bodys.filter(':gt('+ index +')'+ FILTER).remove();
} else { // 关闭所有标签
headers.filter(FILTER).remove();
bodys.filter(FILTER).remove();
}
that.roll('auto');
// 回调
var params = that.data();
// 标签关闭后的事件
layui.event.call(
params.thisHeaderItem[0],
component.CONST.MOD_NAME,
'afterClose('+ options.id +')',
params
);
};
/**
* 切换标签
* @param {Object} thisHeaderItem - 当前标签头部项元素
* @param {boolean} [force=false] - 是否强制切换
* @returns
*/
Class.prototype.change = function(thisHeaderItem, force) {
if (!thisHeaderItem || !thisHeaderItem[0]) return;
var that = this;
var options = that.config;
var index = thisHeaderItem.index();
var thatA = thisHeaderItem.find('a');
// 是否存在跳转链接
var isLink = typeof thatA.attr('href') === 'string' && thatA.attr('target') === '_blank';
// 是否不允许选中
var unselect = typeof thisHeaderItem.attr('lay-unselect') === 'string';
// 不满足切换的条件
if (isLink || unselect) {
return;
}
// 当前标签相关数据
var params = that.data();
// 标签关闭前的事件。若非强制关闭,可则根据事件的返回结果决定是否关闭
if (!force) {
var enable = layui.event.call(
thisHeaderItem[0],
component.CONST.MOD_NAME,
'beforeChange('+ options.id +')',
$.extend(params, {
from: {
index: params.index,
headerItem: params.thisHeaderItem
},
to: {
index: thisHeaderItem.index(),
headerItem: thisHeaderItem
}
})
);
// 是否阻止切换
if (enable === false) {
return;
}
}
// 执行标签头部切换
thisHeaderItem.addClass(component.CONST.CLASS_THIS).siblings()
.removeClass(component.CONST.CLASS_THIS);
// 执行标签内容切换
that.findBodyItem(index).addClass(component.CONST.CLASS_SHOW)
.siblings().removeClass(component.CONST.CLASS_SHOW);
that.roll('auto', index);
// 重新获取标签相关数据
var params = that.data();
// 标签切换后的事件
layui.event.call(
params.thisHeaderItem[0],
component.CONST.MOD_NAME,
'afterChange('+ options.id +')',
params
);
};
/**
* 渲染标签头部项
* @param {Object} opts - 标签项配置信息
*/
Class.prototype.renderHeaderItem = function(opts) {
var that = this;
var options = that.config;
var headerItem = $(opts.headerItem || options.headerItem || '<li></li>');
headerItem.html(opts.title || 'New Tab');
// 追加属性
layui.each(opts, function(key, value){
if(/^(title|content|mode|done)$/.test(key)) return;
headerItem.attr('lay-'+ key, value);
});
// 追加标签关闭元素
that.appendClose(headerItem, opts);
return headerItem;
};
/**
* 渲染标签内容项
* @param {Object} opts - 标签项配置信息
*/
Class.prototype.renderBodyItem = function(opts) {
var that = this
var options = that.config
var bodyItem = $(opts.bodyItem || options.bodyItem || '<div class="'+ component.CONST.ITEM +'"></div>');
bodyItem.html(opts.content || '');
return bodyItem;
};
/**
* 给某一个标签项追加可关闭元素
* @param {Object} headerItem - 标签项元素
* @param {Object} opts - 标签项配置信息
*/
Class.prototype.appendClose = function(headerItem, opts) {
var that = this
var options = that.config;
if(!options.closable) return;
opts = opts || {};
// 不可关闭项
if (opts.closable === 'false' || headerItem.attr('lay-closable') === 'false') {
return;
}
// 可关闭项追加关闭按钮
if (!headerItem.find('.'+ component.CONST.CLOSE)[0]) {
var close = $('<i class="layui-icon layui-icon-close layui-unselect '+ component.CONST.CLOSE +'"></i>');
close.on('click', function(){
that.close($(this).parent());
return false;
});
headerItem.append(close);
}
};
// 渲染标签可关闭元素
Class.prototype.renderClose = function() {
var that = this;
var options = that.config;
var container = that.getContainer();
var hasDel = that.cache('close');
// 是否开启关闭
if (options.closable) {
if (!hasDel) {
container.header.items.each(function(){
that.appendClose($(this));
});
that.cache('close', true);
}
} else if(hasDel) {
container.header.items.each(function() {
$(this).find('.'+ component.CONST.CLOSE).remove();
});
}
};
/**
* 标签头滚动
* @param {('auto'|'prev'|'next'|'init')} [mode='next'] - 滚动方式
* @param {number} index - 标签索引默认取当前选中标签的索引值
* @returns
*/
Class.prototype.roll = function(mode, index) {
var that = this;
var options = that.config;
var container = that.getContainer();
var headerElem = container.header.elem;
var headerItems = container.header.items;
var scrollWidth = headerElem.prop('scrollWidth'); // 实际总长度
var outerWidth = Math.ceil(headerElem.outerWidth()); // 可视区域的长度
var tabsLeft = headerElem.data('left') || 0;
var scrollMode = options.headerMode === 'scroll'; // 标签头部是否始终保持滚动模式
// 让选中标签始终保持在可视区域
var rollToVisibleArea = function() {
index = isNaN(index) ? that.data().index : index;
var thisItemElem = headerItems.eq(index);
if (!thisItemElem[0]) return;
// 当前标签的相对水平坐标值
var thisLeft = Math.ceil(thisItemElem.position().left);
var padding = 1; // 让边界额外保持一定间距
// 当选中标签溢出在可视区域「左侧」时
var countWidth = thisLeft - (thisItemElem.prev().outerWidth() || 0); // 始终空出上一个标签
if (countWidth > 0) countWidth = countWidth - padding;
// 左侧临界值
if (tabsLeft + countWidth < 0) {
tabsLeft = countWidth >= 0 ? countWidth : 0; // 标签的复原位移不能超出 0
return headerElem.css('left', -tabsLeft).data('left', -tabsLeft);
}
// 当选中标签溢出在可视区域「右侧」时,
var countWidth = thisLeft + thisItemElem.outerWidth()
+ (thisItemElem.next().outerWidth() || 0) + padding; // 始终空出下一个标签
// 右侧临界值
if (tabsLeft + countWidth - outerWidth > 0) {
tabsLeft = countWidth - outerWidth;
headerElem.css('left', -tabsLeft).data('left', -tabsLeft);
}
};
// css 类名
var CLASS_SCROLL = 'layui-tabs-scroll';
var CLASS_BAR = 'layui-tabs-bar';
var CLASS_BAR_ICON = ['layui-icon-prev', 'layui-icon-next'];
// 滚动结构
var rollElem = {
elem: $('<div class="'+ CLASS_SCROLL +' layui-unselect"></div>'),
bar: $([
'<div class="'+ CLASS_BAR +'">',
'<i class="layui-icon '+ CLASS_BAR_ICON[0] +'" lay-mode="prev"></i>',
'<i class="layui-icon '+ CLASS_BAR_ICON[1] +'" lay-mode="next"></i>',
'</div>'
].join(''))
};
// 不渲染头部滚动结构
if (options.headerMode === 'normal') return;
// 是否渲染滚动结构
var elemScroll = headerElem.parent('.'+ CLASS_SCROLL);
if (scrollMode || (!scrollMode && scrollWidth > outerWidth)) {
if (!elemScroll[0]) {
if (options.elem.hasClass(component.CONST.CARD)) {
rollElem.elem.addClass(component.CONST.CARD);
}
headerElem.wrap(rollElem.elem);
headerElem.after(rollElem.bar);
// 点击左右箭头
rollElem.bar.children().on('click', function(){
var othis = $(this);
var mode = othis.attr('lay-mode');
if ($(this).hasClass(component.CONST.CLASS_DISABLED)) return;
mode && that.roll(mode);
});
}
} else if(!scrollMode) {
if (elemScroll[0]) {
elemScroll.find('.'+ CLASS_BAR).remove();
headerElem.unwrap().css('left', 0).data('left', 0);
} else {
return;
}
}
// 初始化滚动模式
if (mode === 'init') return;
// 重新获取
scrollWidth = headerElem.prop('scrollWidth') // 实际总长度
outerWidth = headerElem.outerWidth() // 可视区域的长度
elemScroll = headerElem.parent('.'+ CLASS_SCROLL);
// 左箭头(往右滚动)
if (mode === 'prev') {
// 当前的 left 减去可视宽度,用于与上一轮的页签比较
var prevLeft = -tabsLeft - outerWidth;
if(prevLeft < 0) prevLeft = 0;
headerItems.each(function(i, item){
var li = $(item);
var left = Math.ceil(li.position().left);
if (left >= prevLeft) {
headerElem.css('left', -left).data('left', -left);
return false;
}
});
} else if(mode === 'auto') { // 自动识别滚动
rollToVisibleArea();
} else { // 右箭头(往左滚动) 默认 next
headerItems.each(function(i, item){
var li = $(item);
var left = Math.ceil(li.position().left);
if (left + li.outerWidth() >= outerWidth - tabsLeft) {
headerElem.css('left', -left).data('left', -left);
return false;
}
});
}
// 同步箭头状态
tabsLeft = headerElem.data('left') || 0;
// 左
elemScroll.find('.'+ CLASS_BAR_ICON[0])[
tabsLeft < 0 ? 'removeClass' : 'addClass'
](component.CONST.CLASS_DISABLED);
// 右
elemScroll.find('.'+ CLASS_BAR_ICON[1])[
parseFloat(tabsLeft + scrollWidth) - outerWidth > 0
? 'removeClass'
: 'addClass'
](component.CONST.CLASS_DISABLED);
};
/**
* 根据 id index 获取相关标签头部项
* @param {number|string} index - 标签索引或 id
*/
Class.prototype.findHeaderItem = function(index) {
if(!(
typeof index === 'number'
|| (typeof index === 'string' && index)
)) return;
var headerItems = this.getContainer().header.items;
var item = headerItems.filter('[lay-id="'+ index +'"]');
return item[0] ? item : headerItems.eq(index);
};
/**
* 根据 index 获取相关标签内容项
* @param {number} index - 标签索引
*/
Class.prototype.findBodyItem = function(index) {
return this.getContainer().body.items.eq(index);
};
/**
* 返回给回调的公共信息
* @returns
*/
Class.prototype.data = function() {
var that = this;
var options = that.config;
var container = that.getContainer();
var thisHeaderItem = container.header.items.filter('.'+ component.CONST.CLASS_THIS);
var index = thisHeaderItem.index();
return {
options: options, // 标签配置信息
container: container, // 标签容器的相关元素
thisHeaderItem: thisHeaderItem, // 当前标签头部项
thisBodyItem: that.findBodyItem(index), // 当前标签内容项
index: index, // 当前标签索引
length: container.header.items.length // 当前标签数
}
};
// 扩展组件接口
$.extend(component, {
/**
* 添加标签
* @param {string} id - 渲染时的实例 ID
* @param {Object} opts - 添加标签的配置项详见 Class.prototype.add
*/
add: function(id, opts) {
var that = component.getThis(id);
if(!that) return;
that.add(opts);
},
/**
* 关闭标签
* @param {string} id - 渲染时的实例 ID
* @param {number} index - 标签索引
* @param {boolean} [force=false] - 是否强制关闭
*/
close: function(id, index, force) {
var that = component.getThis(id);
if(!that) return;
if(index === undefined) index = that.data().index; // index 若不传,则表示关闭当前标签
that.close(that.findHeaderItem(index), force);
},
/**
* 关闭多个标签
* @param {string} id - 渲染时的实例 ID
* @param {('other'|'right'|'all')} [mode="all"] - 关闭方式
* @param {number} index - 活动标签的索引默认取当前选中标签的索引一般用于标签右键事件
*/
closeMult: function(id, mode, index, force) {
var that = component.getThis(id);
if(!that) return;
that.closeMult(mode, index, force);
},
/**
* 切换标签
* @param {string} id - 渲染时的实例 ID
* @param {number} index - 标签索引
*/
change: function(id, index, force) {
var that = component.getThis(id);
if(!that) return;
that.change(that.findHeaderItem(index), force);
},
/**
* 获取标签信息
* @param {string} id - 渲染时的实例 ID
*/
data: function(id) {
var that = component.getThis(id);
return that ? that.data() : {};
},
/**
* 获取标签指定头部项
* @param {string} id - 渲染时的实例 ID
* @param {number} index - 标签索引
* @returns
*/
getHeaderItem: function(id, index) {
var that = component.getThis(id);
if(!that) return;
return that.findHeaderItem(index);
},
/**
* 获取标签指定内容项
* @param {string} id - 渲染时的实例 ID
* @param {number} index - 标签索引
* @returns
*/
getBodyItem: function(id, index) {
var that = component.getThis(id);
if(!that) return;
return that.findBodyItem(index);
},
/**
* 刷新标签视图结构
* @param {string} id - 渲染时的实例 ID
*/
refresh: function(id) {
var that = component.getThis(id);
if (!that) return;
that.roll('auto');
}
});
// 初始化渲染
$(function() {
component.render();
});
exports(component.CONST.MOD_NAME, component);
});