@ -1,9 +1,22 @@
|
|||||||
package cn.keking.service.impl;
|
package cn.keking.service.impl;
|
||||||
|
|
||||||
|
import cn.keking.config.ConfigConstants;
|
||||||
import cn.keking.model.FileAttribute;
|
import cn.keking.model.FileAttribute;
|
||||||
|
import cn.keking.model.ReturnResponse;
|
||||||
|
import cn.keking.service.FileHandlerService;
|
||||||
import cn.keking.service.FilePreview;
|
import cn.keking.service.FilePreview;
|
||||||
|
import cn.keking.utils.DownloadUtils;
|
||||||
|
import cn.keking.utils.KkFileUtils;
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.util.HtmlUtils;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author kl (http://kailing.pub)
|
* @author kl (http://kailing.pub)
|
||||||
@ -13,15 +26,66 @@ import org.springframework.ui.Model;
|
|||||||
@Service
|
@Service
|
||||||
public class JsonFilePreviewImpl implements FilePreview {
|
public class JsonFilePreviewImpl implements FilePreview {
|
||||||
|
|
||||||
private final SimTextFilePreviewImpl simTextFilePreview;
|
private final FileHandlerService fileHandlerService;
|
||||||
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
|
|
||||||
public JsonFilePreviewImpl(SimTextFilePreviewImpl simTextFilePreview) {
|
public JsonFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
|
||||||
this.simTextFilePreview = simTextFilePreview;
|
this.fileHandlerService = fileHandlerService;
|
||||||
|
this.otherFilePreview = otherFilePreview;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||||
simTextFilePreview.filePreviewHandle(url, model, fileAttribute);
|
String fileName = fileAttribute.getName();
|
||||||
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||||
|
String filePath = fileAttribute.getOriginFilePath();
|
||||||
|
|
||||||
|
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
|
||||||
|
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
||||||
|
if (response.isFailure()) {
|
||||||
|
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
||||||
|
}
|
||||||
|
filePath = response.getContent();
|
||||||
|
if (ConfigConstants.isCacheEnabled()) {
|
||||||
|
fileHandlerService.addConvertedFile(fileName, filePath);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String fileData = readJsonFile(filePath, fileName);
|
||||||
|
String escapedData = HtmlUtils.htmlEscape(fileData);
|
||||||
|
String base64Data = Base64.encodeBase64String(escapedData.getBytes(StandardCharsets.UTF_8));
|
||||||
|
model.addAttribute("textData", base64Data);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
|
||||||
|
}
|
||||||
return JSON_FILE_PREVIEW_PAGE;
|
return JSON_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String fileData = null;
|
||||||
|
try {
|
||||||
|
fileData = HtmlUtils.htmlEscape(readJsonFile(filePath, fileName));
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
String base64Data = Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8));
|
||||||
|
model.addAttribute("textData", base64Data);
|
||||||
|
return JSON_FILE_PREVIEW_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取 JSON 文件,强制使用 UTF-8 编码
|
||||||
|
* JSON 标准规定必须使用 UTF-8 编码
|
||||||
|
*/
|
||||||
|
private String readJsonFile(String filePath, String fileName) throws IOException {
|
||||||
|
File file = new File(filePath);
|
||||||
|
if (KkFileUtils.isIllegalFileName(fileName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!file.exists() || file.length() == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON 标准规定使用 UTF-8 编码,不依赖自动检测
|
||||||
|
byte[] bytes = Files.readAllBytes(Paths.get(filePath));
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ public class SimTextFilePreviewImpl implements FilePreview {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
|
String fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
|
||||||
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes()));
|
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8)));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
|
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ public class SimTextFilePreviewImpl implements FilePreview {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes()));
|
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8)));
|
||||||
return TXT_FILE_PREVIEW_PAGE;
|
return TXT_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,25 +17,18 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.panel-body {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
#json {
|
#json {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
background-color: #f8f9fa;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
#text_view {
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
padding: 20px;
|
||||||
|
padding-left: 65px; /* 为行号留出空间 */
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.json-key {
|
.json-key {
|
||||||
color: #881391;
|
color: #881391;
|
||||||
@ -55,11 +48,31 @@
|
|||||||
color: #808080;
|
color: #808080;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.btn-group {
|
.json-toggle {
|
||||||
margin-bottom: 10px;
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
user-select: none;
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.view-mode-btn {
|
.json-toggle:hover {
|
||||||
min-width: 100px;
|
color: #333;
|
||||||
|
}
|
||||||
|
.json-node {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.line-number {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 55px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -68,18 +81,21 @@
|
|||||||
<input hidden id="textData" value="${textData}"/>
|
<input hidden id="textData" value="${textData}"/>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div id="formatted_btn" class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
|
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
|
||||||
${file.name}
|
${file.name}
|
||||||
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<button type="button" class="btn btn-primary view-mode-btn" id="formatted_btn">格式化视图</button>
|
|
||||||
<button type="button" class="btn btn-default view-mode-btn" id="raw_btn">原始文本</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="raw_btn" class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
|
||||||
|
${file.name}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div id="json" class="panel-body">
|
||||||
<div id="json"></div>
|
|
||||||
<div id="text_view" style="display:none;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -89,22 +105,20 @@
|
|||||||
* 初始化
|
* 初始化
|
||||||
*/
|
*/
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
|
$("#formatted_btn").hide();
|
||||||
initWaterMark();
|
initWaterMark();
|
||||||
loadJsonData();
|
loadJsonData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTML 反转义(用于还原后端转义的内容)
|
* HTML 反转义(用于还原后端转义的内容)
|
||||||
|
* 使用浏览器的 DOM 来正确解码所有 HTML 实体
|
||||||
*/
|
*/
|
||||||
function htmlUnescape(str) {
|
function htmlUnescape(str) {
|
||||||
if (!str || str.length === 0) return "";
|
if (!str || str.length === 0) return "";
|
||||||
var s = str;
|
var textarea = document.createElement('textarea');
|
||||||
s = s.replace(/"/g, '"');
|
textarea.innerHTML = str;
|
||||||
s = s.replace(/'/g, "'");
|
return textarea.value;
|
||||||
s = s.replace(/</g, "<");
|
|
||||||
s = s.replace(/>/g, ">");
|
|
||||||
s = s.replace(/&/g, "&");
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,8 +145,132 @@
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全局行号计数器
|
||||||
|
var lineNumber = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON 语法高亮
|
* 构建可展开/收起的 JSON 树形结构
|
||||||
|
*/
|
||||||
|
function buildJsonTree(obj, indent, skipLineNumber) {
|
||||||
|
indent = indent || 0;
|
||||||
|
skipLineNumber = skipLineNumber || false;
|
||||||
|
var html = '';
|
||||||
|
var indentStr = ' '.repeat(indent);
|
||||||
|
|
||||||
|
if (obj === null) {
|
||||||
|
return '<span class="json-null">null</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj !== 'object') {
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
// 转义特殊字符,避免换行和制表符破坏布局
|
||||||
|
var escapedStr = obj
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/\n/g, '\\n')
|
||||||
|
.replace(/\r/g, '\\r')
|
||||||
|
.replace(/\t/g, '\\t')
|
||||||
|
.replace(/"/g, '\\"');
|
||||||
|
return '<span class="json-string">"' + htmlEscape(escapedStr) + '"</span>';
|
||||||
|
} else if (typeof obj === 'number') {
|
||||||
|
return '<span class="json-number">' + obj + '</span>';
|
||||||
|
} else if (typeof obj === 'boolean') {
|
||||||
|
return '<span class="json-boolean">' + obj + '</span>';
|
||||||
|
}
|
||||||
|
return htmlEscape(String(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
var isArray = Array.isArray(obj);
|
||||||
|
var entries = isArray ? obj : Object.keys(obj);
|
||||||
|
var openBracket = isArray ? '[' : '{';
|
||||||
|
var closeBracket = isArray ? ']' : '}';
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return openBracket + closeBracket;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeId = 'node_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
|
// 如果不跳过行号,说明是新的一行
|
||||||
|
if (!skipLineNumber) {
|
||||||
|
html += '<span class="line-number">' + lineNumber++ + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<span class="json-toggle" onclick="toggleJsonNode(\'' + nodeId + '\')">▼</span> ';
|
||||||
|
html += openBracket + '\n';
|
||||||
|
html += '<div id="' + nodeId + '" class="json-node">';
|
||||||
|
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var key = isArray ? i : entries[i];
|
||||||
|
var value = isArray ? entries[i] : obj[entries[i]];
|
||||||
|
|
||||||
|
html += '<span class="line-number">' + lineNumber++ + '</span>';
|
||||||
|
html += indentStr + ' ';
|
||||||
|
if (!isArray) {
|
||||||
|
html += '<span class="json-key">"' + htmlEscape(key) + '"</span>: ';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果值是对象或数组,跳过它的行号(因为已经在上面添加了)
|
||||||
|
html += buildJsonTree(value, indent + 1, true);
|
||||||
|
|
||||||
|
if (i < entries.length - 1) {
|
||||||
|
html += ',';
|
||||||
|
}
|
||||||
|
html += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
html += '<span class="line-number">' + lineNumber++ + '</span>';
|
||||||
|
html += indentStr + closeBracket;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换 JSON 节点展开/收起
|
||||||
|
*/
|
||||||
|
function toggleJsonNode(nodeId) {
|
||||||
|
var node = document.getElementById(nodeId);
|
||||||
|
var toggle = event.target;
|
||||||
|
|
||||||
|
if (node.style.display === 'none') {
|
||||||
|
node.style.display = 'block';
|
||||||
|
toggle.textContent = '▼';
|
||||||
|
} else {
|
||||||
|
node.style.display = 'none';
|
||||||
|
toggle.textContent = '▶';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全部展开
|
||||||
|
*/
|
||||||
|
function expandAll() {
|
||||||
|
var nodes = document.querySelectorAll('.json-node');
|
||||||
|
var toggles = document.querySelectorAll('.json-toggle');
|
||||||
|
nodes.forEach(function(node) {
|
||||||
|
node.style.display = 'block';
|
||||||
|
});
|
||||||
|
toggles.forEach(function(toggle) {
|
||||||
|
toggle.textContent = '▼';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全部收起
|
||||||
|
*/
|
||||||
|
function collapseAll() {
|
||||||
|
var nodes = document.querySelectorAll('.json-node');
|
||||||
|
var toggles = document.querySelectorAll('.json-toggle');
|
||||||
|
nodes.forEach(function(node) {
|
||||||
|
node.style.display = 'none';
|
||||||
|
});
|
||||||
|
toggles.forEach(function(toggle) {
|
||||||
|
toggle.textContent = '▶';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 语法高亮(简单版本,用于原始文本视图)
|
||||||
*/
|
*/
|
||||||
function syntaxHighlight(json) {
|
function syntaxHighlight(json) {
|
||||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
@ -153,12 +291,36 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UTF-8 解码 Base64(正确处理中文等 UTF-8 字符)
|
||||||
|
*/
|
||||||
|
function decodeBase64UTF8(base64Str) {
|
||||||
|
try {
|
||||||
|
// 方法1:使用现代浏览器的 TextDecoder API(推荐)
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
var binaryString = window.atob(base64Str);
|
||||||
|
var bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (var i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new TextDecoder('utf-8').decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法2:降级方案
|
||||||
|
return decodeURIComponent(escape(window.atob(base64Str)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Base64 decode error:', e);
|
||||||
|
// 最后降级到 Base64.js 库
|
||||||
|
return Base64.decode(base64Str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载 JSON 数据
|
* 加载 JSON 数据
|
||||||
*/
|
*/
|
||||||
function loadJsonData() {
|
function loadJsonData() {
|
||||||
try {
|
try {
|
||||||
var textData = Base64.decode($("#textData").val());
|
var textData = decodeBase64UTF8($("#textData").val());
|
||||||
|
|
||||||
// 1. 先反转义 HTML 实体(因为后端已经转义过)
|
// 1. 先反转义 HTML 实体(因为后端已经转义过)
|
||||||
textData = htmlUnescape(textData);
|
textData = htmlUnescape(textData);
|
||||||
@ -167,15 +329,27 @@
|
|||||||
textData = removeBOM(textData);
|
textData = removeBOM(textData);
|
||||||
|
|
||||||
// 保存原始文本(用于显示时再次转义以保证安全)
|
// 保存原始文本(用于显示时再次转义以保证安全)
|
||||||
window.rawText = "<pre>" + htmlEscape(textData) + "</pre>";
|
window.rawText = "<pre style='background-color: #FFFFFF; border: none; margin: 0;'>" + htmlEscape(textData) + "</pre>";
|
||||||
|
|
||||||
// 尝试解析并格式化 JSON
|
// 尝试解析并格式化 JSON
|
||||||
try {
|
try {
|
||||||
var jsonObj = JSON.parse(textData);
|
var jsonObj = JSON.parse(textData);
|
||||||
var formattedJson = JSON.stringify(jsonObj, null, 4);
|
|
||||||
window.formattedJson = "<pre>" + syntaxHighlight(formattedJson) + "</pre>";
|
|
||||||
|
|
||||||
// 默认显示格式化视图
|
// 重置行号计数器
|
||||||
|
lineNumber = 1;
|
||||||
|
|
||||||
|
// 构建树形视图
|
||||||
|
var treeHtml = '<div style="padding: 20px;">';
|
||||||
|
treeHtml += '<div style="margin-bottom: 10px;">';
|
||||||
|
treeHtml += '<button onclick="expandAll()" class="btn btn-sm btn-default" style="margin-right: 5px;">全部展开</button>';
|
||||||
|
treeHtml += '<button onclick="collapseAll()" class="btn btn-sm btn-default">全部收起</button>';
|
||||||
|
treeHtml += '</div>';
|
||||||
|
treeHtml += '<pre style="background-color: #f8f9fa; border: none; margin: 0;">';
|
||||||
|
treeHtml += buildJsonTree(jsonObj, 0);
|
||||||
|
treeHtml += '</pre></div>';
|
||||||
|
window.formattedJson = treeHtml;
|
||||||
|
|
||||||
|
// 默认显示树形视图
|
||||||
$("#json").html(window.formattedJson);
|
$("#json").html(window.formattedJson);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果不是有效的 JSON,显示错误并回退到原始文本
|
// 如果不是有效的 JSON,显示错误并回退到原始文本
|
||||||
@ -193,19 +367,15 @@
|
|||||||
*/
|
*/
|
||||||
$(function () {
|
$(function () {
|
||||||
$("#formatted_btn").click(function () {
|
$("#formatted_btn").click(function () {
|
||||||
$("#json").show();
|
|
||||||
$("#text_view").hide();
|
|
||||||
$("#json").html(window.formattedJson);
|
$("#json").html(window.formattedJson);
|
||||||
$("#formatted_btn").removeClass("btn-default").addClass("btn-primary");
|
$("#raw_btn").show();
|
||||||
$("#raw_btn").removeClass("btn-primary").addClass("btn-default");
|
$("#formatted_btn").hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#raw_btn").click(function () {
|
$("#raw_btn").click(function () {
|
||||||
$("#json").hide();
|
$("#json").html(window.rawText);
|
||||||
$("#text_view").show();
|
$("#formatted_btn").show();
|
||||||
$("#text_view").html(window.rawText);
|
$("#raw_btn").hide();
|
||||||
$("#raw_btn").removeClass("btn-default").addClass("btn-primary");
|
|
||||||
$("#formatted_btn").removeClass("btn-primary").addClass("btn-default");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user