完成部分短信发送界面
parent
c59c9c906d
commit
3a7b15dd34
@ -0,0 +1,98 @@
|
|||||||
|
class Message {
|
||||||
|
constructor(messageList) {
|
||||||
|
this.messages = new Map();
|
||||||
|
this.setMessages(messageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages(messageList) {
|
||||||
|
if (messageList instanceof Map) {
|
||||||
|
this.messages = messageList;
|
||||||
|
} else {
|
||||||
|
this.messages.clear();
|
||||||
|
for (let key in messageList) {
|
||||||
|
this.messages.set(key, messageList[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localize(msgId, args) {
|
||||||
|
if (this.messages.has(msgId)) {
|
||||||
|
return this.replaceVariables(this.messages.get(msgId) || '{' + msgId + '}', args);
|
||||||
|
} else {
|
||||||
|
return '{' + msgId + '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias for compatibility
|
||||||
|
text(msgId, args) {
|
||||||
|
return this.localize(msgId, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceVariables(template, params = []) {
|
||||||
|
let result = template;
|
||||||
|
let paramId;
|
||||||
|
result = result.replace(/(?<!\\)\$\{(\w+)\}/g, (...matches) => {
|
||||||
|
paramId = parseInt(matches[1]) - 1;
|
||||||
|
console.log(paramId);
|
||||||
|
if (params[paramId]) {
|
||||||
|
return params[paramId];
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static makeGlobal(messageList = []) {
|
||||||
|
window.msg = new Message(messageList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message.makeGlobal();
|
||||||
|
|
||||||
|
class Config {
|
||||||
|
constructor(configList) {
|
||||||
|
this.configs = new Map();
|
||||||
|
this.setConfigs(configList);
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfigs(configList) {
|
||||||
|
if (configList instanceof Map) {
|
||||||
|
this.configs = configList;
|
||||||
|
} else {
|
||||||
|
this.configs.clear();
|
||||||
|
for (let key in configList) {
|
||||||
|
this.configs.set(key, configList[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(configId, defaultValue = null) {
|
||||||
|
if (this.configs.has(configId)) {
|
||||||
|
return this.configs.get(configId)
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static makeGlobal(configList = []) {
|
||||||
|
window.config = new Config(configList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.makeGlobal();
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const messages = document.querySelector('script[type="application/json"][data-messages]');
|
||||||
|
if (messages) {
|
||||||
|
const messageList = JSON.parse(messages.textContent);
|
||||||
|
window.msg.setMessages(messageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configs = document.querySelector('script[type="application/json"][data-configs]');
|
||||||
|
if (configs) {
|
||||||
|
const configList = JSON.parse(configs.textContent);
|
||||||
|
window.config.setConfigs(configList);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
export class PhoneProviderGeetestCaptcha {
|
||||||
|
constructor() {
|
||||||
|
this._captchaObj = null;
|
||||||
|
this._shouldCallCaptcha = false;
|
||||||
|
this._onSuccess = null;
|
||||||
|
this._onError = null;
|
||||||
|
|
||||||
|
this.realmUrl = config.get('realmUrl');
|
||||||
|
|
||||||
|
fetch(this.realmUrl + '/geetest/code').then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
} else {
|
||||||
|
throw msg.text('captcha-code-api-error');
|
||||||
|
}
|
||||||
|
}).then((data) => {
|
||||||
|
initGeetest({
|
||||||
|
gt: data.gt,
|
||||||
|
challenge: data.challenge,
|
||||||
|
offline: !data.success,
|
||||||
|
new_captcha: true,
|
||||||
|
product: 'bind',
|
||||||
|
}, (obj) => {
|
||||||
|
this._captchaObj = obj;
|
||||||
|
if (this._shouldCallCaptcha) { //加载完成后立刻调用
|
||||||
|
this._captchaObj.verify();
|
||||||
|
this._captchaObj.onSuccess(this.buildOnSuccess(this._onSuccess));
|
||||||
|
this._shouldCallCaptcha = false;
|
||||||
|
this._onSuccess = null;
|
||||||
|
this._onError = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
verify() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this._captchaObj) {
|
||||||
|
this._shouldCallCaptcha = true;
|
||||||
|
this._onSuccess = resolve;
|
||||||
|
this._onError = reject;
|
||||||
|
} else { //已经加载成功的情况,直接调用
|
||||||
|
this._captchaObj.verify();
|
||||||
|
this._captchaObj.onSuccess(this.buildOnSuccess(resolve));
|
||||||
|
this._captchaObj.onError((err) => {
|
||||||
|
alert(msg.text('captcha-load-error'));
|
||||||
|
reject('error', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._captchaObj.onClose(() => {
|
||||||
|
reject('close');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOnSuccess(callback) {
|
||||||
|
return (function() {
|
||||||
|
var result = this._captchaObj.getValidate();
|
||||||
|
callback(result);
|
||||||
|
this._captchaObj.reset();
|
||||||
|
}).bind(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
const _pfui = {
|
||||||
|
fadeOutElement: ($el, duration = 150, useHidden = false) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
$el.style.transition = `opacity ${duration}ms linear`;
|
||||||
|
$el.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (useHidden) {
|
||||||
|
$el.hidden = true;
|
||||||
|
} else {
|
||||||
|
$el.style.display = 'none';
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fadeInElement: ($el, duration = 150, useHidden = false, display = 'block') => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (useHidden) {
|
||||||
|
$el.hidden = false;
|
||||||
|
} else {
|
||||||
|
$el.style.display = display;
|
||||||
|
}
|
||||||
|
$el.style.opacity = '0';
|
||||||
|
$el.style.transition = `opacity ${duration}ms`;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
$el.style.opacity = '1';
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
compControllers: [],
|
||||||
|
init: () => {
|
||||||
|
// Initialize components if any
|
||||||
|
document.querySelectorAll('.pf-v5-c-tabs').forEach($tabsBar => {
|
||||||
|
if (!_pfui.compControllers.find(c => c.$el === $tabsBar)) {
|
||||||
|
const tabs = new Tabs($tabsBar);
|
||||||
|
_pfui.compControllers.push({ $el: $tabsBar, controller: tabs });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Tabs {
|
||||||
|
constructor($tabsBar) {
|
||||||
|
this.$tabsBar = $tabsBar;
|
||||||
|
this._handleTabClickEvent = this._handleTabClick.bind(this);
|
||||||
|
|
||||||
|
// Find the associated tab panel
|
||||||
|
if (this.$tabsBar && this.$tabsBar.dataset.tabPanel) {
|
||||||
|
this.$tabPanel = document.getElementById(this.$tabsBar.dataset.tabPanel);
|
||||||
|
if (this.$tabPanel) {
|
||||||
|
this._init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_init() {
|
||||||
|
this.$tabsBar.querySelectorAll('.pf-v5-c-tabs__link').forEach($tab => {
|
||||||
|
$tab.addEventListener('click', this._handleTabClickEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.$tabsBar.querySelectorAll('.pf-v5-c-tabs__link').forEach($tab => {
|
||||||
|
$tab.removeEventListener('click', this._handleTabClickEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleTabClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const $tab = e.currentTarget;
|
||||||
|
this._activateTab($tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
_activateTab($tab) {
|
||||||
|
const targetId = $tab.dataset.tabTarget;
|
||||||
|
if (!targetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate all tabs
|
||||||
|
this.$tabsBar.querySelectorAll('.pf-v5-c-tabs__item').forEach($item => {
|
||||||
|
$item.classList.remove('pf-m-current');
|
||||||
|
const $link = $item.querySelector('.pf-v5-c-tabs__link');
|
||||||
|
if ($link) {
|
||||||
|
$link.setAttribute('aria-selected', 'false');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate the clicked tab
|
||||||
|
const $tabItem = $tab.closest('.pf-v5-c-tabs__item');
|
||||||
|
if ($tabItem) {
|
||||||
|
$tabItem.classList.add('pf-m-current');
|
||||||
|
}
|
||||||
|
$tab.setAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
|
// Hide all tab panels
|
||||||
|
let promises = [];
|
||||||
|
this.$tabPanel.querySelectorAll('.pf-v5-c-tab-content').forEach($panel => {
|
||||||
|
if (!$panel.hidden) {
|
||||||
|
promises.push(_pfui.fadeOutElement($panel, 200, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the corresponding tab panel
|
||||||
|
Promise.all(promises).then(() => {
|
||||||
|
const $targetPanel = this.$tabPanel.querySelector(`.pf-v5-c-tab-content[data-tab-id="${targetId}"]`);
|
||||||
|
if ($targetPanel) {
|
||||||
|
_pfui.fadeInElement($targetPanel, 200, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send pf.tab.activate event
|
||||||
|
const activateEvent = new CustomEvent('pf.tab.activate', {
|
||||||
|
detail: {
|
||||||
|
tabId: targetId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$tabsBar.dispatchEvent(activateEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
_pfui.init();
|
||||||
|
});
|
||||||
@ -1,14 +1,226 @@
|
|||||||
import './lib/gt.js';
|
import './lib/gt.js';
|
||||||
import './lib/tom-select.complete.min.js';
|
import './lib/tom-select.complete.min.js';
|
||||||
|
import { PhoneProviderGeetestCaptcha } from './lib/gtutils.js';
|
||||||
|
|
||||||
|
class SmsSenderForm {
|
||||||
|
constructor(type, formSelector) {
|
||||||
|
this.type = type;
|
||||||
|
this.formElem = document.querySelector(formSelector);
|
||||||
|
this.realmUrl = config.get('realmUrl');
|
||||||
|
|
||||||
|
this.selectAreaCode = document.querySelector("#areaCodeInput");
|
||||||
|
this.inputPhoneNumber = document.querySelector("#phoneNumberInput");
|
||||||
|
this.inputCode = document.querySelector("#smsCodeInput");
|
||||||
|
this.btnSend = document.querySelector("#btnSendSmsCode");
|
||||||
|
|
||||||
|
this.countdown = null;
|
||||||
|
this.resendTimeout = 120;
|
||||||
|
this.areaCodeSelect = null;
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
this.initConfig();
|
||||||
|
this.initCaptcha();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.btnSend.addEventListener('click', this.onBtnSendClick.bind(this));
|
||||||
|
|
||||||
|
this.inputPhoneNumber.addEventListener('input', () => {
|
||||||
|
// Hide error message when user types
|
||||||
|
const errorMsg = this.formElem.querySelector('.pf-v5-c-form__helper-text--error');
|
||||||
|
if (errorMsg) {
|
||||||
|
errorMsg.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.formElem.addEventListener('submit', () => {
|
||||||
|
if (this.areaCodeSelect) {
|
||||||
|
this.areaCodeSelect.enable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initCaptcha() {
|
||||||
|
this.captcha = new PhoneProviderGeetestCaptcha();
|
||||||
|
}
|
||||||
|
|
||||||
|
initConfig() {
|
||||||
|
fetch(this.realmUrl + '/sms').then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(msg.localize('cannotGetConfig'));
|
||||||
|
}
|
||||||
|
}).then((res) => {
|
||||||
|
this.setAreaCodeList(res.areaCodeList || []);
|
||||||
|
|
||||||
|
// Initialize TomSelect after data is loaded
|
||||||
|
this.areaCodeSelect = new TomSelect('#areaCodeInput', {
|
||||||
|
maxItems: 1,
|
||||||
|
valueField: 'value',
|
||||||
|
labelField: 'text',
|
||||||
|
searchField: ['text', 'value'],
|
||||||
|
create: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.areaLocked) {
|
||||||
|
this.areaCodeSelect.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAreaCode = this.selectAreaCode.getAttribute('data-value') || res.defaultAreaCode || '86';
|
||||||
|
this.areaCodeSelect.setValue(defaultAreaCode.toString());
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.showError(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilledAreaCode(areaCode) {
|
||||||
|
const maxLen = 4;
|
||||||
|
let areaCodeStr = areaCode.toString();
|
||||||
|
return '+' + areaCodeStr + ('\u2002'.repeat(maxLen - areaCodeStr.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalizedName(nameList, langCode) {
|
||||||
|
if (langCode in nameList) {
|
||||||
|
return nameList[langCode];
|
||||||
|
} else {
|
||||||
|
return nameList['en'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(errorMessage) {
|
||||||
|
// Find or create error message element
|
||||||
|
let errorElement = this.formElem.querySelector('.phone-error-message');
|
||||||
|
if (!errorElement) {
|
||||||
|
errorElement = document.createElement('div');
|
||||||
|
errorElement.className = 'pf-v5-c-form__helper-text pf-v5-c-form__helper-text--error phone-error-message';
|
||||||
|
const phoneGroup = this.formElem.querySelector('[name="phoneNumber"]').closest('.pf-v5-c-form__group');
|
||||||
|
if (phoneGroup) {
|
||||||
|
phoneGroup.appendChild(errorElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errorElement.textContent = errorMessage;
|
||||||
|
errorElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
setAreaCodeList(areaCodeList) {
|
||||||
|
const langCode = msg.localize('countryNameLangCode');
|
||||||
|
|
||||||
|
// Clear existing options
|
||||||
|
this.selectAreaCode.innerHTML = '';
|
||||||
|
|
||||||
|
// Add new options
|
||||||
|
areaCodeList.forEach((info) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = info.areaCode;
|
||||||
|
const displayName = this.getFilledAreaCode(info.areaCode) + '\u2002\u2002' + this.getLocalizedName(info.name, langCode);
|
||||||
|
option.textContent = displayName;
|
||||||
|
this.selectAreaCode.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startCountdown() {
|
||||||
|
let currentTimeout = this.resendTimeout;
|
||||||
|
const resendMsg = msg.localize('resendVerificationCode');
|
||||||
|
const secondMsg = msg.localize('second');
|
||||||
|
|
||||||
|
this.countdown = setInterval(() => {
|
||||||
|
currentTimeout -= 1;
|
||||||
|
if (currentTimeout <= 0) {
|
||||||
|
this.stopCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.btnSend.textContent = resendMsg + ' (' + currentTimeout.toString() + secondMsg + ')';
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCountdown() {
|
||||||
|
if (this.countdown !== null) {
|
||||||
|
clearInterval(this.countdown);
|
||||||
|
this.countdown = null;
|
||||||
|
this.btnSend.textContent = msg.localize('resendVerificationCode');
|
||||||
|
this.btnSend.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInput() {
|
||||||
|
if (this.inputPhoneNumber.value.trim() === "") {
|
||||||
|
this.showError(msg.localize('phoneNumberIsEmpty'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBtnSendClick() {
|
||||||
|
if (!this.checkInput()) return;
|
||||||
|
|
||||||
|
this.captcha.verify().then((data) => {
|
||||||
|
this.btnSend.disabled = true;
|
||||||
|
this.btnSend.textContent = msg.localize('sending');
|
||||||
|
|
||||||
|
data.areaCode = this.areaCodeSelect.getValue();
|
||||||
|
data.phoneNumber = this.inputPhoneNumber.value;
|
||||||
|
|
||||||
|
fetch(this.realmUrl + '/sms/' + this.type + '-code', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(msg.localize('sendVerificationError', [res.status + ' ' + res.statusText]));
|
||||||
|
}
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.status == 1) {
|
||||||
|
this.startCountdown();
|
||||||
|
} else if (res.status == 0) {
|
||||||
|
throw new Error(res.errormsg ? msg.localize(res.errormsg, [res.error]) : res.error);
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.showError(err.message);
|
||||||
|
|
||||||
|
this.btnSend.disabled = false;
|
||||||
|
this.btnSend.textContent = msg.localize('sendVerificationCode');
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
if (err !== 'close') { // User closed captcha
|
||||||
|
console.error(err);
|
||||||
|
this.showError(typeof err === 'string' ? err : err.message);
|
||||||
|
this.btnSend.disabled = false;
|
||||||
|
this.btnSend.textContent = msg.localize('sendVerificationCode');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Initialize TomSelect for area code selection
|
// Initialize SMS sender form for login
|
||||||
const areaCodeSelect = new TomSelect('#areaCodeInput', {
|
window.SmsSenderForm = new SmsSenderForm("login", "#kc-form-login");
|
||||||
maxItems: 1,
|
|
||||||
valueField: 'areaCode',
|
// Handle tab switching to set the login type
|
||||||
labelField: 'countryName',
|
document.addEventListener('pf.tab.activate', function(event) {
|
||||||
searchField: ['countryName', 'countryCode', 'areaCode'],
|
const tabId = event.detail.tabId;
|
||||||
create: false,
|
let loginTypeInput = document.querySelector('#loginType');
|
||||||
|
|
||||||
|
if (!loginTypeInput) {
|
||||||
|
loginTypeInput = document.createElement('input');
|
||||||
|
loginTypeInput.type = 'hidden';
|
||||||
|
loginTypeInput.name = 'loginType';
|
||||||
|
loginTypeInput.id = 'loginType';
|
||||||
|
document.querySelector('#kc-form-login').appendChild(loginTypeInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
loginTypeInput.value = tabId; // 'password' or 'phone'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set initial login type to password (default active tab)
|
||||||
|
const initialLoginType = document.createElement('input');
|
||||||
|
initialLoginType.type = 'hidden';
|
||||||
|
initialLoginType.name = 'loginType';
|
||||||
|
initialLoginType.id = 'loginType';
|
||||||
|
initialLoginType.value = 'password';
|
||||||
|
document.querySelector('#kc-form-login').appendChild(initialLoginType);
|
||||||
});
|
});
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
<#macro tabsBar panelId filled=false>
|
||||||
|
<div
|
||||||
|
class="pf-v5-c-tabs<#if filled> pf-m-fill</#if>"
|
||||||
|
role="region"
|
||||||
|
id="default-tabs"
|
||||||
|
<#if panelId??>data-tab-panel="${panelId}"</#if>
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="pf-v5-c-tabs__scroll-button"
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-label="Scroll left"
|
||||||
|
>
|
||||||
|
<i class="fas fa-angle-left" aria-hidden="true">
|
||||||
|
</i>
|
||||||
|
</button>
|
||||||
|
<ul class="pf-v5-c-tabs__list" role="tablist">
|
||||||
|
<#nested>
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
class="pf-v5-c-tabs__scroll-button"
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-label="Scroll right"
|
||||||
|
>
|
||||||
|
<i class="fas fa-angle-right" aria-hidden="true">
|
||||||
|
</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</#macro>
|
||||||
|
|
||||||
|
<#macro tabButton tabId label panelId="" active=false disabled=false>
|
||||||
|
<li class="pf-v5-c-tabs__item<#if active> pf-m-current</#if>" role="presentation">
|
||||||
|
<a
|
||||||
|
class="pf-v5-c-tabs__link"
|
||||||
|
<#if tabId??>
|
||||||
|
href="#${tabId}"
|
||||||
|
data-tab-target="${tabId}"
|
||||||
|
</#if>
|
||||||
|
<#if panelId??>
|
||||||
|
aria-controls="${panelId}"
|
||||||
|
</#if>
|
||||||
|
role="tab"
|
||||||
|
aria-selected="<#if active>true<#else>false</#if>"
|
||||||
|
<#if disabled>
|
||||||
|
aria-disabled="true"
|
||||||
|
disabled
|
||||||
|
</#if>
|
||||||
|
>
|
||||||
|
${kcSanitize(msg(label))?no_esc}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</#macro>
|
||||||
|
|
||||||
|
<#macro tabPanel panelId>
|
||||||
|
<div
|
||||||
|
class="pf-v5-c-tab-panel"
|
||||||
|
id="${panelId}"
|
||||||
|
>
|
||||||
|
<#nested>
|
||||||
|
</div>
|
||||||
|
</#macro>
|
||||||
|
|
||||||
|
<#macro tabContent tabId active=false>
|
||||||
|
<div
|
||||||
|
data-tab-id="${tabId}"
|
||||||
|
role="tabpanel"
|
||||||
|
class="pf-v5-c-tab-content"
|
||||||
|
<#if !active>
|
||||||
|
hidden
|
||||||
|
</#if>
|
||||||
|
>
|
||||||
|
<#nested>
|
||||||
|
</div>
|
||||||
|
</#macro>
|
||||||
Loading…
Reference in New Issue