From c94cc7cba703a3ec6c4c699938c907992c27c370 Mon Sep 17 00:00:00 2001 From: astatine Date: Sun, 12 Apr 2020 15:16:02 +0200 Subject: [PATCH 01/12] chat: wip rework as chat2 --- .../[gameplay]/chat-theme-gtao/style.css | 21 +- resources/[gameplay]/chat/.gitignore | 3 + resources/[gameplay]/chat/cl_chat.lua | 95 +- resources/[gameplay]/chat/fxmanifest.lua | 27 +- resources/[gameplay]/chat/html/App.js | 255 - resources/[gameplay]/chat/html/App.ts | 434 ++ resources/[gameplay]/chat/html/App.vue | 44 + .../chat/html/{Message.js => Message.ts} | 54 +- resources/[gameplay]/chat/html/Message.vue | 7 + .../html/{Suggestions.js => Suggestions.ts} | 27 +- .../[gameplay]/chat/html/Suggestions.vue | 29 + .../html/{config.default.js => config.ts} | 6 +- resources/[gameplay]/chat/html/index.css | 51 +- resources/[gameplay]/chat/html/index.d.ts | 4 + resources/[gameplay]/chat/html/index.html | 109 +- resources/[gameplay]/chat/html/main.ts | 7 + resources/[gameplay]/chat/html/tsconfig.json | 18 + resources/[gameplay]/chat/html/utils.ts | 31 + .../chat/html/vendor/vue.2.3.3.min.js | 8 - resources/[gameplay]/chat/package.json | 24 + resources/[gameplay]/chat/sv_chat.lua | 167 +- resources/[gameplay]/chat/webpack.config.js | 45 + resources/[gameplay]/chat/yarn.lock | 4549 +++++++++++++++++ 23 files changed, 5566 insertions(+), 449 deletions(-) create mode 100644 resources/[gameplay]/chat/.gitignore delete mode 100644 resources/[gameplay]/chat/html/App.js create mode 100644 resources/[gameplay]/chat/html/App.ts create mode 100644 resources/[gameplay]/chat/html/App.vue rename resources/[gameplay]/chat/html/{Message.js => Message.ts} (58%) create mode 100644 resources/[gameplay]/chat/html/Message.vue rename resources/[gameplay]/chat/html/{Suggestions.js => Suggestions.ts} (75%) create mode 100644 resources/[gameplay]/chat/html/Suggestions.vue rename resources/[gameplay]/chat/html/{config.default.js => config.ts} (81%) create mode 100644 resources/[gameplay]/chat/html/index.d.ts create mode 100644 resources/[gameplay]/chat/html/main.ts create mode 100644 resources/[gameplay]/chat/html/tsconfig.json create mode 100644 resources/[gameplay]/chat/html/utils.ts delete mode 100644 resources/[gameplay]/chat/html/vendor/vue.2.3.3.min.js create mode 100644 resources/[gameplay]/chat/package.json create mode 100644 resources/[gameplay]/chat/webpack.config.js create mode 100644 resources/[gameplay]/chat/yarn.lock diff --git a/resources/[gameplay]/chat-theme-gtao/style.css b/resources/[gameplay]/chat-theme-gtao/style.css index c2c0fa3..5e28f97 100644 --- a/resources/[gameplay]/chat-theme-gtao/style.css +++ b/resources/[gameplay]/chat-theme-gtao/style.css @@ -85,7 +85,9 @@ } .chat-input > div { - background-color: rgba(0, 0, 0, .6); + background-color: rgba(0, 0, 0, .6) !important; + border: calc(0.28vh / 2) solid rgba(180, 180, 180, .6); + outline: calc(0.28vh / 2) solid rgba(0, 0, 0, .8); /* to replace margin-background */ padding: calc(0.28vh / 2); } @@ -93,6 +95,19 @@ margin: 0; margin-left: 0.7%; margin-top: -0.1%; + line-height: 2.8vh; +} + +.chat-input .prefix.any { + opacity: 0.8; +} + +.chat-input .prefix.any:before { + content: '['; +} + +.chat-input .prefix.any:after { + content: ']'; } .chat-input > div + div { @@ -110,9 +125,7 @@ textarea { background: transparent; - border: calc(0.28vh / 2) solid rgba(180, 180, 180, .6); - padding: calc(0.28vh / 2); - padding-left: calc(3.5% + (0.28vh / 2)); + padding: 0.5vh; } @media screen and (min-aspect-ratio: 21/9) { diff --git a/resources/[gameplay]/chat/.gitignore b/resources/[gameplay]/chat/.gitignore new file mode 100644 index 0000000..682c87a --- /dev/null +++ b/resources/[gameplay]/chat/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +yarn-error.log +dist/ \ No newline at end of file diff --git a/resources/[gameplay]/chat/cl_chat.lua b/resources/[gameplay]/chat/cl_chat.lua index f42d441..09e3eac 100644 --- a/resources/[gameplay]/chat/cl_chat.lua +++ b/resources/[gameplay]/chat/cl_chat.lua @@ -2,7 +2,6 @@ local isRDR = not TerraingridActivate and true or false local chatInputActive = false local chatInputActivating = false -local chatHidden = true local chatLoaded = false RegisterNetEvent('chatMessage') @@ -10,6 +9,8 @@ RegisterNetEvent('chat:addTemplate') RegisterNetEvent('chat:addMessage') RegisterNetEvent('chat:addSuggestion') RegisterNetEvent('chat:addSuggestions') +RegisterNetEvent('chat:addMode') +RegisterNetEvent('chat:removeMode') RegisterNetEvent('chat:removeSuggestion') RegisterNetEvent('chat:clear') @@ -47,14 +48,25 @@ AddEventHandler('__cfx_internal:serverPrint', function(msg) }) end) -AddEventHandler('chat:addMessage', function(message) +-- addMessage +local addMessage = function(message) + if type(message) == 'string' then + message = { + args = { message } + } + end + SendNUIMessage({ type = 'ON_MESSAGE', message = message }) -end) +end -AddEventHandler('chat:addSuggestion', function(name, help, params) +exports('addMessage', addMessage) +AddEventHandler('chat:addMessage', addMessage) + +-- addSuggestion +local addSuggestion = function(name, help, params) SendNUIMessage({ type = 'ON_SUGGESTION_ADD', suggestion = { @@ -63,7 +75,10 @@ AddEventHandler('chat:addSuggestion', function(name, help, params) params = params or nil } }) -end) +end + +exports('addSuggestion', addSuggestion) +AddEventHandler('chat:addSuggestion', addSuggestion) AddEventHandler('chat:addSuggestions', function(suggestions) for _, suggestion in ipairs(suggestions) do @@ -81,6 +96,20 @@ AddEventHandler('chat:removeSuggestion', function(name) }) end) +AddEventHandler('chat:addMode', function(mode) + SendNUIMessage({ + type = 'ON_MODE_ADD', + mode = mode + }) +end) + +AddEventHandler('chat:removeMode', function(name) + SendNUIMessage({ + type = 'ON_MODE_REMOVE', + name = name + }) +end) + AddEventHandler('chat:addTemplate', function(id, html) SendNUIMessage({ type = 'ON_TEMPLATE_ADD', @@ -110,7 +139,7 @@ RegisterNUICallback('chatResult', function(data, cb) if data.message:sub(1, 1) == '/' then ExecuteCommand(data.message:sub(2)) else - TriggerServerEvent('_chat:messageEntered', GetPlayerName(id), { r, g, b }, data.message) + TriggerServerEvent('_chat:messageEntered', GetPlayerName(id), { r, g, b }, data.message, data.mode) end end @@ -124,7 +153,7 @@ local function refreshCommands() local suggestions = {} for _, command in ipairs(registeredCommands) do - if IsAceAllowed(('command.%s'):format(command.name)) then + if IsAceAllowed(('command.%s'):format(command.name)) and command.name ~= 'toggleChat' then table.insert(suggestions, { name = '/' .. command.name, help = '' @@ -178,7 +207,7 @@ AddEventHandler('onClientResourceStop', function(resName) end) RegisterNUICallback('loaded', function(data, cb) - TriggerServerEvent('chat:init'); + TriggerServerEvent('chat:init') refreshCommands() refreshThemes() @@ -188,10 +217,41 @@ RegisterNUICallback('loaded', function(data, cb) cb('ok') end) +local CHAT_HIDE_STATES = { + SHOW_WHEN_ACTIVE = 0, + ALWAYS_SHOW = 1, + ALWAYS_HIDE = 2 +} + +local kvpEntry = GetResourceKvpString('hideState') +local chatHideState = kvpEntry and tonumber(kvpEntry) or CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE +local isFirstHide = true + +if not isRDR then + RegisterKeyMapping('toggleChat', 'Toggle chat', 'keyboard', 'l') + + RegisterCommand('toggleChat', function() + if chatHideState == CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE then + chatHideState = CHAT_HIDE_STATES.ALWAYS_SHOW + elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_SHOW then + chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE + elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_HIDE then + chatHideState = CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE + end + + isFirstHide = false + + SetResourceKvp('hideState', tostring(chatHideState)) + end, false) +end + Citizen.CreateThread(function() SetTextChatEnabled(false) SetNuiFocus(false) + local lastChatHideState = -1 + local origChatHideState = -1 + while true do Wait(0) @@ -215,19 +275,26 @@ Citizen.CreateThread(function() end if chatLoaded then - local shouldBeHidden = false + local forceHide = IsScreenFadedOut() or IsPauseMenuActive() - if IsScreenFadedOut() or IsPauseMenuActive() then - shouldBeHidden = true + if forceHide then + origChatHideState = chatHideState + chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE + elseif origChatHideState ~= -1 then + chatHideState = origChatHideState + origChatHideState = -1 end - if (shouldBeHidden and not chatHidden) or (not shouldBeHidden and chatHidden) then - chatHidden = shouldBeHidden + if chatHideState ~= lastChatHideState then + lastChatHideState = chatHideState SendNUIMessage({ type = 'ON_SCREEN_STATE_CHANGE', - shouldHide = shouldBeHidden + hideState = chatHideState, + fromUserInteraction = not forceHide and not isFirstHide }) + + isFirstHide = false end end end diff --git a/resources/[gameplay]/chat/fxmanifest.lua b/resources/[gameplay]/chat/fxmanifest.lua index 14c61bd..246f135 100644 --- a/resources/[gameplay]/chat/fxmanifest.lua +++ b/resources/[gameplay]/chat/fxmanifest.lua @@ -1,30 +1,17 @@ description 'chat management stuff' -ui_page 'html/index.html' +ui_page 'dist/ui.html' client_script 'cl_chat.lua' server_script 'sv_chat.lua' files { - 'html/index.html', - 'html/index.css', - 'html/config.default.js', - 'html/config.js', - 'html/App.js', - 'html/Message.js', - 'html/Suggestions.js', - 'html/vendor/vue.2.3.3.min.js', - 'html/vendor/flexboxgrid.6.3.1.min.css', - 'html/vendor/animate.3.5.2.min.css', - 'html/vendor/latofonts.css', - 'html/vendor/fonts/LatoRegular.woff2', - 'html/vendor/fonts/LatoRegular2.woff2', - 'html/vendor/fonts/LatoLight2.woff2', - 'html/vendor/fonts/LatoLight.woff2', - 'html/vendor/fonts/LatoBold.woff2', - 'html/vendor/fonts/LatoBold2.woff2', - } + 'dist/ui.html', + 'dist/index.css', + 'html/vendor/*.css', + 'html/vendor/fonts/*.woff2', +} fx_version 'adamant' games { 'rdr3', 'gta5' } -rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' \ No newline at end of file +rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' diff --git a/resources/[gameplay]/chat/html/App.js b/resources/[gameplay]/chat/html/App.js deleted file mode 100644 index 6e54d2f..0000000 --- a/resources/[gameplay]/chat/html/App.js +++ /dev/null @@ -1,255 +0,0 @@ -window.APP = { - template: '#app_template', - name: 'app', - data() { - return { - style: CONFIG.style, - showInput: false, - showWindow: false, - shouldHide: true, - backingSuggestions: [], - removedSuggestions: [], - templates: CONFIG.templates, - message: '', - messages: [], - oldMessages: [], - oldMessagesIndex: -1, - tplBackups: [], - msgTplBackups: [] - }; - }, - destroyed() { - clearInterval(this.focusTimer); - window.removeEventListener('message', this.listener); - }, - mounted() { - post('http://chat/loaded', JSON.stringify({})); - this.listener = window.addEventListener('message', (event) => { - const item = event.data || event.detail; //'detail' is for debuging via browsers - if (this[item.type]) { - this[item.type](item); - } - }); - }, - watch: { - messages() { - if (this.showWindowTimer) { - clearTimeout(this.showWindowTimer); - } - this.showWindow = true; - this.resetShowWindowTimer(); - - const messagesObj = this.$refs.messages; - this.$nextTick(() => { - messagesObj.scrollTop = messagesObj.scrollHeight; - }); - }, - }, - computed: { - suggestions() { - return this.backingSuggestions.filter((el) => this.removedSuggestions.indexOf(el.name) <= -1); - }, - }, - methods: { - ON_SCREEN_STATE_CHANGE({ shouldHide }) { - this.shouldHide = shouldHide; - }, - ON_OPEN() { - this.showInput = true; - this.showWindow = true; - if (this.showWindowTimer) { - clearTimeout(this.showWindowTimer); - } - this.focusTimer = setInterval(() => { - if (this.$refs.input) { - this.$refs.input.focus(); - } else { - clearInterval(this.focusTimer); - } - }, 100); - }, - ON_MESSAGE({ message }) { - this.messages.push(message); - }, - ON_CLEAR() { - this.messages = []; - this.oldMessages = []; - this.oldMessagesIndex = -1; - }, - ON_SUGGESTION_ADD({ suggestion }) { - const duplicateSuggestion = this.backingSuggestions.find(a => a.name == suggestion.name); - if (duplicateSuggestion) { - if(suggestion.help || suggestion.params) { - duplicateSuggestion.help = suggestion.help || ""; - duplicateSuggestion.params = suggestion.params || []; - } - return; - } - if (!suggestion.params) { - suggestion.params = []; //TODO Move somewhere else - } - this.backingSuggestions.push(suggestion); - }, - ON_SUGGESTION_REMOVE({ name }) { - if(this.removedSuggestions.indexOf(name) <= -1) { - this.removedSuggestions.push(name); - } - }, - ON_TEMPLATE_ADD({ template }) { - if (this.templates[template.id]) { - this.warn(`Tried to add duplicate template '${template.id}'`) - } else { - this.templates[template.id] = template.html; - } - }, - ON_UPDATE_THEMES({ themes }) { - this.removeThemes(); - - this.setThemes(themes); - }, - removeThemes() { - for (let i = 0; i < document.styleSheets.length; i++) { - const styleSheet = document.styleSheets[i]; - const node = styleSheet.ownerNode; - - if (node.getAttribute('data-theme')) { - node.parentNode.removeChild(node); - } - } - - this.tplBackups.reverse(); - - for (const [ elem, oldData ] of this.tplBackups) { - elem.innerText = oldData; - } - - this.tplBackups = []; - - this.msgTplBackups.reverse(); - - for (const [ id, oldData ] of this.msgTplBackups) { - this.templates[id] = oldData; - } - - this.msgTplBackups = []; - }, - setThemes(themes) { - for (const [ id, data ] of Object.entries(themes)) { - if (data.style) { - const style = document.createElement('style'); - style.type = 'text/css'; - style.setAttribute('data-theme', id); - style.appendChild(document.createTextNode(data.style)); - - document.head.appendChild(style); - } - - if (data.styleSheet) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = data.baseUrl + data.styleSheet; - link.setAttribute('data-theme', id); - - document.head.appendChild(link); - } - - if (data.templates) { - for (const [ tplId, tpl ] of Object.entries(data.templates)) { - const elem = document.getElementById(tplId); - - if (elem) { - this.tplBackups.push([ elem, elem.innerText ]); - elem.innerText = tpl; - } - } - } - - if (data.script) { - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = data.baseUrl + data.script; - - document.head.appendChild(script); - } - - if (data.msgTemplates) { - for (const [ tplId, tpl ] of Object.entries(data.msgTemplates)) { - this.msgTplBackups.push([ tplId, this.templates[tplId] ]); - this.templates[tplId] = tpl; - } - } - } - }, - warn(msg) { - this.messages.push({ - args: [msg], - template: '^3CHAT-WARN: ^0{0}', - }); - }, - clearShowWindowTimer() { - clearTimeout(this.showWindowTimer); - }, - resetShowWindowTimer() { - this.clearShowWindowTimer(); - this.showWindowTimer = setTimeout(() => { - if (!this.showInput) { - this.showWindow = false; - } - }, CONFIG.fadeTimeout); - }, - keyUp() { - this.resize(); - }, - keyDown(e) { - if (e.which === 38 || e.which === 40) { - e.preventDefault(); - this.moveOldMessageIndex(e.which === 38); - } else if (e.which == 33) { - var buf = document.getElementsByClassName('chat-messages')[0]; - buf.scrollTop = buf.scrollTop - 100; - } else if (e.which == 34) { - var buf = document.getElementsByClassName('chat-messages')[0]; - buf.scrollTop = buf.scrollTop + 100; - } - }, - moveOldMessageIndex(up) { - if (up && this.oldMessages.length > this.oldMessagesIndex + 1) { - this.oldMessagesIndex += 1; - this.message = this.oldMessages[this.oldMessagesIndex]; - } else if (!up && this.oldMessagesIndex - 1 >= 0) { - this.oldMessagesIndex -= 1; - this.message = this.oldMessages[this.oldMessagesIndex]; - } else if (!up && this.oldMessagesIndex - 1 === -1) { - this.oldMessagesIndex = -1; - this.message = ''; - } - }, - resize() { - const input = this.$refs.input; - input.style.height = '5px'; - input.style.height = `${input.scrollHeight + 2}px`; - }, - send(e) { - if(this.message !== '') { - post('http://chat/chatResult', JSON.stringify({ - message: this.message, - })); - this.oldMessages.unshift(this.message); - this.oldMessagesIndex = -1; - this.hideInput(); - } else { - this.hideInput(true); - } - }, - hideInput(canceled = false) { - if (canceled) { - post('http://chat/chatResult', JSON.stringify({ canceled })); - } - this.message = ''; - this.showInput = false; - clearInterval(this.focusTimer); - this.resetShowWindowTimer(); - }, - }, -}; diff --git a/resources/[gameplay]/chat/html/App.ts b/resources/[gameplay]/chat/html/App.ts new file mode 100644 index 0000000..627e617 --- /dev/null +++ b/resources/[gameplay]/chat/html/App.ts @@ -0,0 +1,434 @@ +import { post } from './utils'; +import CONFIG from './config'; +import Vue from 'vue'; + +import Suggestions from './Suggestions.vue'; +import MessageV from './Message.vue'; +import { Suggestion } from './Suggestions'; + +export interface Message { + args: string[]; + template: string; + params?: { [key: string]: string }; + multiline?: boolean; + color?: [ number, number, number ]; + templateId?: number; + + id?: string; +} + +export interface ThemeData { + style: string; + styleSheet: string; + baseUrl: string; + script: string; + templates: { [id: string]: string }; // not supported rn + msgTemplates: { [id: string]: string }; + +} + +export interface Mode { + name: string; + displayName: string; + color: string; +} + +enum ChatHideStates { + ShowWhenActive = 0, + AlwaysShow = 1, + AlwaysHide = 2, +} + +const defaultMode: Mode = { + name: 'all', + displayName: 'All', + color: '#fff' +}; + +export default Vue.extend({ + template: "#app_template", + name: "app", + components: { + Suggestions, + MessageV + }, + data() { + return { + style: CONFIG.style, + showInput: false, + showWindow: false, + showHideState: false, + hideState: ChatHideStates.ShowWhenActive, + backingSuggestions: [] as Suggestion[], + removedSuggestions: [] as string[], + templates: { ...CONFIG.templates } as { [ key: string ]: string }, + message: "", + messages: [] as Message[], + oldMessages: [] as string[], + oldMessagesIndex: -1, + tplBackups: [] as unknown as [ HTMLElement, string ][], + msgTplBackups: [] as unknown as [ string, string ][], + focusTimer: 0, + showWindowTimer: 0, + showHideStateTimer: 0, + listener: (event: MessageEvent) => {}, + modes: [defaultMode] as Mode[], + modeIdx: 0, + }; + }, + destroyed() { + clearInterval(this.focusTimer); + window.removeEventListener("message", this.listener); + }, + mounted() { + post("http://chat/loaded", JSON.stringify({})); + + this.listener = (event: MessageEvent) => { + const item: any = event.data || (event).detail; //'detail' is for debugging via browsers + + if (!item || !item.type) { + return; + } + + const typeRef = item.type as + 'ON_OPEN' | 'ON_SCREEN_STATE_CHANGE' | 'ON_MESSAGE' | 'ON_CLEAR' | 'ON_SUGGESTION_ADD' | + 'ON_SUGGESTION_REMOVE' | 'ON_TEMPLATE_ADD' | 'ON_UPDATE_THEMES' | 'ON_MODE_ADD' | 'ON_MODE_REMOVE'; + + if (this[typeRef]) { + this[typeRef](item); + } + }; + + window.addEventListener("message", this.listener); + }, + watch: { + messages() { + if (this.hideState !== ChatHideStates.AlwaysHide) { + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + this.showWindow = true; + this.resetShowWindowTimer(); + } + + const messagesObj = this.$refs.messages as HTMLDivElement; + this.$nextTick(() => { + messagesObj.scrollTop = messagesObj.scrollHeight; + }); + } + }, + computed: { + suggestions(): Suggestion[] { + return this.backingSuggestions.filter( + el => this.removedSuggestions.indexOf(el.name) <= -1 + ); + }, + + hideAnimated(): boolean { + return this.hideState !== ChatHideStates.AlwaysHide; + }, + + modeIdxGet(): number { + return (this.modeIdx >= this.modes.length) ? (this.modes.length - 1) : this.modeIdx; + }, + + modePrefix(): string { + if (this.modes.length === 1) { + return `➤`; + } + + return this.modes[this.modeIdxGet].displayName; + }, + + modeColor(): string { + return this.modes[this.modeIdxGet].color; + }, + + hideStateString(): string { + // TODO: localization + switch (this.hideState) { + case ChatHideStates.AlwaysShow: + return 'Visible'; + case ChatHideStates.AlwaysHide: + return 'Hidden'; + case ChatHideStates.ShowWhenActive: + return 'When active'; + } + } + }, + methods: { + ON_SCREEN_STATE_CHANGE({ hideState, fromUserInteraction }: { hideState: ChatHideStates, fromUserInteraction: boolean }) { + this.hideState = hideState; + + if (this.hideState === ChatHideStates.AlwaysHide) { + if (!this.showInput) { + this.showWindow = false; + } + } else if (this.hideState === ChatHideStates.AlwaysShow) { + this.showWindow = true; + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + } else { + this.resetShowWindowTimer(); + } + + if (fromUserInteraction) { + this.showHideState = true; + + if (this.showHideStateTimer) { + clearTimeout(this.showHideStateTimer); + } + + this.showHideStateTimer = window.setTimeout(() => { + this.showHideState = false; + }, 1500); + } + }, + ON_OPEN() { + this.showInput = true; + this.showWindow = true; + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + this.focusTimer = window.setInterval(() => { + if (this.$refs.input) { + (this.$refs.input as HTMLInputElement).focus(); + } else { + clearInterval(this.focusTimer); + } + }, 100); + }, + ON_MESSAGE({ message }: { message: Message }) { + message.id = `${new Date().getTime()}${Math.random()}`; + this.messages.push(message); + }, + ON_CLEAR() { + this.messages = []; + this.oldMessages = []; + this.oldMessagesIndex = -1; + }, + ON_SUGGESTION_ADD({ suggestion }: { suggestion: Suggestion }) { + const duplicateSuggestion = this.backingSuggestions.find( + a => a.name == suggestion.name + ); + if (duplicateSuggestion) { + if (suggestion.help || suggestion.params) { + duplicateSuggestion.help = suggestion.help || ""; + duplicateSuggestion.params = suggestion.params || []; + } + return; + } + if (!suggestion.params) { + suggestion.params = []; //TODO Move somewhere else + } + this.backingSuggestions.push(suggestion); + }, + ON_SUGGESTION_REMOVE({ name }: { name: string }) { + if (this.removedSuggestions.indexOf(name) <= -1) { + this.removedSuggestions.push(name); + } + }, + ON_MODE_ADD({ mode }: { mode: Mode }) { + this.modes = [ + ...this.modes.filter(a => a.name !== mode.name), + mode + ]; + }, + ON_MODE_REMOVE({ name }: { name: string }) { + this.modes = this.modes.filter(a => a.name !== name); + + if (this.modes.length === 0) { + this.modes = [defaultMode]; + } + }, + ON_TEMPLATE_ADD({ template }: { template: { id: string, html: string }}) { + if (this.templates[template.id]) { + this.warn(`Tried to add duplicate template '${template.id}'`); + } else { + this.templates[template.id] = template.html; + } + }, + ON_UPDATE_THEMES({ themes }: { themes: { [key: string]: ThemeData } }) { + this.removeThemes(); + + this.setThemes(themes); + }, + removeThemes() { + for (let i = 0; i < document.styleSheets.length; i++) { + const styleSheet = document.styleSheets[i]; + const node = styleSheet.ownerNode as Element; + + if (node.getAttribute("data-theme")) { + node.parentNode?.removeChild(node); + } + } + + this.tplBackups.reverse(); + + for (const [elem, oldData] of this.tplBackups) { + elem.innerText = oldData; + } + + this.tplBackups = []; + + this.msgTplBackups.reverse(); + + for (const [id, oldData] of this.msgTplBackups) { + this.templates[id] = oldData; + } + + this.msgTplBackups = []; + }, + setThemes(themes: { [key: string]: ThemeData }) { + for (const [id, data] of Object.entries(themes)) { + if (data.style) { + const style = document.createElement("style"); + style.type = "text/css"; + style.setAttribute("data-theme", id); + style.appendChild(document.createTextNode(data.style)); + + document.head.appendChild(style); + } + + if (data.styleSheet) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = data.baseUrl + data.styleSheet; + link.setAttribute("data-theme", id); + + document.head.appendChild(link); + } + + if (data.templates) { + for (const [tplId, tpl] of Object.entries(data.templates)) { + const elem = document.getElementById(tplId); + + if (elem) { + this.tplBackups.push([elem, elem.innerText]); + elem.innerText = tpl; + } + } + } + + if (data.script) { + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = data.baseUrl + data.script; + + document.head.appendChild(script); + } + + if (data.msgTemplates) { + for (const [tplId, tpl] of Object.entries(data.msgTemplates)) { + this.msgTplBackups.push([tplId, this.templates[tplId]]); + this.templates[tplId] = tpl; + } + } + } + }, + warn(msg: string) { + this.messages.push({ + args: [msg], + template: "^3CHAT-WARN: ^0{0}" + }); + }, + clearShowWindowTimer() { + clearTimeout(this.showWindowTimer); + }, + resetShowWindowTimer() { + this.clearShowWindowTimer(); + this.showWindowTimer = window.setTimeout(() => { + if (this.hideState !== ChatHideStates.AlwaysShow && !this.showInput) { + this.showWindow = false; + } + }, CONFIG.fadeTimeout); + }, + keyUp() { + this.resize(); + }, + keyDown(e: KeyboardEvent) { + if (e.which === 38 || e.which === 40) { + e.preventDefault(); + this.moveOldMessageIndex(e.which === 38); + } else if (e.which == 33) { + var buf = document.getElementsByClassName("chat-messages")[0]; + buf.scrollTop = buf.scrollTop - 100; + } else if (e.which == 34) { + var buf = document.getElementsByClassName("chat-messages")[0]; + buf.scrollTop = buf.scrollTop + 100; + } else if (e.which === 9) { // tab + if (e.shiftKey || e.altKey) { + --this.modeIdx; + + if (this.modeIdx < 0) { + this.modeIdx = this.modes.length - 1; + } + } else { + this.modeIdx = (this.modeIdx + 1) % this.modes.length; + } + } + + this.resize(); + }, + moveOldMessageIndex(up: boolean) { + if (up && this.oldMessages.length > this.oldMessagesIndex + 1) { + this.oldMessagesIndex += 1; + this.message = this.oldMessages[this.oldMessagesIndex]; + } else if (!up && this.oldMessagesIndex - 1 >= 0) { + this.oldMessagesIndex -= 1; + this.message = this.oldMessages[this.oldMessagesIndex]; + } else if (!up && this.oldMessagesIndex - 1 === -1) { + this.oldMessagesIndex = -1; + this.message = ""; + } + }, + resize() { + const input = this.$refs.input as HTMLInputElement; + + // scrollHeight includes padding, but content-box excludes padding + // remove padding before setting height on the element + const style = getComputedStyle(input); + const paddingRemove = parseFloat(style.paddingBottom) + parseFloat(style.paddingTop); + + input.style.height = "5px"; + input.style.height = `${input.scrollHeight - paddingRemove}px`; + }, + send() { + if (this.message !== "") { + post( + "http://chat/chatResult", + JSON.stringify({ + message: this.message, + mode: this.modes[this.modeIdxGet].name + }) + ); + this.oldMessages.unshift(this.message); + this.oldMessagesIndex = -1; + this.hideInput(); + } else { + this.hideInput(true); + } + }, + hideInput(canceled = false) { + setTimeout(() => { + const input = this.$refs.input as HTMLInputElement; + delete input.style.height; + }, 50); + + if (canceled) { + post("http://chat/chatResult", JSON.stringify({ canceled })); + } + this.message = ""; + this.showInput = false; + clearInterval(this.focusTimer); + + if (this.hideState !== ChatHideStates.AlwaysHide) { + this.resetShowWindowTimer(); + } else { + this.showWindow = false; + } + } + } +}); \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/App.vue b/resources/[gameplay]/chat/html/App.vue new file mode 100644 index 0000000..b6eade7 --- /dev/null +++ b/resources/[gameplay]/chat/html/App.vue @@ -0,0 +1,44 @@ + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/Message.js b/resources/[gameplay]/chat/html/Message.ts similarity index 58% rename from resources/[gameplay]/chat/html/Message.js rename to resources/[gameplay]/chat/html/Message.ts index 067d842..9326fd7 100644 --- a/resources/[gameplay]/chat/html/Message.js +++ b/resources/[gameplay]/chat/html/Message.ts @@ -1,42 +1,48 @@ -Vue.component('message', { - template: '#message_template', +import CONFIG from './config'; +import Vue, { PropType } from 'vue'; + +export default Vue.component('message', { data() { return {}; }, computed: { - textEscaped() { + textEscaped(): string { let s = this.template ? this.template : this.templates[this.templateId]; - if (this.template) { - //We disable templateId since we are using a direct raw template - this.templateId = -1; - } - //This hack is required to preserve backwards compatability - if (this.templateId == CONFIG.defaultTemplateId + if (!this.template && this.templateId == CONFIG.defaultTemplateId && this.args.length == 1) { s = this.templates[CONFIG.defaultAltTemplateId] //Swap out default template :/ } + s = s.replace(`@default`, this.templates[this.templateId]); + s = s.replace(/{(\d+)}/g, (match, number) => { - const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match + const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match; if (number == 0 && this.color) { //color is deprecated, use templates or ^1 etc. return this.colorizeOld(argEscaped); } return argEscaped; }); + + // format variant args + s = s.replace(/\{\{([a-zA-Z0-9_\-]+?)\}\}/g, (match, id) => { + const argEscaped = this.params[id] != undefined ? this.escape(this.params[id]) : match; + return argEscaped; + }); + return this.colorize(s); }, }, methods: { - colorizeOld(str) { + colorizeOld(str: string): string { return `${str}` }, - colorize(str) { - let s = "" + (str.replace(/\^([0-9])/g, (str, color) => ``)) + ""; + colorize(str: string): string { + let s = "" + colorTrans(str) + ""; - const styleDict = { + const styleDict: {[ key: string ]: string} = { '*': 'font-weight: bold;', '_': 'text-decoration: underline;', '~': 'text-decoration: line-through;', @@ -49,8 +55,15 @@ Vue.component('message', { s = s.replace(styleRegex, (str, style, inner) => `${inner}`) } return s.replace(/]*><\/span[^>]*>/g, ''); + + function colorTrans(str: string) { + return str + .replace(/\^([0-9])/g, (str, color) => ``) + .replace(/\^#([0-9A-F]{3,6})/gi, (str, color) => ``) + .replace(/~([a-z])~/g, (str, color) => ``); + } }, - escape(unsafe) { + escape(unsafe: string): string { return String(unsafe) .replace(/&/g, '&') .replace(/, }, args: { - type: Array, + type: Array as PropType, + }, + params: { + type: Object as PropType<{ [ key: string]: string }>, }, template: { type: String, @@ -79,8 +95,8 @@ Vue.component('message', { default: false, }, color: { //deprecated - type: Array, - default: false, + type: Array as PropType, + default: null, }, }, }); diff --git a/resources/[gameplay]/chat/html/Message.vue b/resources/[gameplay]/chat/html/Message.vue new file mode 100644 index 0000000..7f87cea --- /dev/null +++ b/resources/[gameplay]/chat/html/Message.vue @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/Suggestions.js b/resources/[gameplay]/chat/html/Suggestions.ts similarity index 75% rename from resources/[gameplay]/chat/html/Suggestions.js rename to resources/[gameplay]/chat/html/Suggestions.ts index 07c4688..08cd9ae 100644 --- a/resources/[gameplay]/chat/html/Suggestions.js +++ b/resources/[gameplay]/chat/html/Suggestions.ts @@ -1,11 +1,29 @@ -Vue.component('suggestions', { - template: '#suggestions_template', - props: ['message', 'suggestions'], +import CONFIG from './config'; +import Vue, { PropType } from 'vue'; + +export interface Suggestion { + name: string; + help: string; + params: string[]; + + disabled: boolean; +} + +export default Vue.component('suggestions', { + props: { + message: { + type: String + }, + + suggestions: { + type: Array as PropType + } + }, data() { return {}; }, computed: { - currentSuggestions() { + currentSuggestions(): Suggestion[] { if (this.message === '') { return []; } @@ -34,6 +52,7 @@ Vue.component('suggestions', { const regex = new RegExp(`${s.name} (?:\\w+ ){${index}}(?:${wType}*)$`, 'g'); // eslint-disable-next-line no-param-reassign + // @ts-ignore p.disabled = this.message.match(regex) == null; }); }); diff --git a/resources/[gameplay]/chat/html/Suggestions.vue b/resources/[gameplay]/chat/html/Suggestions.vue new file mode 100644 index 0000000..d8d5c38 --- /dev/null +++ b/resources/[gameplay]/chat/html/Suggestions.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/config.default.js b/resources/[gameplay]/chat/html/config.ts similarity index 81% rename from resources/[gameplay]/chat/html/config.default.js rename to resources/[gameplay]/chat/html/config.ts index 82192ad..bd76f77 100644 --- a/resources/[gameplay]/chat/html/config.default.js +++ b/resources/[gameplay]/chat/html/config.ts @@ -1,6 +1,4 @@ -// DO NOT EDIT THIS FILE -// Copy it to `config.js` and edit it -window.CONFIG = { +export default { defaultTemplateId: 'default', //This is the default template for 2 args1 defaultAltTemplateId: 'defaultAlt', //This one for 1 arg templates: { //You can add static templates here @@ -13,7 +11,7 @@ window.CONFIG = { suggestionLimit: 5, style: { background: 'rgba(52, 73, 94, 0.7)', - width: '38%', + width: '38vw', height: '22%', } }; diff --git a/resources/[gameplay]/chat/html/index.css b/resources/[gameplay]/chat/html/index.css index 30f1699..df5c1c2 100644 --- a/resources/[gameplay]/chat/html/index.css +++ b/resources/[gameplay]/chat/html/index.css @@ -8,6 +8,14 @@ .color-8{color: #cc0000;} .color-9{color: #cc0068;} +.gameColor-w{color: #ffffff;} +.gameColor-r{color: #ff4444;} +.gameColor-g{color: #99cc00;} +.gameColor-y{color: #ffbb33;} +.gameColor-b{color: #33b5e5;} + +/* todo: more game colors */ + * { font-family: 'Lato', sans-serif; margin: 0; @@ -63,26 +71,47 @@ em { box-sizing: border-box; } +.chat-input > div.input { + position: relative; + display: flex; + align-items: stretch; + width: 100%; + background-color: rgba(44, 62, 80, 1.0); +} + +.chat-hide-state { + text-transform: uppercase; + margin-left: 0.05vw; + font-size: 1.65vh; +} + .prefix { font-size: 1.8vh; - position: absolute; - margin-top: 0.5%; - left: 0.208%; + /*position: absolute; + top: 0%;*/ + height: 100%; + vertical-align: middle; + line-height: calc(1vh + 1vh + 1.85vh); + padding-left: 0.5vh; + text-transform: uppercase; + font-weight: bold; + display: inline-block; } textarea { font-size: 1.65vh; + line-height: 1.85vh; display: block; - box-sizing: border-box; - padding: 1%; - padding-left: 3.5%; + box-sizing: content-box; + padding: 1vh; + padding-left: 0.5vh; color: white; - background-color: rgba(44, 62, 80, 1.0); - width: 100%; border-width: 0; height: 3.15%; overflow: hidden; text-overflow: ellipsis; + flex: 1; + background-color: transparent; } textarea:focus, input:focus { @@ -123,5 +152,9 @@ textarea:focus, input:focus { } .hidden { - display: none; + opacity: 0; +} + +.hidden.animated { + transition: opacity 1s; } \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/index.d.ts b/resources/[gameplay]/chat/html/index.d.ts new file mode 100644 index 0000000..314e3aa --- /dev/null +++ b/resources/[gameplay]/chat/html/index.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue' + export default Vue +} \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/index.html b/resources/[gameplay]/chat/html/index.html index 6ce65dd..c9b40ac 100644 --- a/resources/[gameplay]/chat/html/index.html +++ b/resources/[gameplay]/chat/html/index.html @@ -3,115 +3,12 @@ - - - + + + - - - -
- - - - - - - - - - - - - - - - - - diff --git a/resources/[gameplay]/chat/html/main.ts b/resources/[gameplay]/chat/html/main.ts new file mode 100644 index 0000000..9a487fd --- /dev/null +++ b/resources/[gameplay]/chat/html/main.ts @@ -0,0 +1,7 @@ +import Vue from 'vue'; +import App from './App.vue'; + +const instance = new Vue({ + el: '#app', + render: h => h(App), +}); \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/tsconfig.json b/resources/[gameplay]/chat/html/tsconfig.json new file mode 100644 index 0000000..9a44563 --- /dev/null +++ b/resources/[gameplay]/chat/html/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "./", + "module": "es6", + "strict": true, + "moduleResolution": "node", + "target": "es6", + "allowJs": true, + "lib": [ + "es2017", + "dom" + ] + }, + "include": [ + "./**/*" + ], + "exclude": [] +} \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/utils.ts b/resources/[gameplay]/chat/html/utils.ts new file mode 100644 index 0000000..76c30e5 --- /dev/null +++ b/resources/[gameplay]/chat/html/utils.ts @@ -0,0 +1,31 @@ +export function post(url: string, data: any) { + var request = new XMLHttpRequest(); + request.open('POST', url, true); + request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + request.send(data); +} + +function emulate(type: string, detail = {}) { + const detailRef = { + type, + ...detail + }; + + window.dispatchEvent(new CustomEvent('message', { + detail: detailRef + })); +} + +(window as any)['emulate'] = emulate; + +(window as any)['demo'] = () => { + emulate('ON_MESSAGE', { + message: { + args: [ 'me', 'hello!' ] + } + }) + + emulate('ON_SCREEN_STATE_CHANGE', { + shouldHide: false + }); +}; \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/vendor/vue.2.3.3.min.js b/resources/[gameplay]/chat/html/vendor/vue.2.3.3.min.js deleted file mode 100644 index 757d5aa..0000000 --- a/resources/[gameplay]/chat/html/vendor/vue.2.3.3.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * Vue.js v2.3.3 - * (c) 2014-2017 Evan You - * Released under the MIT License. - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";function e(e){return void 0===e||null===e}function t(e){return void 0!==e&&null!==e}function n(e){return!0===e}function r(e){return!1===e}function i(e){return"string"==typeof e||"number"==typeof e}function o(e){return null!==e&&"object"==typeof e}function a(e){return"[object Object]"===Ti.call(e)}function s(e){return"[object RegExp]"===Ti.call(e)}function c(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function u(e){var t=parseFloat(e);return isNaN(t)?e:t}function l(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i-1)return e.splice(n,1)}}function p(e,t){return ji.call(e,t)}function d(e){var t=Object.create(null);return function(n){return t[n]||(t[n]=e(n))}}function v(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function h(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function m(e,t){for(var n in t)e[n]=t[n];return e}function g(e){for(var t={},n=0;nTo&&wo[n].id>e.id;)n--;wo.splice(n+1,0,e)}else wo.push(e);Oo||(Oo=!0,ao(xe))}}function Se(e){No.clear(),Te(e,No)}function Te(e,t){var n,r,i=Array.isArray(e);if((i||o(e))&&Object.isExtensible(e)){if(e.__ob__){var a=e.__ob__.dep.id;if(t.has(a))return;t.add(a)}if(i)for(n=e.length;n--;)Te(e[n],t);else for(r=Object.keys(e),n=r.length;n--;)Te(e[r[n]],t)}}function Ee(e,t,n){Lo.get=function(){return this[t][n]},Lo.set=function(e){this[t][n]=e},Object.defineProperty(e,n,Lo)}function je(e){e._watchers=[];var t=e.$options;t.props&&Ne(e,t.props),t.methods&&Re(e,t.methods),t.data?Le(e):j(e._data={},!0),t.computed&&De(e,t.computed),t.watch&&Fe(e,t.watch)}function Ne(e,t){var n=e.$options.propsData||{},r=e._props={},i=e.$options._propKeys=[],o=!e.$parent;vo.shouldConvert=o;for(var a in t)!function(o){i.push(o);var a=V(o,t,n,e);N(r,o,a),o in e||Ee(e,"_props",o)}(a);vo.shouldConvert=!0}function Le(e){var t=e.$options.data;t=e._data="function"==typeof t?Ie(t,e):t||{},a(t)||(t={});for(var n=Object.keys(t),r=e.$options.props,i=n.length;i--;)r&&p(r,n[i])||C(n[i])||Ee(e,"_data",n[i]);j(t,!0)}function Ie(e,t){try{return e.call(t)}catch(e){return k(e,t,"data()"),{}}}function De(e,t){var n=e._computedWatchers=Object.create(null);for(var r in t){var i=t[r],o="function"==typeof i?i:i.get;n[r]=new jo(e,o,y,Io),r in e||Me(e,r,i)}}function Me(e,t,n){"function"==typeof n?(Lo.get=Pe(t),Lo.set=y):(Lo.get=n.get?!1!==n.cache?Pe(t):n.get:y,Lo.set=n.set?n.set:y),Object.defineProperty(e,t,Lo)}function Pe(e){return function(){var t=this._computedWatchers&&this._computedWatchers[e];if(t)return t.dirty&&t.evaluate(),co.target&&t.depend(),t.value}}function Re(e,t){e.$options.props;for(var n in t)e[n]=null==t[n]?y:v(t[n],e)}function Fe(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i=0||n.indexOf(e[i])<0)&&r.push(e[i]);return r}return e}function vt(e){this._init(e)}function ht(e){e.use=function(e){if(e.installed)return this;var t=h(arguments,1);return t.unshift(this),"function"==typeof e.install?e.install.apply(e,t):"function"==typeof e&&e.apply(null,t),e.installed=!0,this}}function mt(e){e.mixin=function(e){return this.options=H(this.options,e),this}}function gt(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,r=n.cid,i=e._Ctor||(e._Ctor={});if(i[r])return i[r];var o=e.name||n.options.name,a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=H(n.options,e),a.super=n,a.options.props&&yt(a),a.options.computed&&_t(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,Ri.forEach(function(e){a[e]=n[e]}),o&&(a.options.components[o]=a),a.superOptions=n.options,a.extendOptions=e,a.sealedOptions=m({},a.options),i[r]=a,a}}function yt(e){var t=e.options.props;for(var n in t)Ee(e.prototype,"_props",n)}function _t(e){var t=e.options.computed;for(var n in t)Me(e.prototype,n,t[n])}function bt(e){Ri.forEach(function(t){e[t]=function(e,n){return n?("component"===t&&a(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}})}function $t(e){return e&&(e.Ctor.options.name||e.tag)}function Ct(e,t){return"string"==typeof e?e.split(",").indexOf(t)>-1:!!s(e)&&e.test(t)}function xt(e,t,n){for(var r in e){var i=e[r];if(i){var o=$t(i.componentOptions);o&&!n(o)&&(i!==t&&wt(i),e[r]=null)}}}function wt(e){e&&e.componentInstance.$destroy()}function kt(e){for(var n=e.data,r=e,i=e;t(i.componentInstance);)i=i.componentInstance._vnode,i.data&&(n=At(i.data,n));for(;t(r=r.parent);)r.data&&(n=At(n,r.data));return Ot(n)}function At(e,n){return{staticClass:St(e.staticClass,n.staticClass),class:t(e.class)?[e.class,n.class]:n.class}}function Ot(e){var n=e.class,r=e.staticClass;return t(r)||t(n)?St(r,Tt(n)):""}function St(e,t){return e?t?e+" "+t:e:t||""}function Tt(n){if(e(n))return"";if("string"==typeof n)return n;var r="";if(Array.isArray(n)){for(var i,a=0,s=n.length;a-1?pa[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:pa[e]=/HTMLUnknownElement/.test(t.toString())}function Nt(e){if("string"==typeof e){var t=document.querySelector(e);return t||document.createElement("div")}return e}function Lt(e,t){var n=document.createElement(e);return"select"!==e?n:(t.data&&t.data.attrs&&void 0!==t.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n)}function It(e,t){return document.createElementNS(sa[e],t)}function Dt(e){return document.createTextNode(e)}function Mt(e){return document.createComment(e)}function Pt(e,t,n){e.insertBefore(t,n)}function Rt(e,t){e.removeChild(t)}function Ft(e,t){e.appendChild(t)}function Bt(e){return e.parentNode}function Ht(e){return e.nextSibling}function Ut(e){return e.tagName}function Vt(e,t){e.textContent=t}function zt(e,t,n){e.setAttribute(t,n)}function Jt(e,t){var n=e.data.ref;if(n){var r=e.context,i=e.componentInstance||e.elm,o=r.$refs;t?Array.isArray(o[n])?f(o[n],i):o[n]===i&&(o[n]=void 0):e.data.refInFor?Array.isArray(o[n])&&o[n].indexOf(i)<0?o[n].push(i):o[n]=[i]:o[n]=i}}function Kt(e,n){return e.key===n.key&&e.tag===n.tag&&e.isComment===n.isComment&&t(e.data)===t(n.data)&&qt(e,n)}function qt(e,n){if("input"!==e.tag)return!0;var r;return(t(r=e.data)&&t(r=r.attrs)&&r.type)===(t(r=n.data)&&t(r=r.attrs)&&r.type)}function Wt(e,n,r){var i,o,a={};for(i=n;i<=r;++i)o=e[i].key,t(o)&&(a[o]=i);return a}function Zt(e,t){(e.data.directives||t.data.directives)&&Gt(e,t)}function Gt(e,t){var n,r,i,o=e===ha,a=t===ha,s=Yt(e.data.directives,e.context),c=Yt(t.data.directives,t.context),u=[],l=[];for(n in c)r=s[n],i=c[n],r?(i.oldValue=r.value,Xt(i,"update",t,e),i.def&&i.def.componentUpdated&&l.push(i)):(Xt(i,"bind",t,e),i.def&&i.def.inserted&&u.push(i));if(u.length){var f=function(){for(var n=0;n=0&&" "===(m=e.charAt(h));h--);m&&Ca.test(m)||(l=!0)}}else void 0===o?(v=i+1,o=e.slice(0,i).trim()):t();if(void 0===o?o=e.slice(0,i).trim():0!==v&&t(),a)for(i=0;i=Vo}function _n(e){return 34===e||39===e}function bn(e){var t=1;for(qo=Ko;!yn();)if(e=gn(),_n(e))$n(e);else if(91===e&&t++,93===e&&t--,0===t){Wo=Ko;break}}function $n(e){for(var t=e;!yn()&&(e=gn())!==t;);}function Cn(e,t,n){Zo=n;var r=t.value,i=t.modifiers,o=e.tag,a=e.attrsMap.type;if("select"===o)kn(e,r,i);else if("input"===o&&"checkbox"===a)xn(e,r,i);else if("input"===o&&"radio"===a)wn(e,r,i);else if("input"===o||"textarea"===o)An(e,r,i);else if(!Bi.isReservedTag(o))return vn(e,r,i),!1;return!0}function xn(e,t,n){var r=n&&n.number,i=pn(e,"value")||"null",o=pn(e,"true-value")||"true",a=pn(e,"false-value")||"false";cn(e,"checked","Array.isArray("+t+")?_i("+t+","+i+")>-1"+("true"===o?":("+t+")":":_q("+t+","+o+")")),fn(e,wa,"var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+o+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$c){$$i<0&&("+t+"=$$a.concat($$v))}else{$$i>-1&&("+t+"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{"+hn(t,"$$c")+"}",null,!0)}function wn(e,t,n){var r=n&&n.number,i=pn(e,"value")||"null";i=r?"_n("+i+")":i,cn(e,"checked","_q("+t+","+i+")"),fn(e,wa,hn(t,i),null,!0)}function kn(e,t,n){var r=n&&n.number,i='Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return '+(r?"_n(val)":"val")+"})",o="var $$selectedVal = "+i+";";o=o+" "+hn(t,"$event.target.multiple ? $$selectedVal : $$selectedVal[0]"),fn(e,"change",o,null,!0)}function An(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,u=o?"change":"range"===r?xa:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=hn(t,l);c&&(f="if($event.target.composing)return;"+f),cn(e,"value","("+t+")"),fn(e,u,f,null,!0),(s||a||"number"===r)&&fn(e,"blur","$forceUpdate()")}function On(e){var n;t(e[xa])&&(n=qi?"change":"input",e[n]=[].concat(e[xa],e[n]||[]),delete e[xa]),t(e[wa])&&(n=Qi?"click":"change",e[n]=[].concat(e[wa],e[n]||[]),delete e[wa])}function Sn(e,t,n,r,i){if(n){var o=t,a=Go;t=function(n){null!==(1===arguments.length?o(n):o.apply(null,arguments))&&Tn(e,t,r,a)}}Go.addEventListener(e,t,Xi?{capture:r,passive:i}:r)}function Tn(e,t,n,r){(r||Go).removeEventListener(e,t,n)}function En(t,n){if(!e(t.data.on)||!e(n.data.on)){var r=n.data.on||{},i=t.data.on||{};Go=n.elm,On(r),Y(r,i,Sn,Tn,n.context)}}function jn(n,r){if(!e(n.data.domProps)||!e(r.data.domProps)){var i,o,a=r.elm,s=n.data.domProps||{},c=r.data.domProps||{};t(c.__ob__)&&(c=r.data.domProps=m({},c));for(i in s)e(c[i])&&(a[i]="");for(i in c)if(o=c[i],"textContent"!==i&&"innerHTML"!==i||(r.children&&(r.children.length=0),o!==s[i]))if("value"===i){a._value=o;var u=e(o)?"":String(o);Nn(a,r,u)&&(a.value=u)}else a[i]=o}}function Nn(e,t,n){return!e.composing&&("option"===t.tag||Ln(e,n)||In(e,n))}function Ln(e,t){return document.activeElement!==e&&e.value!==t}function In(e,n){var r=e.value,i=e._vModifiers;return t(i)&&i.number||"number"===e.type?u(r)!==u(n):t(i)&&i.trim?r.trim()!==n.trim():r!==n}function Dn(e){var t=Mn(e.style);return e.staticStyle?m(e.staticStyle,t):t}function Mn(e){return Array.isArray(e)?g(e):"string"==typeof e?Oa(e):e}function Pn(e,t){var n,r={};if(t)for(var i=e;i.componentInstance;)i=i.componentInstance._vnode,i.data&&(n=Dn(i.data))&&m(r,n);(n=Dn(e.data))&&m(r,n);for(var o=e;o=o.parent;)o.data&&(n=Dn(o.data))&&m(r,n);return r}function Rn(n,r){var i=r.data,o=n.data;if(!(e(i.staticStyle)&&e(i.style)&&e(o.staticStyle)&&e(o.style))){var a,s,c=r.elm,u=o.staticStyle,l=o.normalizedStyle||o.style||{},f=u||l,p=Mn(r.data.style)||{};r.data.normalizedStyle=t(p.__ob__)?m({},p):p;var d=Pn(r,!0);for(s in f)e(d[s])&&Ea(c,s,"");for(s in d)(a=d[s])!==f[s]&&Ea(c,s,null==a?"":a)}}function Fn(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function Bn(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t);else{for(var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");e.setAttribute("class",n.trim())}}function Hn(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&m(t,Ia(e.name||"v")),m(t,e),t}return"string"==typeof e?Ia(e):void 0}}function Un(e){Ua(function(){Ua(e)})}function Vn(e,t){(e._transitionClasses||(e._transitionClasses=[])).push(t),Fn(e,t)}function zn(e,t){e._transitionClasses&&f(e._transitionClasses,t),Bn(e,t)}function Jn(e,t,n){var r=Kn(e,t),i=r.type,o=r.timeout,a=r.propCount;if(!i)return n();var s=i===Ma?Fa:Ha,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=a&&u()};setTimeout(function(){c0&&(n=Ma,l=a,f=o.length):t===Pa?u>0&&(n=Pa,l=u,f=c.length):(l=Math.max(a,u),n=l>0?a>u?Ma:Pa:null,f=n?n===Ma?o.length:c.length:0),{type:n,timeout:l,propCount:f,hasTransform:n===Ma&&Va.test(r[Ra+"Property"])}}function qn(e,t){for(;e.length1}function Xn(e,t){!0!==t.data.show&&Zn(t)}function er(e,t,n){var r=t.value,i=e.multiple;if(!i||Array.isArray(r)){for(var o,a,s=0,c=e.options.length;s-1,a.selected!==o&&(a.selected=o);else if(_(nr(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function tr(e,t){for(var n=0,r=t.length;n=0&&a[i].lowerCasedTag!==s;i--);else i=0;if(i>=0){for(var c=a.length-1;c>=i;c--)t.end&&t.end(a[c].tag,n,r);a.length=i,o=i&&a[i-1].tag}else"br"===s?t.start&&t.start(e,[],!0,n,r):"p"===s&&(t.start&&t.start(e,[],!1,n,r),t.end&&t.end(e,n,r))}for(var i,o,a=[],s=t.expectHTML,c=t.isUnaryTag||Di,u=t.canBeLeftOpenTag||Di,l=0;e;){if(i=e,o&&Ds(o)){var f=o.toLowerCase(),p=Ms[f]||(Ms[f]=new RegExp("([\\s\\S]*?)(]*>)","i")),d=0,v=e.replace(p,function(e,n,r){return d=r.length,Ds(f)||"noscript"===f||(n=n.replace(//g,"$1").replace(//g,"$1")),t.chars&&t.chars(n),""});l+=e.length-v.length,e=v,r(f,l-d,l)}else{var h=e.indexOf("<");if(0===h){if(vs.test(e)){var m=e.indexOf("--\x3e");if(m>=0){n(m+3);continue}}if(hs.test(e)){var g=e.indexOf("]>");if(g>=0){n(g+2);continue}}var y=e.match(ds);if(y){n(y[0].length);continue}var _=e.match(ps);if(_){var b=l;n(_[0].length),r(_[1],b,l);continue}var $=function(){var t=e.match(ls);if(t){var r={tagName:t[1],attrs:[],start:l};n(t[0].length);for(var i,o;!(i=e.match(fs))&&(o=e.match(cs));)n(o[0].length),r.attrs.push(o);if(i)return r.unarySlash=i[1],n(i[0].length),r.end=l,r}}();if($){!function(e){var n=e.tagName,i=e.unarySlash;s&&("p"===o&&as(n)&&r(o),u(n)&&o===n&&r(n));for(var l=c(n)||"html"===n&&"head"===o||!!i,f=e.attrs.length,p=new Array(f),d=0;d=0){for(x=e.slice(h);!(ps.test(x)||ls.test(x)||vs.test(x)||hs.test(x)||(w=x.indexOf("<",1))<0);)h+=w,x=e.slice(h);C=e.substring(0,h),n(h)}h<0&&(C=e,e=""),t.chars&&C&&t.chars(C)}if(e===i){t.chars&&t.chars(e);break}}r()}function yr(e,t){var n=t?Hs(t):Bs;if(n.test(e)){for(var r,i,o=[],a=n.lastIndex=0;r=n.exec(e);){i=r.index,i>a&&o.push(JSON.stringify(e.slice(a,i)));var s=rn(r[1].trim());o.push("_s("+s+")"),a=i+r[0].length}return a0,Zi=Ki&&Ki.indexOf("edge/")>0,Gi=Ki&&Ki.indexOf("android")>0,Yi=Ki&&/iphone|ipad|ipod|ios/.test(Ki),Qi=Ki&&/chrome\/\d+/.test(Ki)&&!Zi,Xi=!1;if(Ji)try{var eo={};Object.defineProperty(eo,"passive",{get:function(){Xi=!0}}),window.addEventListener("test-passive",null,eo)}catch(e){}var to,no,ro=function(){return void 0===to&&(to=!Ji&&"undefined"!=typeof global&&"server"===global.process.env.VUE_ENV),to},io=Ji&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,oo="undefined"!=typeof Symbol&&A(Symbol)&&"undefined"!=typeof Reflect&&A(Reflect.ownKeys),ao=function(){function e(){r=!1;var e=n.slice(0);n.length=0;for(var t=0;t1?h(n):n;for(var r=h(arguments,1),i=0,o=n.length;i1&&(t[n[0].trim()]=n[1].trim())}}),t}),Sa=/^--/,Ta=/\s*!important$/,Ea=function(e,t,n){if(Sa.test(t))e.style.setProperty(t,n);else if(Ta.test(n))e.style.setProperty(t,n.replace(Ta,""),"important");else{var r=Na(t);if(Array.isArray(n))for(var i=0,o=n.length;iv?(f=e(i[g+1])?null:i[g+1].elm,y(n,f,i,d,g,o)):d>g&&b(n,r,p,v)}function x(r,i,o,a){if(r!==i){if(n(i.isStatic)&&n(r.isStatic)&&i.key===r.key&&(n(i.isCloned)||n(i.isOnce)))return i.elm=r.elm,void(i.componentInstance=r.componentInstance);var s,c=i.data;t(c)&&t(s=c.hook)&&t(s=s.prepatch)&&s(r,i);var u=i.elm=r.elm,l=r.children,f=i.children;if(t(c)&&h(i)){for(s=0;s',n.innerHTML.indexOf(t)>0}("\n"," "),is=l("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr"),os=l("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source"),as=l("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track"),ss=[/"([^"]*)"+/.source,/'([^']*)'+/.source,/([^\s"'=<>`]+)/.source],cs=new RegExp("^\\s*"+/([^\s"'<>\/=]+)/.source+"(?:\\s*("+/(?:=)/.source+")\\s*(?:"+ss.join("|")+"))?"),us="[a-zA-Z_][\\w\\-\\.]*",ls=new RegExp("^<((?:"+us+"\\:)?"+us+")"),fs=/^\s*(\/?)>/,ps=new RegExp("^<\\/((?:"+us+"\\:)?"+us+")[^>]*>"),ds=/^]+>/i,vs=/^