Initial plugin commit
This commit is contained in:
302
assets/css/admin.css
Normal file
302
assets/css/admin.css
Normal file
@@ -0,0 +1,302 @@
|
||||
.kgvvm-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin: 18px 0 24px;
|
||||
}
|
||||
|
||||
.kgvvm-card {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.kgvvm-card h2,
|
||||
.kgvvm-card h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.kgvvm-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.kgvvm-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kgvvm-form-table th {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.kgvvm-help {
|
||||
color: #50575e;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.kgvvm-status {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.kgvvm-status--green {
|
||||
background: #edf7ed;
|
||||
color: #0a5c1a;
|
||||
}
|
||||
|
||||
.kgvvm-status--orange {
|
||||
background: #fff4e5;
|
||||
color: #8a4b00;
|
||||
}
|
||||
|
||||
.kgvvm-status--gray {
|
||||
background: #f1f1f1;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.kgvvm-status--blue {
|
||||
background: #eef5ff;
|
||||
color: #1d4f91;
|
||||
}
|
||||
|
||||
.kgvvm-multiselect {
|
||||
min-width: 320px;
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.kgvvm-open-swap-modal {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.kgvvm-modal[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.kgvvm-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.kgvvm-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.kgvvm-modal__dialog {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(760px, calc(100vw - 32px));
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow: auto;
|
||||
margin: 24px auto;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.2);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.kgvvm-modal__close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.kgvvm-modal__summary {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.kgvvm-modal__summary p {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.kgvvm-modal__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kgvvm-print-page {
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.kgvvm-print-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 12px 0 18px;
|
||||
}
|
||||
|
||||
.kgvvm-print-page ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.kgvvm-print-page li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-app {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 220px) 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-room-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-room {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.kgvvm-chat-room.is-active {
|
||||
background: #2271b1;
|
||||
border-color: #2271b1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.kgvvm-chat-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-messages {
|
||||
background: #f6f7f7;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 8px;
|
||||
min-height: 320px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-empty {
|
||||
margin: 0;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.kgvvm-chat-message {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-message--own {
|
||||
border-color: #72aee6;
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
.kgvvm-chat-message__meta {
|
||||
color: #50575e;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-message__body {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.kgvvm-chat-form {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-form textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.kgvvm-chat-form__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.kgvvm-chat-form__actions .is-error {
|
||||
color: #b32d2e;
|
||||
}
|
||||
|
||||
@media (max-width: 782px) {
|
||||
.kgvvm-chat-app {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.kgvvm-chat-panel__header,
|
||||
.kgvvm-chat-form__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
#wpadminbar,
|
||||
#adminmenumain,
|
||||
#screen-meta,
|
||||
#screen-meta-links,
|
||||
.notice,
|
||||
.update-nag,
|
||||
.kgvvm-print-actions,
|
||||
.page-title-action {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#wpcontent,
|
||||
#wpfooter {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.wrap,
|
||||
.kgvvm-print-page {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.kgvvm-card {
|
||||
box-shadow: none;
|
||||
border-color: #d0d0d0;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
table {
|
||||
break-inside: auto;
|
||||
}
|
||||
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
262
assets/js/chat.js
Normal file
262
assets/js/chat.js
Normal file
@@ -0,0 +1,262 @@
|
||||
(function () {
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderMessage(message, currentUserId) {
|
||||
var ownClass = Number(message.user_id) === Number(currentUserId) ? ' kgvvm-chat-message--own' : '';
|
||||
var roleLabel = message.role ? ' · ' + escapeHtml(message.role) : '';
|
||||
var text = escapeHtml(message.message).replace(/\n/g, '<br>');
|
||||
|
||||
return '' +
|
||||
'<article class="kgvvm-chat-message' + ownClass + '" data-id="' + escapeHtml(message.id) + '">' +
|
||||
'<div class="kgvvm-chat-message__meta">' +
|
||||
'<strong>' + escapeHtml(message.user) + '</strong>' + roleLabel + ' · ' + escapeHtml(message.time) +
|
||||
'</div>' +
|
||||
'<div class="kgvvm-chat-message__body">' + text + '</div>' +
|
||||
'</article>';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var app = document.querySelector('.kgvvm-chat-app');
|
||||
|
||||
if (!app || typeof window.kgvvmChatConfig === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
var config = window.kgvvmChatConfig;
|
||||
var messagesEl = app.querySelector('[data-chat-messages]');
|
||||
var form = app.querySelector('[data-chat-form]');
|
||||
var input = app.querySelector('[data-chat-input]');
|
||||
var status = app.querySelector('[data-chat-status]');
|
||||
var roomButtons = app.querySelectorAll('[data-chat-room]');
|
||||
var roomTitle = app.querySelector('[data-chat-room-title]');
|
||||
var roomDescription = app.querySelector('[data-chat-room-description]');
|
||||
var currentRoom = app.getAttribute('data-room') || 'general';
|
||||
var initialRoom = currentRoom;
|
||||
var currentUserId = Number(app.getAttribute('data-current-user-id') || '0');
|
||||
var sendLocked = false;
|
||||
var storageKey = 'kgvvmActiveChatRoom';
|
||||
|
||||
function getAvailableRooms() {
|
||||
return Array.prototype.map.call(roomButtons, function (button) {
|
||||
return button.getAttribute('data-chat-room') || '';
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function syncRoomState() {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, currentRoom);
|
||||
} catch (error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
|
||||
if (window.history && typeof window.history.replaceState === 'function') {
|
||||
var url = new window.URL(window.location.href);
|
||||
url.searchParams.set('room', currentRoom);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
function restoreRoomState() {
|
||||
var availableRooms = getAvailableRooms();
|
||||
var url = new window.URL(window.location.href);
|
||||
var requestedRoom = url.searchParams.get('room') || '';
|
||||
var storedRoom = '';
|
||||
|
||||
try {
|
||||
storedRoom = window.localStorage.getItem(storageKey) || '';
|
||||
} catch (error) {
|
||||
storedRoom = '';
|
||||
}
|
||||
|
||||
var preferredRoom = requestedRoom || storedRoom;
|
||||
|
||||
if (preferredRoom && availableRooms.indexOf(preferredRoom) !== -1) {
|
||||
currentRoom = preferredRoom;
|
||||
app.setAttribute('data-room', currentRoom);
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(text, isError) {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
status.textContent = text || '';
|
||||
status.classList.toggle('is-error', !!isError);
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (messagesEl) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRoomButtons() {
|
||||
roomButtons.forEach(function (button) {
|
||||
button.classList.toggle('is-active', button.getAttribute('data-chat-room') === currentRoom);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRoomMeta() {
|
||||
roomButtons.forEach(function (button) {
|
||||
if (button.getAttribute('data-chat-room') !== currentRoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomTitle) {
|
||||
roomTitle.textContent = button.getAttribute('data-chat-label') || '';
|
||||
}
|
||||
|
||||
if (roomDescription) {
|
||||
roomDescription.textContent = button.getAttribute('data-chat-description') || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderMessages(messages, replaceAll) {
|
||||
if (!messagesEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (replaceAll) {
|
||||
messagesEl.innerHTML = '';
|
||||
}
|
||||
|
||||
if (!Array.isArray(messages) || !messages.length) {
|
||||
if (replaceAll) {
|
||||
messagesEl.innerHTML = '<p class="kgvvm-chat-empty">' + escapeHtml(config.i18n.empty) + '</p>';
|
||||
messagesEl.dataset.lastId = '0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (messagesEl.querySelector('.kgvvm-chat-empty')) {
|
||||
messagesEl.innerHTML = '';
|
||||
}
|
||||
|
||||
messages.forEach(function (message) {
|
||||
messagesEl.insertAdjacentHTML('beforeend', renderMessage(message, currentUserId));
|
||||
messagesEl.dataset.lastId = String(message.id || messagesEl.dataset.lastId || '0');
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function fetchMessages(replaceAll) {
|
||||
var data = new window.FormData();
|
||||
data.append('action', 'kgvvm_fetch_chat_messages');
|
||||
data.append('nonce', config.nonce);
|
||||
data.append('room', currentRoom);
|
||||
|
||||
if (!replaceAll && messagesEl && messagesEl.dataset.lastId) {
|
||||
data.append('after_id', messagesEl.dataset.lastId);
|
||||
}
|
||||
|
||||
return window.fetch(config.ajaxUrl, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: data
|
||||
})
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (payload) {
|
||||
if (!payload || !payload.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderMessages(payload.data.messages || [], replaceAll);
|
||||
})
|
||||
.catch(function () {
|
||||
setStatus(config.i18n.fetchError, true);
|
||||
});
|
||||
}
|
||||
|
||||
roomButtons.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
currentRoom = button.getAttribute('data-chat-room') || 'general';
|
||||
app.setAttribute('data-room', currentRoom);
|
||||
syncRoomState();
|
||||
updateRoomMeta();
|
||||
|
||||
if (messagesEl) {
|
||||
messagesEl.dataset.lastId = '0';
|
||||
}
|
||||
|
||||
updateRoomButtons();
|
||||
setStatus(config.i18n.loading, false);
|
||||
fetchMessages(true).then(function () {
|
||||
setStatus('', false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (form && input) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (sendLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
var message = String(input.value || '').trim();
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendLocked = true;
|
||||
setStatus(config.i18n.sending, false);
|
||||
|
||||
var data = new window.FormData();
|
||||
data.append('action', 'kgvvm_send_chat_message');
|
||||
data.append('nonce', config.nonce);
|
||||
data.append('room', currentRoom);
|
||||
data.append('message', message);
|
||||
|
||||
window.fetch(config.ajaxUrl, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: data
|
||||
})
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (payload) {
|
||||
sendLocked = false;
|
||||
|
||||
if (!payload || !payload.success) {
|
||||
setStatus(config.i18n.sendError, true);
|
||||
return;
|
||||
}
|
||||
|
||||
renderMessages(payload.data.messages || [], false);
|
||||
input.value = '';
|
||||
input.focus();
|
||||
setStatus('', false);
|
||||
})
|
||||
.catch(function () {
|
||||
sendLocked = false;
|
||||
setStatus(config.i18n.sendError, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
restoreRoomState();
|
||||
updateRoomButtons();
|
||||
updateRoomMeta();
|
||||
syncRoomState();
|
||||
|
||||
if (currentRoom !== initialRoom) {
|
||||
fetchMessages(true);
|
||||
}
|
||||
|
||||
scrollToBottom();
|
||||
window.setInterval(function () {
|
||||
fetchMessages(false);
|
||||
}, Number(config.refreshInterval || 7000));
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user