diff --git a/ChangeLog.md b/ChangeLog.md index 9bd931ae..9f1ea2d8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,7 +1,27 @@ # Changelog: + * **XX.XX.XX** - Removed icon size restriction for SVGs - Fixed permission editor icon select for not granted icon permissions + - Fixed "disconnect" button not showing up after beeing connected + - Improved handling of `disableMultiSession` settings (Connect in a new tab does not show up anymore) + - Implemented avatar upload + - Sorting server group icons within client channel tree + - Fixed buggy away message position + - Logging the servers welcome message [#54](https://github.com/TeaSpeak/TeaWeb/issues/54) + - Showing servers hostbutton + - Fixed microphone and sound action sounds [#67](https://github.com/TeaSpeak/TeaWeb/issues/67) + - Added option to mute clients [#64](https://github.com/TeaSpeak/TeaWeb/issues/64) + - Improved debug loader (no dependency faults anymore) + - Saving private conversations and showing the messages again after client restart + - Fixed some general memory leaks + - Implemented the hostmessage functions + - Fixed bookmark server password + + Big UI Improvement: + - New "dark theme" design + - All elements are responsive to the font-size (Supporting now large & small screens (No mobile support yet)) + - Implemented an active ping calculation * **22.06.19** - Fixed channel create not working issue diff --git a/client/css/static/main.scss b/client/css/static/main.scss index 2ab1d888..47f6a545 100644 --- a/client/css/static/main.scss +++ b/client/css/static/main.scss @@ -22,4 +22,8 @@ html, body { display: flex; flex-direction: column; resize: both; } +} + +footer { + display: none!important; } \ No newline at end of file diff --git a/client/js/.keepalive b/client/js/.keepalive new file mode 100644 index 00000000..e69de29b diff --git a/client/js/teaforo.ts b/client/js/teaforo.ts deleted file mode 100644 index e7ea53aa..00000000 --- a/client/js/teaforo.ts +++ /dev/null @@ -1,33 +0,0 @@ -namespace forum { - const ipc = require("electron").ipcRenderer; - let callback_listener: (() => any)[] = []; - - ipc.on('teaforo-update', (event, data) => { - console.log("Got data update: %o", data); - profiles.identities.set_static_identity(data ? new profiles.identities.TeaForumIdentity(data.application_data, data.application_data_sign) : undefined); - try { - for(let listener of callback_listener) - setImmediate(listener); - } catch(e) { - console.log(e); - } - - callback_listener = []; - }); - - export function register_callback(callback: () => any) { - callback_listener.push(callback); - } - - export function open() { - ipc.send("teaforo-login"); - } - - export function logout() { - ipc.send("teaforo-logout"); - } - - export function sync_main() { - ipc.send("teaforo-update"); - } -} \ No newline at end of file diff --git a/files.php b/files.php index 8a4989db..92983355 100644 --- a/files.php +++ b/files.php @@ -34,7 +34,7 @@ "path" => "js/", "local-path" => "./shared/js/", - "req-parm" => ["-js-map"] + "req-parm" => ["--mappings"] ], [ /* shared generated worker codec */ "type" => "js", @@ -52,6 +52,15 @@ "path" => "css/", "local-path" => "./shared/css/" ], + [ /* shared css mapping files (development mode only) */ + "type" => "css", + "search-pattern" => "/.*\.(css.map|scss)$/", + "build-target" => "dev", + + "path" => "css/", + "local-path" => "./shared/css/", + "req-parm" => ["--mappings"] + ], [ /* shared release css files */ "type" => "css", "search-pattern" => "/.*\.css$/", @@ -137,7 +146,7 @@ $APP_FILE_LIST_SHARED_VENDORS = [ [ "type" => "js", - "search-pattern" => "/.*\.js$/", + "search-pattern" => "/.*(\.min)?\.js$/", "build-target" => "dev|rel", "path" => "vendor/", diff --git a/shared/audio/speech/mapping.json b/shared/audio/speech/mapping.json index 9c37451f..2756d403 100644 --- a/shared/audio/speech/mapping.json +++ b/shared/audio/speech/mapping.json @@ -1 +1 @@ -[{"file": "sound.test.wav", "key": "sound.test"}, {"file": "sound.egg.wav", "key": "sound.egg"}, {"file": "away_activated.wav", "key": "away_activated"}, {"file": "away_deactivated.wav", "key": "away_deactivated"}, {"file": "connection.connected.wav", "key": "connection.connected"}, {"file": "connection.disconnected.wav", "key": "connection.disconnected"}, {"file": "connection.disconnected.timeout.wav", "key": "connection.disconnected.timeout"}, {"file": "connection.refused.wav", "key": "connection.refused"}, {"file": "connection.banned.wav", "key": "connection.banned"}, {"file": "server.edited.wav", "key": "server.edited"}, {"file": "server.edited.self.wav", "key": "server.edited.self"}, {"file": "server.kicked.wav", "key": "server.kicked"}, {"file": "channel.kicked.wav", "key": "channel.kicked"}, {"file": "channel.moved.wav", "key": "channel.moved"}, {"file": "channel.joined.wav", "key": "channel.joined"}, {"file": "channel.created.wav", "key": "channel.created"}, {"file": "channel.edited.wav", "key": "channel.edited"}, {"file": "channel.edited.self.wav", "key": "channel.edited.self"}, {"file": "channel.deleted.wav", "key": "channel.deleted"}, {"file": "user.moved.wav", "key": "user.moved"}, {"file": "user.moved.self.wav", "key": "user.moved.self"}, {"file": "user.poked.self.wav", "key": "user.poked.self"}, {"file": "user.banned.wav", "key": "user.banned"}, {"file": "user.joined.wav", "key": "user.joined"}, {"file": "user.joined.moved.wav", "key": "user.joined.moved"}, {"file": "user.joined.kicked.wav", "key": "user.joined.kicked"}, {"file": "user.joined.connect.wav", "key": "user.joined.connect"}, {"file": "user.left.wav", "key": "user.left"}, {"file": "user.left.kicked.channel.wav", "key": "user.left.kicked.channel"}, {"file": "user.left.kicked.server.wav", "key": "user.left.kicked.server"}, {"file": "user.left.moved.wav", "key": "user.left.moved"}, {"file": "user.left.disconnect.wav", "key": "user.left.disconnect"}, {"file": "user.left.banned.wav", "key": "user.left.banned"}, {"file": "error.insufficient_permissions.wav", "key": "error.insufficient_permissions"}, {"file": "group.server.assigned.wav", "key": "group.server.assigned"}, {"file": "group.server.revoked.wav", "key": "group.server.revoked"}, {"file": "group.channel.changed.wav", "key": "group.channel.changed"}, {"file": "group.server.assigned.self.wav", "key": "group.server.assigned.self"}, {"file": "group.server.revoked.self.wav", "key": "group.server.revoked.self"}, {"file": "group.channel.changed.self.wav", "key": "group.channel.changed.self"}] \ No newline at end of file +[{"file": "sound.test.wav", "key": "sound.test"}, {"file": "sound.egg.wav", "key": "sound.egg"}, {"file": "away_activated.wav", "key": "away_activated"}, {"file": "away_deactivated.wav", "key": "away_deactivated"}, {"file": "connection.connected.wav", "key": "connection.connected"}, {"file": "connection.disconnected.wav", "key": "connection.disconnected"}, {"file": "connection.disconnected.timeout.wav", "key": "connection.disconnected.timeout"}, {"file": "connection.refused.wav", "key": "connection.refused"}, {"file": "connection.banned.wav", "key": "connection.banned"}, {"file": "server.edited.wav", "key": "server.edited"}, {"file": "server.edited.self.wav", "key": "server.edited.self"}, {"file": "server.kicked.wav", "key": "server.kicked"}, {"file": "channel.kicked.wav", "key": "channel.kicked"}, {"file": "channel.moved.wav", "key": "channel.moved"}, {"file": "channel.joined.wav", "key": "channel.joined"}, {"file": "channel.created.wav", "key": "channel.created"}, {"file": "channel.edited.wav", "key": "channel.edited"}, {"file": "channel.edited.self.wav", "key": "channel.edited.self"}, {"file": "channel.deleted.wav", "key": "channel.deleted"}, {"file": "user.moved.wav", "key": "user.moved"}, {"file": "user.moved.self.wav", "key": "user.moved.self"}, {"file": "user.poked.self.wav", "key": "user.poked.self"}, {"file": "user.banned.wav", "key": "user.banned"}, {"file": "user.joined.wav", "key": "user.joined"}, {"file": "user.joined.moved.wav", "key": "user.joined.moved"}, {"file": "user.joined.kicked.wav", "key": "user.joined.kicked"}, {"file": "user.joined.connect.wav", "key": "user.joined.connect"}, {"file": "user.left.wav", "key": "user.left"}, {"file": "user.left.kicked.channel.wav", "key": "user.left.kicked.channel"}, {"file": "user.left.kicked.server.wav", "key": "user.left.kicked.server"}, {"file": "user.left.moved.wav", "key": "user.left.moved"}, {"file": "user.left.disconnect.wav", "key": "user.left.disconnect"}, {"file": "user.left.banned.wav", "key": "user.left.banned"}, {"file": "error.insufficient_permissions.wav", "key": "error.insufficient_permissions"}, {"file": "group.server.assigned.wav", "key": "group.server.assigned"}, {"file": "group.server.revoked.wav", "key": "group.server.revoked"}, {"file": "group.channel.changed.wav", "key": "group.channel.changed"}, {"file": "group.server.assigned.self.wav", "key": "group.server.assigned.self"}, {"file": "group.server.revoked.self.wav", "key": "group.server.revoked.self"}, {"file": "group.channel.changed.self.wav", "key": "group.channel.changed.self"}, {"key": "microphone.muted", "file": "microphone.muted.wav"}, {"key": "microphone.activated", "file": "microphone.activated.wav"}, {"key": "sound.muted", "file": "sound.muted.wav"}, {"key": "sound.activated", "file": "sound.activated.wav"}, {"file": "user.left.timeout.wav", "key": "user.left.timeout"}] \ No newline at end of file diff --git a/shared/audio/speech/microphone.activated.wav b/shared/audio/speech/microphone.activated.wav new file mode 100644 index 00000000..c80fe4cb Binary files /dev/null and b/shared/audio/speech/microphone.activated.wav differ diff --git a/shared/audio/speech/microphone.muted.wav b/shared/audio/speech/microphone.muted.wav new file mode 100644 index 00000000..7492e98f Binary files /dev/null and b/shared/audio/speech/microphone.muted.wav differ diff --git a/shared/audio/speech/sound.activated.wav b/shared/audio/speech/sound.activated.wav new file mode 100644 index 00000000..fc4926ca Binary files /dev/null and b/shared/audio/speech/sound.activated.wav differ diff --git a/shared/audio/speech/sound.muted.wav b/shared/audio/speech/sound.muted.wav new file mode 100644 index 00000000..931c5ade Binary files /dev/null and b/shared/audio/speech/sound.muted.wav differ diff --git a/shared/audio/speech/user.left.timeout.wav b/shared/audio/speech/user.left.timeout.wav new file mode 100644 index 00000000..6cca26e0 Binary files /dev/null and b/shared/audio/speech/user.left.timeout.wav differ diff --git a/shared/audio/speech_sentences.csv b/shared/audio/speech_sentences.csv index b831093c..31353999 100644 --- a/shared/audio/speech_sentences.csv +++ b/shared/audio/speech_sentences.csv @@ -6,6 +6,14 @@ sound.egg;WolverinDEV is the best and I love TeaSpeak! away_activated;See you soon away_deactivated;Welcome back +#Microphone +microphone.muted;Microphone muted +microphone.activated;Microphone activated + +#Sound +sound.muted;Sound muted +sound.activated;Sound activated + #Connection connection.connected;Connected connection.disconnected;Disconnected @@ -44,6 +52,7 @@ user.left.kicked.server;User in your channel got kicked from the server user.left.moved;User was moved out of your channel user.left.disconnect;User disconnected from your channel user.left.banned;User in your channel was banned from the server +user.left.timeout;User in your channel timed out #Error error.insufficient_permissions;insufficient permissions diff --git a/shared/backend/connection.d.ts b/shared/backend/connection.d.ts index e48cd0f2..e816b7df 100644 --- a/shared/backend/connection.d.ts +++ b/shared/backend/connection.d.ts @@ -1,3 +1,4 @@ declare namespace connection { export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection; + export function destroy_server_connection(handle: AbstractServerConnection); } \ No newline at end of file diff --git a/shared/css/static/channel-tree.scss b/shared/css/static/channel-tree.scss index 9623c0b9..f5767091 100644 --- a/shared/css/static/channel-tree.scss +++ b/shared/css/static/channel-tree.scss @@ -1,4 +1,15 @@ @import "properties"; +@import "mixin"; + +.channel-tree-container { + height: 100%; + + flex-grow: 1; + flex-shrink: 1; + + overflow: hidden; + overflow-y: auto; +} /* the channel tree */ .channel-tree { @@ -71,6 +82,11 @@ align-self: center; color: $channel_tree_entry_text_color; + + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .icon_property { @@ -84,6 +100,8 @@ flex-direction: column; .container-channel { + position: relative; + display: flex; flex-direction: row; justify-content: stretch; @@ -94,6 +112,39 @@ align-items: center; cursor: pointer; + .marker-text-unread { + position: absolute; + left: 0; + top: 0; + bottom: 0; + + width: 1px; + background-color: #a814147F; + + opacity: 1; + + &:before { + content: ''; + position: absolute; + + left: 0; + top: 0; + bottom: 0; + + width: 24px; + + background: -moz-linear-gradient(left, rgba(168,20,20,.18) 0%, rgba(168,20,20,0) 100%); /* FF3.6-15 */ + background: -webkit-linear-gradient(left, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* Chrome10-25,Safari5.1-6 */ + background: linear-gradient(to right, rgba(168,20,20,.18) 0%,rgba(168,20,20,0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ + } + + &.hidden { + opacity: 0; + } + + @include transition(opacity $button_hover_animation_time); + } + .channel-type { flex-grow: 0; flex-shrink: 0; @@ -125,6 +176,17 @@ .channel-name { align-self: center; color: $channel_tree_entry_text_color; + + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &.align-repetitive { + .channel-name { + text-overflow: clip; + } } } @@ -174,16 +236,20 @@ } .client-name { + line-height: 16px; + + flex-grow: 0; + flex-shrink: 1; + + padding-right: .25em; + color: $channel_tree_entry_text_color; + &.client-name-own { font-weight: bold; } + } - line-height: 16px; - - flex-grow: 1; - flex-shrink: 1; - - min-width: 75px; + .client-away-message { color: $channel_tree_entry_text_color; } @@ -191,7 +257,8 @@ margin-right: 0; /* override from previous thing */ position: absolute; - right: 5px; + right: 0; + padding-right: 5px; display: flex; flex-direction: row; @@ -211,14 +278,30 @@ } &.selected { + &:focus-within { + .container-icons { + background-color: $channel_tree_entry_selected; + padding-left: 5px; + z-index: 1001; /* show before client name */ + + height: 18px; + } + } + .client-name { &:focus { + position: absolute; color: black; padding-top: 1px; padding-bottom: 1px; z-index: 1000; + + margin-right: -10px; + margin-left: 18px; + + width: 100%; } } } diff --git a/shared/css/static/connection_handlers.scss b/shared/css/static/connection_handlers.scss index 409f99ca..0995b588 100644 --- a/shared/css/static/connection_handlers.scss +++ b/shared/css/static/connection_handlers.scss @@ -1,3 +1,5 @@ +@import "mixin"; + .container-connection-handlers { $animation_length: .25s; @@ -14,10 +16,7 @@ background-color: transparent; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + @include user-select(none); position: relative; @@ -32,6 +31,8 @@ overflow-x: auto; overflow-y: visible; + max-width: 100%; + .connection-container { padding-top: 4px; position: relative; @@ -57,25 +58,13 @@ color: #a8a8a8; align-self: center; - margin-right: -5px; /* 5px padding which have to be overcommed */ + margin-right: 20px; position: relative; - max-width: 16em; - overflow: visible; text-overflow: clip; white-space: nowrap; - - &:before { - content: ''; - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - background: linear-gradient(to right, transparent, #1e1e1e calc(100% - 20px)); - } } .button-close { @@ -89,6 +78,23 @@ } } + &.cutoff-name { + .server-name { + max-width: 10em; + margin-right: -5px; /* 5px padding which have to be overcommed */ + + &:before { + content: ''; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + background: linear-gradient(to right, transparent, #1e1e1e calc(100% - 20px)); + } + } + } + &:hover { background-color: #242425; } diff --git a/shared/css/static/context_menu.scss b/shared/css/static/context_menu.scss index 33ea6153..8a00ff87 100644 --- a/shared/css/static/context_menu.scss +++ b/shared/css/static/context_menu.scss @@ -86,65 +86,66 @@ position: absolute; margin-left: 3px; } + } +} - .checkbox { - margin-top: 1px; - margin-left: 1px; - display: block; - position: relative; - padding-left: 14px; - margin-bottom: 12px; - cursor: pointer; - font-size: 22px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; +/* we call it "ccheckbox" else it will be messed up the the global checkbox */ +.ccheckbox { + margin-top: 1px; + margin-left: 1px; + display: block; + position: relative; + padding-left: 14px; + margin-bottom: 12px; + cursor: pointer; + font-size: 22px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; - /* Hide the browser's default checkbox */ - input { - position: absolute; - opacity: 0; - cursor: pointer; - display: none; - } + /* Hide the browser's default checkbox */ + input { + position: absolute; + opacity: 0; + cursor: pointer; + display: none; + } - .checkmark { - position: absolute; - top: 0; - left: 0; - height: 11px; - width: 11px; - background-color: #eee; + .checkmark { + position: absolute; + top: 0; + left: 0; + height: 11px; + width: 11px; + background-color: #eee; - &:after { - content: ""; - position: absolute; - display: none; + &:after { + content: ""; + position: absolute; + display: none; - left: 4px; - top: 1px; - width: 3px; - height: 7px; - border: solid white; - border-width: 0 2px 2px 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - } - } - - &:hover input ~ .checkmark { - background-color: #ccc; - } - - input:checked ~ .checkmark { - background-color: #2196F3; - } - - input:checked ~ .checkmark:after { - display: block; - } + left: 4px; + top: 1px; + width: 3px; + height: 7px; + border: solid white; + border-width: 0 2px 2px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); } } + + &:hover input ~ .checkmark { + background-color: #ccc; + } + + input:checked ~ .checkmark { + background-color: #2196F3; + } + + input:checked ~ .checkmark:after { + display: block; + } } \ No newline at end of file diff --git a/shared/css/static/control_bar.scss b/shared/css/static/control_bar.scss index 9a005fbb..fbaca182 100644 --- a/shared/css/static/control_bar.scss +++ b/shared/css/static/control_bar.scss @@ -1,13 +1,14 @@ +@import "properties"; +@import "mixin"; + $border_color_activated: rgba(255, 255, 255, .75); +/* max height is 2em */ .control_bar { display: flex; flex-direction: row; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + @include user-select(none); height: 100%; align-items: center; @@ -25,8 +26,8 @@ $border_color_activated: rgba(255, 255, 255, .75); .button, .dropdown-arrow { text-align: center; - border: 1px solid rgba(0, 0, 0, 0); - border-radius: 3px; + border: .05em solid rgba(0, 0, 0, 0); + border-radius: $border_radius_small; background-color: #454545; @@ -56,6 +57,8 @@ $border_color_activated: rgba(255, 255, 255, .75); } } + @include transition(background-color $button_hover_animation_time ease-in-out, border-color $button_hover_animation_time ease-in-out); + > .icon_x24 { vertical-align: middle; } @@ -69,9 +72,25 @@ $border_color_activated: rgba(255, 255, 255, .75); margin-left: 5px; &:not(.icon_x24) { - min-width: 28px; - max-width: 28px; - height: 28px; + min-width: 2em; + max-width: 2em; + height: 2em; + } + + .icon_em { + vertical-align: text-top; + } + + &.button-hostbutton { + img { + min-width: 1.5em; + max-width: 1.5em; + + height: 1.5em; + width: 1.5em; + } + + padding: .25em; } } @@ -80,8 +99,7 @@ $border_color_activated: rgba(255, 255, 255, .75); position: relative; .buttons { - margin-top: 1px; - height: 28px; + height: 2em; align-items: center; @@ -89,14 +107,14 @@ $border_color_activated: rgba(255, 255, 255, .75); flex-direction: row; .dropdown-arrow { - height: 28px; + height: 2em; display: inline-flex; justify-content: space-around; - width: 18px; + width: 1.5em; cursor: pointer; - border-radius: 0 3px 3px 0; + border-radius: 0 $border_radius_small $border_radius_small 0; align-items: center; border-left: 0; } @@ -131,8 +149,8 @@ $border_color_activated: rgba(255, 255, 255, .75); background-color: #2d3032; align-items: center; - border: 1px solid #2c2525; - border-radius: 0 5px 5px 5px; + border: .05em solid #2c2525; + border-radius: 0 $border_radius_middle $border_radius_middle $border_radius_middle; width: 230px; @@ -143,7 +161,7 @@ $border_color_activated: rgba(255, 255, 255, .75); right: 0; } - .icon { + .icon, .icon-container, .icon_em { vertical-align: middle; margin-right: 5px; } @@ -159,16 +177,16 @@ $border_color_activated: rgba(255, 255, 255, .75); } & > div:first-of-type { - border-radius: 2px 2px 0 0; + border-radius: .1em .1em 0 0; } & > div:last-of-type { - border-radius: 0 0 2px 2px; + border-radius: 0 0 .1em .1em; } &.display_left { margin-left: -179px; - border-radius: 5px 0 5px 5px; + border-radius: $border_radius_middle 0 $border_radius_middle $border_radius_middle; } } @@ -263,4 +281,8 @@ $border_color_activated: rgba(255, 255, 255, .75); } } } + + .icon_em { + font-size: 1.5em; + } } \ No newline at end of file diff --git a/shared/css/static/frame-chat.scss b/shared/css/static/frame-chat.scss index c1b3bc67..f91e468a 100644 --- a/shared/css/static/frame-chat.scss +++ b/shared/css/static/frame-chat.scss @@ -1,3 +1,10 @@ +@import "./mixin"; +@import "./properties"; + +//$color_client_normal: #bebebe; +$color_client_normal: #cccccc; +$client_info_avatar_size: 10em; + .container-chat-frame { flex-grow: 1; flex-shrink: 1; @@ -6,6 +13,8 @@ flex-direction: column; justify-content: stretch; + min-height: 200px; + .container-info { user-select: none; @@ -78,7 +87,7 @@ color: #dab8b4; font-size: .66em; - height: .9em; + height: 1.3em; min-width: .9em; padding-right: 2px; @@ -92,17 +101,40 @@ display: inline-block; - border-radius: 3px; - padding-right: 5px; - padding-left: 5px; + border-radius: .18em; + padding-right: .31em; + padding-left: .31em; > div { display: inline-block; } - .icon-container { - margin-right: 5px; + .icon-container, .icon { vertical-align: middle; + margin-right: .25em; + } + + &.value-ping { + //very-good good medium poor very-poor + &.very-good { + color: #3f7538; + } + &.good { + color: #365632; + } + &.medium { + color: #777f2c; + } + &.poor { + color: #7f5122; + } + &.very-poor { + color: #7f2222; + } + } + + &.chat-counter { + cursor: pointer; } } @@ -113,11 +145,40 @@ vertical-align: top; margin-top: -.2em; } + + .button { + color: #5a5a5a; + background-color: #373737; + + display: inline-block; + + &:not(.value) { + border-radius: .18em; + padding-right: .31em; + padding-left: .31em; + + margin-top: 1.5em; /* because we've no title */ + } + + cursor: pointer; + + &:hover { + background-color: #4e4e4e; /* TODO: Evaluate color */ + } + @include transition(background-color $button_hover_animation_time ease-in-out); + } + + &.mode-client_info { + min-width: 50%; + margin-right: calc(#{$client_info_avatar_size} / 2); + } } } } .container-chat { + width: 100%; + flex-grow: 1; flex-shrink: 1; @@ -125,9 +186,14 @@ border-bottom-right-radius: 5px; min-width: 350px; - min-height: 200px; + min-height: 16em; + + display: flex; + flex-direction: column; .container-private-conversations { + height: 100%; + flex-grow: 1; flex-shrink: 1; @@ -164,8 +230,9 @@ } } - //Avatar within cat 40x40 .conversation-entry { + position: relative; + display: flex; flex-direction: row; justify-content: stretch; @@ -185,8 +252,8 @@ .avatar { overflow: hidden; - width: 30px; - height: 30px; + width: 2em; + height: 2em; border-radius: 50%; } @@ -237,7 +304,7 @@ } .client-name { - color: #bebebe; + color: $color_client_normal; font-weight: bold; margin-bottom: -.4em; @@ -251,6 +318,42 @@ } } + .button-close { + font-size: 2em; + + position: absolute; + right: 0; + top: 0; + bottom: 0; + + opacity: 0.3; + + width: .5em; + height: .5em; + + &:hover { + opacity: 1; + } + @include transition(opacity $button_hover_animation_time ease-in-out); + + &:before, &:after { + position: absolute; + left: .25em; + content: ' '; + height: .5em; + width: .05em; + background-color: #5a5a5a; + } + + &:before { + transform: rotate(45deg); + } + + &:after { + transform: rotate(-45deg); + } + } + &:hover { background-color: #393939; } @@ -258,6 +361,7 @@ &.selected { background-color: #2c2c2c; } + @include transition(background-color $button_hover_animation_time ease-in-out); } } @@ -269,16 +373,134 @@ flex-direction: column; justify-content: stretch; - .messages { + .spacer { + min-height: 0; flex-grow: 1; flex-shrink: 1; + } + + .messages { + @include user-select(none); + + display: block; + position: relative; + + flex-grow: 0; + flex-shrink: 1; min-height: 100px; + + overflow-y: auto; + overflow-x: hidden; + + .message-entry { + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-avatar { + flex-grow: 0; + flex-shrink: 0; + + position: relative; + + display: inline-block; + margin: .5em 1em .5em .5em; + + .avatar { + overflow: hidden; + + width: 2.5em; + height: 2.5em; + + border-radius: 50%; + } + } + + .container-message { + flex-grow: 1; + flex-shrink: 1; + + min-width: 2em; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + + .info { + display: block; + + white-space : nowrap; + overflow : hidden; + text-overflow: ellipsis; + + margin-top: .5em; + + .client-name, .client-name .htmltag-client { + display: inline; + + font-weight: bold; + color: $color_client_normal; + } + + .timestamp { + display: inline; + + font-size: 0.66em; + color: #5d5b5b; + } + } + + .message { + color: #b5b5b5; + line-height: 1.1em; + word-wrap: break-word; + } + } + } + + .spacer-entry { + text-align: center; + //padding: .5em 1em; + color: #565353; + + &.type-new { + color: darkred; /* TODO: Evaluate color */ + } + + &.type-disconnect { + color: red; /* TODO: Evaluate color */ + } + + &.type-reconnect { + color: green; /* TODO: Evaluate color */ + } + + &.type-closed { + color: yellow; /* TODO: Evaluate color */ + } + + &.type-date { + color: #565353; + background-color: #353535; + + z-index: 2; /* over the avatar */ + position: sticky; + top: 0; + } + } + + @include chat-scrollbar(); } .chatbox { flex-grow: 0; - flex-shrink: 1; + flex-shrink: 0; display: flex; justify-content: stretch; @@ -298,14 +520,12 @@ } .container-chatbox { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - + @include user-select(none); + flex-grow: 1; - flex-shrink: 1; + flex-shrink: 0; min-height: calc(1.5em + 10px); + height: min-content; display: flex; flex-direction: row; @@ -327,11 +547,17 @@ cursor: pointer; + display: flex; + flex-direction: row; + justify-content: space-around; + &:hover { background-color: #393939; } + @include transition(background-color $button_hover_animation_time ease-in-out); .container-icon { + position: absolute; display: flex; width: calc(1.5em - 4px); @@ -342,6 +568,40 @@ width: 100%; } } + + .lsx-emojipicker-appender { + position: relative; + + width: calc(1.5em - 4px); + height: calc(1.5em - 4px); + + .lsx-emojipicker-container { + &:after { + right: calc(0.75em + 2px) !important + } + + .lsx-emojipicker-tabs { + height: 38px; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + li { + height: 36px; + width: 36px; + + display: flex; + flex-direction: column; + justify-content: space-around; + + img { + margin: 0; + } + } + } + } + } } } @@ -353,24 +613,25 @@ width: 100%; background-color: #464646; + border: 2px none #353535; /* background color (like no border) */ + padding: 0 5px; overflow: hidden; border-radius: 5px; - textarea { - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; + .textarea { + @include user-select(text); width: 100%; - resize: vertical; + resize: none; min-height: 1.5em; max-height: 6em; height: 1.5em; + overflow: auto; + background-color: transparent; padding: 0; margin: 0; @@ -378,32 +639,621 @@ outline: none; color: #a9a9a9; + + @include chat-scrollbar-vertical(); + } - textarea::-webkit-input-placeholder { + @include placeholder(textarea) { color: #363535; font-style: oblique; } - textarea:-moz-placeholder { - color: #363535; - font-style: oblique; + + &:hover { + margin: -2px; + border: 2px solid #474747; } - textarea::-moz-placeholder { - color: #363535; - font-style: oblique; + + &:focus-within { + margin: -2px; + border: 2px solid #585858; } - textarea:-ms-input-placeholder { - color: #363535; - font-style: oblique; + + @include transition(border-color $button_hover_animation_time ease-in-out); + } + } + + .container-channel-chat { + width: 100%; + + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + /* 1.5em + 10px := chatbox; 2em chat */ + min-height: calc(1.5em + 10px + 2em); + + > .spacer { + min-height: 0; + flex-grow: 1; + flex-shrink: 1; + } + + > .container-chat { + @include user-select(none); + + padding: 5px; + + display: flex; + flex-direction: column; + justify-content: stretch; + + position: relative; + + flex-grow: 1; + flex-shrink: 1; + + width: 100%; + min-height: 2em; + + .container-channel-chat-messages { + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + min-height: 2em; } - textarea::-ms-input-placeholder { - color: #363535; - font-style: oblique; + + .container-messages { + flex-grow: 1; + flex-shrink: 1; + + display: block; + + position: relative; + + overflow-y: auto; + overflow-x: hidden; + + min-height: 2em; + + .message-entry { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-avatar { + flex-grow: 0; + flex-shrink: 0; + + position: relative; + + display: inline-block; + margin: 1em 1em .5em .5em; + + .avatar { + overflow: hidden; + + width: 2.5em; + height: 2.5em; + + border-radius: 50%; + } + } + + .container-message { + flex-grow: 0; + flex-shrink: 1; + + min-width: 2em; + + position: relative; + + display: inline-flex; + flex-direction: column; + justify-content: flex-start; + + @include user-select(text); + + background: #303030; + border-radius: 6px 6px 6px 6px; + + margin-top: .5em; + padding: .5em; + + .info { + display: block; + + white-space : nowrap; + overflow : hidden; + text-overflow: ellipsis; + + .client-name, .client-name .htmltag-client { + display: inline; + + font-weight: bold; + color: $color_client_normal; + } + + .timestamp { + display: inline; + + font-size: 0.66em; + color: #5d5b5b; + } + + .button-delete { + width: 1.1em; + cursor: pointer; + + display: inline-block; + align-self: auto; + + opacity: .25; + + > img { + vertical-align: text-top; + } + + &:hover { + opacity: 1; + } + + @include transform(opacity $button_hover_animation_time ease-in-out); + } + } + + .message { + color: #b5b5b5; + line-height: 1.1em; + + word-wrap: break-word; + } + + &:before { + position: absolute; + + content: ' '; + + width: 1em; + height: 1em; + + margin-left: calc(-.5em - 1em); + border-top: .5em solid transparent; + border-right: .75em solid #303030; + border-bottom: .5em solid transparent; + + top: 1.25em; + } + } + } + + .spacer-entry { + text-align: center; + //padding: .5em 1em; + color: #565353; + + &.type-new { + color: darkred; /* TODO: Evaluate color */ + } + + &.type-old { + opacity: 0; + cursor: pointer; + pointer-events: none; + + height: 0; + @include transition(opacity .25s ease-in-out); + &.shown { + height: unset; + + cursor: pointer; + pointer-events: all; + + opacity: 1; + @include transition(opacity .25s ease-in-out); + } + } + } + + @include chat-scrollbar(); } - textarea::placeholder { - color: #363535; - font-style: oblique; + + .new-message { + flex-grow: 0; + flex-shrink: 0; + + position: absolute; + bottom: 0; + right: 0; + left: 0; + + display: block; + text-align: center; + + width: 100%; + color: #8b8b8b; + + background: #353535; /* if we dont support gradients */ + background: linear-gradient(#35353500 10%, #353535 70%); + pointer-events: none; + + opacity: 0; + @include transition(opacity .25s ease-in-out); + &.shown { + cursor: pointer; + pointer-events: all; + + opacity: 1; + @include transition(opacity .25s ease-in-out); + } } + + .no-permissions, .private-conversation { + flex-grow: 0; + flex-shrink: 0; + + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + + text-align: center; + + width: 100%; + color: #5a5a5a; + background: #353535; + + + display: flex; + flex-direction: column; + justify-content: center; + + > div { + display: inline-block; + } + } + } + + > .container-chatbox { + flex-grow: 0; + flex-shrink: 0; + + min-height: 2em; + } + } + + .container-client-info { + position: relative; + + height: 100%; + + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + padding-right: 5px; + padding-left: 5px; + + .heading { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .container-avatar { + flex-grow: 0; + flex-shrink: 0; + + position: relative; + + display: inline-block; + margin: calc(#{$client_info_avatar_size} / -2) .75em .5em .5em; + + align-self: center; + + .avatar { + overflow: hidden; + + width: $client_info_avatar_size; + height: $client_info_avatar_size; + + border-radius: 50%; + } + } + + .container-avatar-edit { + position: absolute; + + display: inline-block; + + left: 0; + right: 0; + top: 0; + bottom: 0; + + z-index: 2; + + text-align: center; + + > img { + cursor: pointer; + + width: $client_info_avatar_size; + height: $client_info_avatar_size; + + padding: calc(#{$client_info_avatar_size} / 6); + + overflow: hidden; + opacity: 0; + + &:hover { + opacity: .75; + } + @include transition(opacity $button_hover_animation_time ease-in-out); + } + } + + .client-name { + display: flex; + flex-direction: row; + justify-content: center; + + .htmltag-client { + text-align: center; + font-size: 1.5em; + color: $color_client_normal; + font-weight: bold; + } + } + + .container-description { + padding-right: calc(#{$client_info_avatar_size} / 2); + padding-left: calc(#{$client_info_avatar_size} / 2); + text-align: center; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .client-description { + color: #6f6f6f; + + max-width: 100%; + flex-shrink: 1; + flex-grow: 1; + overflow-wrap: break-word; + } + } + } + + .general-info { + padding-top: 1em; + + overflow-x: hidden; + overflow-y: auto; + + display: flex; + flex-direction: row; + justify-content: stretch; + + flex-grow: 1; + flex-shrink: 1; + + .block { + display: inline-block; + height: 100%; + + flex-grow: 1; + flex-shrink: 1; + + min-width: 6em; + + &.block-right { + text-align: right; + + .container-property { + flex-direction: row-reverse; + + .icon_em { + margin-left: .2em; + } + + .value { + justify-content: flex-end; + } + } + } + &.block-left { + text-align: left; + + .container-property { + .icon_em { + margin-right: .2em; + } + + .value { + justify-content: flex-start; + } + } + } + + .container-property { + display: flex; + flex-direction: row; + justify-content: stretch; + + > .icon_em { + margin-top: .1em; + margin-bottom: .1em; + + font-size: 2em; + + flex-shrink: 0; + flex-grow: 0; + } + + &.list { + > .icon_em { + margin-top: 0; /* for lists the .1em patting on the top looks odd */ + } + } + + .property { + line-height: 1.1em; + + flex-shrink: 1; + flex-grow: 1; + + min-width: 4em; /* 2em for the icon the last 4 for the text */ + + display: flex; + flex-direction: column; + justify-content: flex-start; + + .title, .value { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + .title { + color: #636363; + font-weight: bold; + text-transform: uppercase; + } + + .value { + color: #d9d9d9; + + display: flex; + flex-direction: row; + + .country { + margin-right: .2em; + align-self: center; + } + + .group-container { + display: flex; + + justify-content: flex-start; + flex-direction: row-reverse; + + .icon-container, .icon_empty, .icon { + margin-left: .5em; + align-self: center; + + & > img { + top: 0; + left: 0; + right: 0; + bottom: 0; + } + } + } + + .status-entry { + > .icon_em { + vertical-align: middle; + } + + .away-message { + margin-left: .25em; + } + } + + &.client-teaforo-account { + a, a:visited { + color: #d9d9d9; + } + } + } + } + + &.list { + .property { + .value { + flex-direction: column; + } + } + } + + &:not(first-of-type) { + margin-top: 1em; + } + } + } + } + + + + .button-close { + font-size: 4em; + + cursor: pointer; + + position: absolute; + right: 0; + top: 0; + bottom: 0; + + opacity: 0.3; + + width: .5em; + height: .5em; + + margin-right: .1em; + margin-top: .1em; + + &:hover { + opacity: 1; + } + @include transition(opacity $button_hover_animation_time ease-in-out); + + &:before, &:after { + position: absolute; + left: .25em; + content: ' '; + height: .5em; + width: .05em; + background-color: #5a5a5a; + } + + &:before { + transform: rotate(45deg); + } + + &:after { + transform: rotate(-45deg); + } + } + } + + .container-private-conversations, .container-channel-chat { + .container-message .emoji { + height: 1em; + width: 1em; + + margin-left: .1em; + margin-right: .1em; + + vertical-align: text-top; } } } diff --git a/shared/css/static/frame/SelectInfo.scss b/shared/css/static/frame/SelectInfo.scss index a5481c4c..3c0f068d 100644 --- a/shared/css/static/frame/SelectInfo.scss +++ b/shared/css/static/frame/SelectInfo.scss @@ -41,10 +41,6 @@ flex-direction: column; padding-right: 0; padding-left: 0; - - .hostbanner { - overflow: hidden; - } } } @@ -84,42 +80,6 @@ display: none; margin-bottom: 5px; } - - .hostbanner { - position: relative; - flex-grow: 1; - flex-shrink: 1; - - .meta-image { - display: none; - } - - - .image-container { - display: flex; - flex-direction: row; - justify-content: center; - height: 100%; - - div { - background-position: center; - - &.hostbanner-mode-0 { } - - &.hostbanner-mode-1 { - width: 100%; - height: auto; - } - - &.hostbanner-mode-2 { - background-size: cover!important; - background-position: top center !important; - width:100%; - height:100% - } - } - } - } } .container-select-info { diff --git a/shared/css/static/general.scss b/shared/css/static/general.scss index f51559c5..feae51d2 100644 --- a/shared/css/static/general.scss +++ b/shared/css/static/general.scss @@ -1,24 +1,11 @@ -*, -*::before, -*::after { - box-sizing: border-box; -} - -.align_row { - display: flex; - flex-direction: row; -} - -.align_column { - display: flex; - flex-direction: column; -} +@import "mixin"; +/* Avatar/Icon loading animations */ .icon_loading { border: 2px solid #f3f3f3; /* Light grey */ border-top: 2px solid #3498db; /* Blue */ border-radius: 50%; - animation: spin 2s linear infinite; + animation: loading_spin 2s linear infinite; width: 14px !important; height: 14px !important; @@ -28,13 +15,13 @@ border: 2px solid #f3f3f3; /* Light grey */ border-top: 2px solid #3498db; /* Blue */ border-radius: 50%; - animation: spin 2s linear infinite; + animation: loading_spin 2s linear infinite; width: 14px !important; height: 14px !important; } -@keyframes spin { +@keyframes loading_spin { 0% { transform: rotate(0deg); } @@ -48,213 +35,200 @@ user-zoom: fixed; } -.select_info { - font-family: Arial; - font-size: 12px; - /*white-space: pre;*/ - line-height: 1; - height: 100%; - display: flex; - flex-direction: column; +/* Some general style settings (from bootstrap) */ +:root { + --blue: #2196f3; + --indigo: #3f51b5; + --purple: #9c27b0; + --pink: #e91e63; + --red: #f44336; + --orange: #ff9800; + --yellow: #ffeb3b; + --green: #4caf50; + --teal: #009688; + --cyan: #00bcd4; + --white: #fff; + --gray: #6c757d; + --gray-dark: #343a40; + --primary: #009688; + --secondary: #6c757d; + --success: #4caf50; + --info: #03a9f4; + --warning: #ff5722; + --danger: #f44336; + --light: #f5f5f5; + --dark: #424242; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: "Roboto", "Helvetica", "Arial", sans-serif; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } -/* The Modal (background) */ -.modal_disabled { - display: none; /* Hidden by default */ - position: fixed; /* Stay in place */ - z-index: 1; /* Sit on top */ - left: 0; - top: 0; - width: 100%; /* Full width */ - height: 100%; /* Full height */ - overflow: auto; /* Enable scroll if needed */ - background-color: rgb(0, 0, 0); /* Fallback color */ - background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ - - /* Modal Header */ - .modal-header { - padding: 2px 16px; - min-height: 30px; - vertical-align: middle; - display: flex; - align-items: center; - - border: grey solid; - border-width: 0 0 1px 0; - - background-color: lightgreen; - } - - /* Modal Body */ - .modal-body:not(:empty) { - display: flex; - padding: 2px 16px; - flex-grow: 1; - flex-direction: column; - justify-content: stretch; - } - - /* Modal Footer */ - .modal-footer:not(:empty) { - padding: 2px 16px; - } - - /* The Close Button */ - .close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - position: absolute; - top: 0px; - right: 4px; - } - - .close:hover, - .close:focus { - color: black; - text-decoration: none; - cursor: pointer; - } - - /* Modal Content */ - .modal-content:not(:empty) { - position: absolute; - display: inline-flex; - flex-direction: column; - justify-content: stretch; - background-color: #fefefe; - margin: auto; - padding: 0; - border: 2px solid #888; - width: auto; - max-width: 90%; - box-shadow: 0 4px 15px 0 rgba(0, 0, 0, 0.2), 2px 6px 20px 0 rgba(0, 0, 0, 0.19); - animation-name: modalFlyIn; - animation-duration: 0.4s; - top: 10%; - max-height: 80%; - - left: 0; - right: 0; - } -} - -/* Add Animation */ -@keyframes modalFlyIn { - from { - top: 0%; - opacity: 0 - } - to { - top: 10%; - opacity: 1 - } -} - -.channel_perm_tbl input { - width: 30px; -} - -.channel_perm_tbl .key { - width: 120px; -} - -.channel_general_properties .value { - width: 100%; +*, :before, :after { + box-sizing: border-box; + outline: none; } html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + -ms-overflow-style: scrollbar; + -webkit-tap-highlight-color: transparent; background-color: gray; } +body { + height: 100vh; + width: 100vw; + top: 0; + left: 0; + right: 0; + padding: 0; + margin: 0; + + + font-family: Roboto, Helvetica, Arial, sans-serif; + font-size: 1rem; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fafafa; + + font-weight: 400; +} + +button, input, optgroup, select, textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + fieldset { border: unset; display: unset; } -.modal-head-error { - background: darkred; - - font-family: Arial; - font-size: 15px; - font-weight: bold; - - vertical-align: middle; -} - -.modal-button-group { - -} - -.modal-button-group button { - width: 100px; - margin-right: 5px; - margin-left: 5px; -} - -.modal-button-group button:last-of-type { - margin-right: 0px; -} - -.invalid_input { - border-color: red; -} - -.GroupBox { - border: gray solid; - border-width: 2px; - border-radius: 0px 6px 6px 6px; -} - code { background-color: lightgray; padding: 2px; } -.icon-playlist-manage { - &.icon { - width: 16px; - height: 16px; - - background-position: -5px -5px; - background-size: 25px; - } - - &.icon_x32 { - width: 32px; - height: 32px; - - background-position: -11px -9px; - background-size: 50px; - } - - &.icon_x24 { - width: 24px; - height: 24px; - - background-position: -11px -9px; - background-size: 50px; - } - - display: inline-block; - background: url('../../img/music/playlist.svg') no-repeat; -} - -x-content { - flex-grow: 1; - flex-shrink: 1; - display: flex; - flex-direction: column; - height: auto; -} - -[class*=" bmd-label"], [class^=bmd-label] { - color: rgba(0, 0, 0, .6) !important; -} /* bootstrap materialize fix */ .form-row { margin-left: 0 !important; margin-right: 0 !important; +} + +/* New design */ +a[href] { + color: #4d7bff; + text-decoration: none; + + &:hover { + color: #8aa8ff; + } + + &:visited { + color: #2752cd; + } +} + +/* code hightliting */ +.tag-hljs-inline-code, .tag-hljs-code { + display: block; + margin: 3px; + + font-size: 80%; + border-radius: .2em; + font-family: Monaco, Menlo, Consolas, "Roboto Mono", "Andale Mono", "Ubuntu Mono", monospace; + box-decoration-break: clone; + + &.tag-hljs-inline-code { + display: inline-block; + + > .hljs { + padding: 0 .25em!important; + } + + white-space: pre-wrap; + margin: 0 0 -0.1em; + vertical-align: bottom; + } + &.tag-hljs-code { + word-wrap: normal; + } + + code { + @include chat-scrollbar-horizontal(); + } +} + +/* fix tailing new line after code blocks */ +.message > { + .tag-hljs-code + br { + display: none; + } +} + +/* tooltip */ +#global-tooltip { + color: #999999; + background-color: #232222; + + position: fixed; + z-index: 1000000; + + pointer-events: none; + + padding: .25em; + transform: translate(-50%, -100%); /* translate up, center */ + + text-align: center; + border-right: 3px; + + display: flex; + flex-direction: column; + justify-content: space-around; + + opacity: 0; + @include transition(opacity .5s ease-in-out); + + &:after { + content: ''; + position: absolute; + + width: 0; + height: 0; + + left: calc(50% - .5em); + bottom: -.4em; + + border-style: solid; + border-width: .5em .5em 0 .5em; + border-color: #232222 transparent transparent transparent; + } + + &.shown { + opacity: 1; + } +} + +/* colored letters */ +a.rainbow-letter { + background-image: url('data:img/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAV/klEQVR4nM1dbY7j1hFsauSsk2CBAPkXBPkZ5Ca+QY6V+wS5SA5g569jrGN7d0diMCNRahar+uNRY7sDmeR7j++rqqublHYyffWPv39jNr+3V5tte8QyVZ4d120ms9fPYpM74vlUKF8+h2Idtjsk5f7aknGij1qjBceVzQSKKnTb44ejmf3pMtRs/GhQ5yFT97B2fC10kaSd7y1rW+lzj7H+I0BZO9U2unezjfvAf/m8P5hNH+IpVKaJ00X/tk17tRa2zq6hkzzC2IqYMm1Xyu8bWlT1WCHK5fzDoQ5wpw0uVW2Nnqtaiy9nhFd9jVjVkysWuUjJ+6MFsSiN10IdDr3pVKYaqUI870gFEHTVLjLVtguwqot2EetbfUceXZN62f5Q83JWVlWHDkm26+3I+V6CjBqushMgy14feUvUNiGLU4BoaiPqYKW6jgqw+7p1lfu9RUlbxaPZvZWx6GS7SV+h/lAHvKsOWSioq4AvY+vKPL+bYI4kaVPwYe0s3YEiy7vgw4YdttOOprnH83V9pm7VMPBo+VerHInrUd6AY73aYExPwYe2SQjISNDx/Gi5WgW6Up8lypFlIFXL1T2ZC8lJvxH4xRDw6Hq+VVUVUGGA7Rvrp2tV8CMdbRMoyu7VeRV8+ATvAfaQoNv2ch3F/cywXUcdKhatspIzlL2/kt2z8xEiWPoeYJQE2VJVGrRWA2UR0I8CvuK1FRVQ99P6itcrj1Z12A7KyXuADrCqHpdYinhyzZW1Zn1VrRrXKyqQEaJMgkeBT/pthABGDrb8KoHqKlANAdl5tT81M6zP6qJ+Q+nvyLmqUwkTtIEQEAGPZWxJXeD74OOasj1U12gVyceZZrRn3l/yenau2nRVAdoETwEZcBkJsu2KjBOhQoZO/oDWlfisDtux883EokUrMLGMtRGfo57+7KatztkKJtIH1rHraXWsjKhG3/ZWAwkt8n5cqTkyDHm/EfCMXEfyPpAfHDUIFpzjChjo7FoBj+ObJAJaB9gKEbJQwMDPVECSIPP6ild37iFtj3zZ2VZ1QDcAFesVrDUVmDd3Pcb7fZ94ngVG5f2p9O8BPyNDrgAViyDJQMc2CJ0J2LQKKNpmWqZmrCwiAZuHVeoUKNF1Rgosi8aZKQGUJFfyhGilEVHYuL5mur0jVBSzlELr8zmZeRa/I/BL3o9g+/PM60fAzwnQkfwRQlSh82V3KKMAgj2OerkyJvPqXJGBmgJ+BPz9BOhaJxSMlHs/ttU51vjNZnfvAV/1z0hgCRFWk1MAYZtR8PcRIBJPv3SkcJUEbFsiReAqwHqO4n8k+xUQ1diZ94fSX/H4EfADwAkB1NIMgJ7g7m7sr9arUHCvU96Ps4tAr1gUzyOShON1PL5KEFaPZTj29UjeA+DMKrE/Wm1GAiv6tW7Nykdmm2XzmANgeZr4KQXYS4AM8F4IqFpF9lm0ZqZCg7m6XAWY/LN9YBZJuIrp5aSvAqQFbV7sXLy/4PW+TBCg4j8ILgN7VDWwz5oKYGsWAjIisL6NkIEpALalS1Pgnd1xRAlYHS74sQrAVveovrDPWAUwSO2ZSSWut3KDyMsXq4KfqUAEuqhrECCS8wl6fitbgJ9u//V7qRSgOrNM0qNsnxoOzEhQAf8RXt8jQCbnWM98bnKr22PMr+fVyEbAz54vIkKg1DP5z9psBs48uwt+FBIsOXf14lWwAjRTAFz1YUAVIkjvdV4LcF1MAXzvbEYqtrN2WZvVQNlnFPyMDLjYWAFmt5wuYOyePSHh4HZFbe98+6/K+lmSWJ1NFNujJPB23yPBP8PkO2pgpA7Oj2vg9xDBb9novb4PZWt451tGsFzfj0wBOqNmRMB6ah3wu/kALjS6Zufbx8AKETArn0Wd2uJzIT+IPH89Bht9mylsCYLnOHIU11UucOs080wEeE8OgE8GOB6rc0fxbeCjFIFF4wMckf7mwgDubD0k4LnBCjKaKiKoBPDWaRdspQRVrx8EHgigAI48nJ1jXYc0CHp2fR9rduNEe4F7wozJegq8Hzz6eGC7wO/xfrbw6/Xgi6CRMFCR/wx0Bs+8GhN1gu1btCojQLMymvipQSPwFehYrvpViyyqQECAiuxXwPdqUJV/X+ZJoNOuBX4MOjgjtaLokU95PpV+Bi4eO0SIFKADPCMo/y4gi/8ZyHidkciAEAz4g1MPP6Zt+o+UUO1HN96niR8DmZGgmwyqsdjC2Tk5NhSgI/WsXbZLizHgl36YGtzLmAqYmCWbtT/PgN9IP1uSAr/j+dH7f0vOsR7L1wSIJL+bDBoAr/rzQLOVnR3A6jgDfPNm7Wrv1EqNgC7jvgJYnTPg98g/LhTnZsFRK8CepwIGPFMBC3IBDzau4gwhwRPB6BdF2f75WVY/m86r4Kt8oEIEBnZ0zshg6/bJ7wGqqpDlBUwV2OoPwfn5/ucMNuBPEArmdA/ZajbyLv4m8GpT0ZO7RGAEMOcLLAQgqBVvF8RoKAB6eFQege7vY9l/tCpPAhNEuPSdkUDZ5HpMwUfgGegdBZgT4B8Euj8Xr4KtWKaOBsArz8cQUNUztLODaE4JwKhoBOww9iPoEQnwvEKALvAV0ElZ8z3ASJkiRfSZ3A5VCIB2bqtA9VGQPrpViJB5fwR8BjpeF8G3LQFm44SoAI9plwoJ5yIRCrNfmVeU6UoAoyTAGXnZDwlQBb1CAlQCcyRAMrBzVVbZNk2AStLnge7kBcv5wd1fIcDkSIN5A9oJ+ucEiDTEx3//fyJx69IDe0quR57/cWssOUdQG+BbHAKsQYjKUamC+qgwIMUZ2l4Sw7kYClTidwMfgT0REpwI8NXkrwv6IOBYF7wKZpa9H2BhQK2mEgYYuc4EeGXzLRfwD5NnsgoE3R830u/BZwRg4FeAHwW9ATha4dtAJe+sjrXxbdnK/DUGwWXXojDgyYBqMF+DwJoEOGoIvgnQUQFQ/hXweI3bwMCtSH50HtQn/zjUb3JGAg+wuleFAazD4xnKPFwnd32Sq57tRPfeBPg3AnhQn4EACvzqI98er4/OszpX3gwBqp1KDKMytmK2K/jU4OE6O+CZkr20f7qeXTQAsTGg1JMjwGuDBehncR7F/4wAagtwiywoU21UPZQXFcCKxFDgG6yIPSUoMmCugOD7sBAlh2fqoIsdEHwDwJ8JEZAAFdnH5bGlG5yPeH1WrkOAknJmrG2kDGxVKugpZWDv/88FAlw+Z1ABg8e+GwEQ/GcgwkkAHz3ndyQ/OlfwVAnh6smLoMWUCrCeWY7g2ylSVI/4mVw653ceYdwS4Xz7rGf05D63eP/5+lnOUQE6Md8SwB8JfFbu6o8cnOSuMjlU30wRTOwK20H8BdEsSMBDwen1f/dniYMD/7X1AvgnIMFCgOiRj30MlqGucZuja9amWufsgf86eDGlBoupHMEIAbCMEYIR4OmK0JOL7Pcxz9dnAk+A4/VzA/7l85EQgCV8UZzHc7U8PO+WKUvaJn8ihlklR6i+TGLkiM4ZGSI1eAYS3H9P8GyfXmvNg3+6gv7T9fMJpB9jfgV4XM6Ip6M9qs2YAlTDhXosY3UqTLA2SkcZCY4uYC8wX0jwEgg+X98b/GYhwI9m9r/r8SMBP5L6aIpqSWx51fKuiX4GCBCBNHqvmkPHXRQZls8LzO883PbRvns9fvnyn+/N7L9XAnjwTwOgV6ef2aPAD+y47/Y9+UP13q7bROHiBcnfmtnvzex31xzgOzu+gP/NlQSfRIxX3XemO2pvSIQ3SAIfbW81vz+a2V/sr//81y+3tF+B/coI8KC5KBFYkrhPV6n/2uzf9pX97TXj+/rl/039qv8nl+0pzcdzNoFH2dtJQBACfmXKkIV/f46qf3Zv8T5ewf/W7PDt5ZaP9qW9sz9fk4Dvr48A/tlvhphgghRv8Rxnje9n+vbLKkDHgaI4zECfXSxfXux8vGb431+c/d319h9fU8MvbbI/mNkXZvYDvATAFwAsI1QJabSAzqZ0XtFXbd6bBILteT6t5HMGe6u83X9B49/q/XRF+wezw/Nd/pbqd5fngeuj4hfujdAzeQWILwLYhNjk1YLxUTgjQrRxzFi7qUCAPf1X2mR7knn7DFic4Yucz0CAq7o/ufi3NDu+vjL6zbXUvyD+HHwNyL79wYniAtRi/Xn0HiVrx4yHkeMur+20q5BfyTzzenwNi7/OeQYCfLyr+uHsXwmtU4SDHWx69X5zXzo9kW+D2BcCODkDQlhACvV63LervGaPNn9LlhoBsv6j+koorEi8CWdDycevcT+Dkp/gFz/k1uP1RyQX8z9AeXa/Qjq57xyQBH6i7B+/sMVHGxN9qVYxfc82BDxCyllZdJ7JvAkHY56/fD6Tr3RfwJ+33wzMcPul7gk2HYnw5FojEw+ECErKKgRQZZEiKEDWKhArQBVoNVd1Hh3VHjEnQwLgr3Y+OyU4rb3/AKKLPLrA7ZXA4JeDJ3etfhF6GCABhgNzZQggaxvZtq0mwN54rs4jb8frDvhM/p/XIfsAfuyn6btbYHta5QCL+R4O7g71jwIiErCFs81Rv52Ifmij3h2sSdRTgM511dst2RsG/Ex+j4cEgIR9Oq9hm2DLkFfnG8yeKgbhAH+PuL27TgJFALVpqAgM+OxHOj4HyMDN2lTCVkR4BbryfAT/DMA/r0nCwGe/GmSRfFr9oQo0Hw5OAPwZgFILZJvANg69mhHCCmpwvz6u+ojAVuAyoNnc1XnX+zP5J4/p07xN43BZOOTysVsAwLtMgO+BnwgBznAdqQAD3h+xXAOdvwcYARzngzuK9ZHHZ8AzEigCQD6Ggm3g/UaGmh2MFzWY4LnBm++VfWahBCws4AapzWP9KkAW26rEOgfYe85Aj9YTAZ+BjwRg9fPa+/GffWEOgMPiE/x0uwuTQn+ONMMcwStBpAKRAjAyGLRVxFibVoCqx1c9PwOfveRBEjCgo3+bR8BXCoBL8BCttxL/XpEHn/WGJEBtqRDAz2wi5wgEkgDL7u05ARQZOqDjbuI17rQFj3sjCjBvY38UBhgHkQh32Fk+4Dffg+7Lz7AZVRLg5jESZGTA84vlBIjqFeA43xHvZ8DPAeAkgCvw2ROAwRQxBKyfCjzQHTLg0QMXEYFtXkSCKDlct4kJUAU9Aj8CnhFAqUD0IX1WvN5bNDWM1HbLB1Q4MAc2ux7ZKJYvsNxAkYE9BUyEAF3Q3wL8SAWKZMDUzAgZmGXgz6s+kVr4J+zwfaMnAsp/pgQmZqS9u0KG7XuAKhFUWRX4igIoAkT7FMR9Rgpv1anrpPAMRDiTR8do49izh/+ov6+U5QTe1uVrAnS9P1MAVs6AizwfCaBIch1nmtc+ybwfwR/hrbl+7z12gccRqzOwQAkQfEWIaYAA6pzNO1tLJP0Z+OqR0c0/Any9BeslI8A9AphTgLM4qo3LvL9CBEvAz0JA1+tHgFdEyOI/O4cxo8e+KBwwy6Y9uaWuSeDVAEkwJ2W4Md587pARoR4S6gSIgM9At+QhO/N+bBe5JAG3m/x1/G89Jo6CJMCyCHymBuxLqY4CbMuO9MkkO46QQIEekaDyjmTOvZ+RomKK/0iAdd84kiIBAxrrO1SMiIBkuZt+DMxUIAPeCgAqImTlRP5x+xX4HSVgy878jffOvN6AFCbIoKwSCnCGBmXRewC2wRnwrGyPArC2ZJzI27NHP28znKtlRVFVk4CFBgtAr6iA7zsignpCUO8BHun9FihBVQFYmcEcYZuzpI89AeB5tsS156NlI6IamCPGROrUGKwNi/n8PE8CO7vR+UQqwMrEGCrejySC0RbgJ1cBNJT7xVQYMKEC+CTg21cSQlvNXhMg2gm2MwavujMPVnVROzWuWxaL95kvKiNDSBVA24aCORhZkUO19aPi46EvZ7OLQkCFDJnXWy97fwTgzPuVt7OcgC3Zmx9ePWFrq9Jtj/xnv1lkVL7YkW44HrskGAW/EhaunynwfgZyRf7VsvGjiMO2fjujyJT8qxdATA2Et9DZzYQAuHoT53uJUAGb9QtLYLKPW54pAVq2fb5++xh4b2NDJGAWeXhkeViIQ0CHBL6O5QJnqK8SBW1ef+HjDcGvJn7KIgXAo8F0fdl2/D1kYH2xDVM5wLqOhwDUthGPNwd65ZGw83HLUO8AVOKnCFH1L0UCvr04FlJlDxHi2K7br8tyAmSK8KiPuX7ZeG4ZatuybL/yCIhDKu9HH1H5fhwKuD7k5jN/ZpH0q6eALuhsdx7xMZi7QEbJvMoJujYLeLBMqQCOP4ck6Mx0AV/lBdFscCbqy6Aq4KzsrUhRADcixV5TCqDlfr2tfA4jRJib4MdqwL8LYBoXgfPGwCvv99dZWBhJBlH6zZ3j0WDKOD4PBQz00bAwZsfVSnHVUd2jwFc7B+XM+1WZFQgRGYJuUMaOUd+MQOs72GjVHivlfrz1Zh9DQDKwWVlVKXCt2T3JlmTL3mOZAvCt3Y7P4YmeDCIisN4iuef36CQwUwBL3v2rtla4B9Yfef+ICkTGfLEj/1keoNtFNFFg45NANHucwaW+/hRQVYWqoijQbX3tX/lmgLIEkZFBgTQCPgLKVIBBux6L0any/KFeBSvbEoK/CRy5jry4Cjq7r+Dh6ulAPSVULALfoNzc1CtPAjoUoFZUSJAlkDPsyHrjx14EZR6d1VnSJ2wYs47MszbRfRn4ka9GCoD9befBRlU9VK8n2FyVA1gAIF5HIEflqh/cPeL9VaD3ZP9okT+hr6pxYsDZGFWaVa+53ix1eQjA8lHwM0LYekzltdEH27F+Rojgp5ZFa+R0FDoiaDTNqqRgM9+2O/zs4FsynjsfyeZHPZ2d41QtmC67D/uMtndtKqOJzrM63u4oZ1cJDRnIbIWZ9Cfer4wlgo/2dtbGwLeYjeT4+7KQSruLHW51jwS/6vHK3RIJV5xWhn11iRF5P7tWfYhoJ/vlweyRSjBdQ0AGLK5ASTfew67xngd4/2jbzCqy7q+romgh6GpVeBwhwbbs0AI8onBVFfBeshuZ92fblJVlVgW+AjCW9Qmh9OsxJNgmgWymkVdnyqHa42qTn3lhmVqiurfSdwZM5LUKWNU36y8nAbOe5GObdQ7QCQPsPpz9Dq/PwIvqWELYtUyaI0/ORFNtSWzKk3GFVYUwkgPgDKvg42oqwOOuNZb/c1s0/czzo2UG/hD0WQ0JrGyrEHkOYAH4VryPnbtjZUkGZartW5GkwuFIHCsqUA8FjyJBlgNEK1MrUaFAWdH7o/IO6FnbzrRVfTcX6Fk3DJggASrAHvAzSrOypvePePajcwEGeiVXYPeOq4AJXTTq4VndPQnEWXVkHlcR9ZmvTlpF5kdUoWsqD1Cyn5EmU5I8H8CyOgnqIQBXizPMyBFMPxOzyjIzT+8QYr9M6367PqUt2jVWrp4CzN7vlvlo5oPeXwVxr8SPGvN0ZqpN1etroWDk+HrP+5cvg/5js72/jaKkPZN8toJAAzupDJ53LFIQnHY0xrIE1UYBytr7sXy/ag5LOa/3NZWju8/sw/8BT1vrMTgvQV8AAAAASUVORK5CYII='); + background-size: 100% 100%; + + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + + font-weight: bold; + border-bottom: 1px solid #ab4788; + line-height: 1em; } \ No newline at end of file diff --git a/shared/css/static/hostbanner.scss b/shared/css/static/hostbanner.scss new file mode 100644 index 00000000..ebedfe4b --- /dev/null +++ b/shared/css/static/hostbanner.scss @@ -0,0 +1,78 @@ +@import "./mixin.scss"; + +#hostbanner { + .container-hostbanner { + position: relative; + + overflow: hidden; + height: 1000px; /* allocate some height to be truncated by the flex :) */ + + display: flex; + flex-direction: column; + justify-content: stretch; + + cursor: pointer; + + &:not(.no-background) { + background-color: #2e2e2e; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + -moz-box-shadow: inset 0 0 5px #00000040; + -webkit-box-shadow: inset 0 0 5px #00000040; + box-shadow: inset 0 0 5px #00000040; + padding-bottom: 5px; + } + + &.disabled { + padding-bottom: 0; + height: 0; + } + + @include transition(height 0.5s ease-in-out); + + .hostbanner-image-container { + height: 100%; + width: 100%; + + flex-grow: 1; + flex-shrink: 1; + + text-align: center; + + &.hostbanner-mode-0 { + /* do not adjust */ + display: block; + } + + &.hostbanner-mode-1 { + /* do adjust and ignore ration */ + display: flex; + + height: 100%; + width: 100%; + + > img { + width: 100%; + height: 100%; + } + } + + &.hostbanner-mode-2 { + display: flex; + flex-direction: row; + justify-content: space-around; + + > img { + object-fit: contain; + max-height: 100%; + + /* "Normal" third more */ + //max-width: 100%; + + /* better adoptable mode */ + width: min-content; + } + } + } + } +} \ No newline at end of file diff --git a/shared/css/static/main-layout.scss b/shared/css/static/main-layout.scss index cc42d86e..d53bdf57 100644 --- a/shared/css/static/main-layout.scss +++ b/shared/css/static/main-layout.scss @@ -8,6 +8,10 @@ $animation_length: .5s; min-height: 330px; .container-app-main { + height: 100%; + width: 100%; + + min-height: 500px; margin-top: 5px; position: relative; @@ -15,14 +19,14 @@ $animation_length: .5s; flex-direction: column; justify-content: stretch; - height: 100%; - width: 100%; .container-channel-chat { - min-height: 200px; - min-width: 100px; + height: 80%; /* "default" settings */ width: 100%; + min-height: 25em; + min-width: 100px; + display: flex; flex-direction: row; justify-content: stretch; @@ -34,25 +38,44 @@ $animation_length: .5s; border-radius: 5px; } - .container-channel-tree { + > .container-channel-tree { + width: 50%; /* "default" settings */ + height: 100%; + background: #353535; min-width: 200px; display: flex; + flex-direction: column; justify-content: stretch; - height: 100%; + min-height: 100px; - padding-top: 5px; - /* - overflow: auto; - overflow-x: visible; - */ overflow: hidden; - overflow-y: auto; + + > .hostbanner { + flex-grow: 0; + flex-shrink: 0; + + max-height: 9em; /* same size as the info pannel */ + + display: flex; + flex-direction: column; + justify-content: stretch; + } + + > .channel-tree { + padding-top: 5px; + + flex-grow: 1; + flex-shrink: 1; + } } - .container-chat { + > .container-chat { + width: 50%; /* "default" settings */ + height: 100%; + background: #353535; min-width: 350px; @@ -62,16 +85,66 @@ $animation_length: .5s; } } - .container-server-log { - min-height: 0; - height: 250px; + + > .container-bottom { + height: 20%; + + min-height: 1.5em; width: 100%; - border-radius: 5px; - padding-right: 5px; - padding-left: 5px; + display: flex; + flex-direction: column; + justify-content: stretch; - background: #353535; + > .container-server-log { + display: flex; + flex-direction: column; + justify-content: stretch; + + flex-shrink: 1; + flex-grow: 1; + + min-height: 0; + width: 100%; + + border-radius: 5px 5px 0 0; + + padding-right: 5px; + padding-left: 5px; + + background: #353535; + } + + > .container-footer { + flex-shrink: 0; + flex-grow: 0; + + height: 1.5em; + + background: #252525; + color: #353535; + + border-radius: 0 0 5px 5px; + padding-right: 5px; + padding-left: 5px; + padding-top: 2px; + + -webkit-box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125); + -moz-box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125); + box-shadow: inset 0px 2px 5px 0px rgba(0,0,0,0.125); + + display: flex; + flex-direction: row; + justify-content: space-between; + + > * { + align-self: center; + } + + a[href], a[href]:visited { + color: #353535!important; + } + } } } @@ -82,7 +155,7 @@ $animation_length: .5s; border-radius: 5px; - height: 30px; + height: 2em; width: 100%; background-color: #454545; @@ -121,39 +194,6 @@ $animation_length: .5s; } @media only screen and (max-width: $small_device) { - .app { - .container-app-main { - .container-info { - display: none; - position: absolute; - - width: 100% !important; /* override the seperator property */ - height: 100%; - - z-index: 1000; - - &.shown { - display: block; - } - - .select_info { - > .close { - display: block; - } - } - } - - .container-channel-chat + .container-seperator { - display: none; - animation: fadeout $animation_length linear; - } - - .container-channel-chat { - width: 100% !important; /* override the seperator property */ - } - } - } - .hide-small { display: none; opacity: 0; diff --git a/shared/css/static/menu-bar.scss b/shared/css/static/menu-bar.scss new file mode 100644 index 00000000..e9d8cbcc --- /dev/null +++ b/shared/css/static/menu-bar.scss @@ -0,0 +1,125 @@ +@import "mixin"; + +.top-menu-bar { + @include user-select(none); + + height: 1.5em; + width: 100%; + + background: #fafafa; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + position: fixed; + top: 0; + z-index: 201; + + font-family: Arial, serif; + + .container-menu-item { + position: relative; + + .menu-item { + cursor: pointer; + + padding-left: .4em; + padding-right: .4em; + + height: 100%; + + display: flex; + flex-direction: row; + width: max-content; + + > * { + vertical-align: middle; + } + + .container-icon { + height: 1.2em; + width: 1.2em; + padding: .1em; + font-size: 1em; + + margin-right: .2em; + display: inline-block; + } + + .container-label { + display: inline-block; + align-self: center; + } + } + + &:hover:not(.disabled) { + background-color: #00000044; + } + + &.disabled { + background-color: #00000022; + } + + .sub-menu { + z-index: 1000; + display: none; + + background: white; + position: absolute; + + top: 100%; + border: 1px solid black; + + > .container-menu-item { + padding-right: .5em; + } + } + + &.type-side { + &.sub-entries:after { + position: absolute; + + display: block; + content: '>'; + + top: 0; + bottom: 0; + + right: .4em; + } + + > .sub-menu { + top: -1px; /* border */ + left: 100%; + } + + &:hover { + > .sub-menu { + display: block; + } + } + } + + &.active { + background-color: #00000044; + + > .sub-menu { + display: block; + } + } + } + + > .container-menu-item { + > .menu-item { + .container-icon { + display: none; + } + } + } + + hr { + margin-top: .125em; + margin-bottom: .125em; + } +} \ No newline at end of file diff --git a/shared/css/static/mixin.scss b/shared/css/static/mixin.scss new file mode 100644 index 00000000..55bd184e --- /dev/null +++ b/shared/css/static/mixin.scss @@ -0,0 +1,145 @@ +/* Some general browser helpers */ + +@mixin transition($transition...) { + -moz-transition: $transition; + -o-transition: $transition; + -webkit-transition: $transition; + transition: $transition; +} +@mixin transition-property($property...) { + -moz-transition-property: $property; + -o-transition-property: $property; + -webkit-transition-property: $property; + transition-property: $property; +} +@mixin transition-duration($duration...) { + -moz-transition-property: $duration; + -o-transition-property: $duration; + -webkit-transition-property: $duration; + transition-property: $duration; +} +@mixin transition-timing-function($timing...) { + -moz-transition-timing-function: $timing; + -o-transition-timing-function: $timing; + -webkit-transition-timing-function: $timing; + transition-timing-function: $timing; +} +@mixin transition-delay($delay...) { + -moz-transition-delay: $delay; + -o-transition-delay: $delay; + -webkit-transition-delay: $delay; + transition-delay: $delay; +} + +@mixin transform($transform...) { + -webkit-transform: $transform; + -o-transform: $transform; + -ms-transform: $transform; + transform: $transform; +} + +@mixin placeholder($element) { + #{$element}::-webkit-input-placeholder { + @content; + } + #{$element}:-moz-placeholder { + @content; + } + #{$element}::-moz-placeholder { + @content; + } + #{$element}:-ms-input-placeholder { + @content; + } + #{$element}::-ms-input-placeholder { + @content; + } + #{$element}::placeholder { + @content; + } +} + +@mixin user-select($mode) { + -webkit-user-select: $mode; + -moz-user-select: $mode; + -ms-user-select: $mode; + user-select: $mode; +} + +@mixin chat-scrollbar() { + & { + /* for moz */ + scrollbar-color: #353535 #555; + } + + &::-webkit-scrollbar-track { + //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: .25em; + background-color: transparent; + } + + &::-webkit-scrollbar { + width: .5em; + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + border-radius: .25em; + //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #555; + } +} + +@mixin chat-scrollbar-vertical() { + & { + /* for moz */ + scrollbar-color: #353535 #555; + scrollbarWidth: .5em; + } + + &::-webkit-scrollbar-track { + //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: .25em; + background-color: transparent; + cursor: pointer; + } + + &::-webkit-scrollbar { + width: .5em; + background-color: transparent; + cursor: pointer; + } + + &::-webkit-scrollbar-thumb { + border-radius: .25em; + //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #555; + } +} + +@mixin chat-scrollbar-horizontal() { + & { + /* for moz */ + scrollbar-color: #353535 #555; + scrollbarWidth: .5em; + } + + &::-webkit-scrollbar-track { + //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: .25em; + background-color: transparent; + cursor: pointer; + } + + &::-webkit-scrollbar { + height: .5em; + background-color: transparent; + cursor: pointer; + } + + &::-webkit-scrollbar-thumb { + border-radius: .25em; + //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #555; + } +} \ No newline at end of file diff --git a/shared/css/static/modal-about.scss b/shared/css/static/modal-about.scss new file mode 100644 index 00000000..f0ce35e6 --- /dev/null +++ b/shared/css/static/modal-about.scss @@ -0,0 +1,53 @@ +.modal-about { + display: flex!important; + flex-direction: row!important; + + text-align: center; + color: #999999; + + .container-right { + text-align: left; + padding-left: 2em; + + h1 { + font-size: 1.5em; + margin-block-start: 0.35em; + margin-block-end: 0.35em; + } + + h2 { + font-size: 1.25em; + margin-block-start: 0.10em; + margin-block-end: 0.10em; + } + + p { + margin-block-start: .25em; + margin-block-end: .25em; + } + } + + .version { + width: 100%; + display: flex; + flex-direction: row; + justify-content: stretch; + + a { + width: 50%; + + flex-grow: 1; + flex-shrink: 1; + + text-align: right; + } + .value { + padding-left: .25em; + + text-align: left; + + flex-grow: 1; + flex-shrink: 1; + } + } +} \ No newline at end of file diff --git a/shared/css/static/modal-avatar.scss b/shared/css/static/modal-avatar.scss index e7162e34..cc9ce10f 100644 --- a/shared/css/static/modal-avatar.scss +++ b/shared/css/static/modal-avatar.scss @@ -172,4 +172,127 @@ } } } +} + +.modal-avatar-upload { + display: flex; + flex-direction: column; + justify-content: stretch; + + .container-upload { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: space-between; + + .bmd-form-group { + padding-top: 0; + } + + input[type="file"] { + display: none; + } + } + + .container-preview { + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .title { + font-size: 1.2em; + font-weight: bold; + + border-bottom: 1px solid gray; + } + + .previews { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: space-evenly; + + align-self: center; + + .preview { + flex-shrink: 1; + flex-grow: 1; + + width: 11rem; + min-width: 11rem; + max-width: 11rem; + + height: 13rem; + min-height: 13rem; + max-height: 13rem; + + text-align: center; + + display: flex; + flex-direction: column; + justify-content: flex-end; + + .container-avatar { + display: flex; + flex-direction: row; + justify-content: space-around; + + .avatar { + position: relative; + + height: 1em; + width: 1em; + + overflow: hidden; + border-radius: 50%; + + > img { + position: absolute; + + top: 0; + left: 0; + + height: 100%; + width: 100%; + } + } + } + + > a { + margin-top: 1em; + } + + &.preview-client-info { + .container-avatar { + font-size: 10rem; + } + } + + &.preview-chat { + .container-avatar { + font-size: 2.5rem; + } + } + + &.preview-chat-entry { + .container-avatar { + font-size: 2rem; + } + } + } + } + } +} + +@media all and (max-width: 40rem) { + .modal-avatar-upload .container-preview .previews { + flex-direction: column; + } } \ No newline at end of file diff --git a/shared/css/static/modal-channel.scss b/shared/css/static/modal-channel.scss index 769ebfac..d3629b14 100644 --- a/shared/css/static/modal-channel.scss +++ b/shared/css/static/modal-channel.scss @@ -1,255 +1,815 @@ $required_notab_height: 800px; -.container-channel-edit-general, .tab-channel-edit-general { - flex-shrink: 0; - - .container-name-icon { - display: flex; - flex-direction: row; - justify-content: stretch; - - .container-name { - flex-grow: 1; - flex-shrink: 1; - } - - .container-icon { - flex-grow: 0; - flex-shrink: 0; - } - } - - .container-icon { - width: 30px; - - margin-left: 10px; - - .button-select-icon { - left: 0; - right: 0; - top: 0; - bottom: 0; - - position: absolute; - - .icon-node { - cursor: pointer; - - height: 100%; - width: 100%; - - &:hover { - background-color: #00000011; - } - - > div { - vertical-align: middle; - text-align: center; - } - } - } - } -} - -.tab-tag-channel-edit-general { - display: none!important; -} - -.tab-channel-edit-general { - padding: 5px; - display: none; -} - -.container-channel-edit-general { - display: flex; - flex-shrink: 0; -} - -@media (max-height: $required_notab_height) { - .tab-tag-channel-edit-general { - display: inline-block!important; - } - - .tab-channel-edit-general { - display: flex; - } - - .container-channel-edit-general { - display: none; - } -} - - -.container-channel-settings-standard { - min-height: 300px; - - flex-grow: 1; - display: flex; - flex-direction: row; - justify-content: stretch; - - .container-divider { - border-left:1px solid #000; - height: auto; - - flex-grow: 0; - flex-shrink: 0; - } - - .container-left, .container-right { - display: flex; - justify-content: space-around; - align-self: center; - - flex-grow: 1; - flex-shrink: 1; - - width: 50%; - } - - .container-right { - flex-direction: column; - align-content: stretch; - vertical-align: center; - - margin: 20px 50px 20px 50px; - } - - .container-channel-type { - padding: 5px; - - border: lightgrey 2px solid; - border-radius: 2px; - text-align: left; - } -} - -.container-channel-settings-audio { - flex-grow: 1; - display: flex; - flex-direction: row; - justify-content: stretch; - - .container-divider { - border-left:1px solid #000; - height: auto; - - flex-grow: 0; - flex-shrink: 0; - } - - .container-presets, .container-custom { - display: flex; - justify-content: space-around; - text-align: left; - align-self: center; - - flex-grow: 1; - flex-shrink: 1; - - width: 50%; - } - - .container-custom { - margin: 20px 50px 20px 50px; - justify-content: stretch; - - > .group_box { - flex-grow: 1; - flex-shrink: 1; - } - } -} - -.container-channel-settings-permission { - flex-grow: 1; - - display: flex; - justify-content: space-evenly; - - align-items: center; - - - .container-left, .container-right { - margin-top: 20px; - margin-bottom: 20px; - - display: flex; - justify-content: space-around; - align-self: center; - - flex-grow: 1; - flex-shrink: 1; - - width: 50%; - - > .group_box { - flex-grow: 1; - flex-shrink: 1; - } - - .form-placeholder { - display: block; - visibility: hidden; - } - } - - .container-left { - margin-left: 10%; - margin-right: 10px; - } - - .container-right { - margin-right: 10%; - margin-left: 10px; - } -} - -.container-channel-settings-advanced { - flex-grow: 1; +@import "mixin"; +@import "properties"; +.modal-body.modal-channel { display: flex; flex-direction: column; - align-items: center; + justify-content: stretch; - .container-max-users, .container-other { + max-height: calc(100vh - 10em); + padding: 1em!important; + + input, textarea, select { width: 100%; } - .container-max-users { - margin-top: 20px; + select { + margin-left: 0!important; + height: 2.5em!important; + } + textarea { + padding: .5em; + } + + .container-general { display: flex; - flex-direction: row; + flex-direction: column; justify-content: stretch; - .group_box:not(:first-of-type) { - margin-left: 40px; + flex-shrink: 0; + + > div:not(:first-of-type) { + flex-grow: 0; + flex-shrink: 0; + + margin-top: 1em; } - > .group_box { - flex-grow: 1; - flex-shrink: 1; - } - - fieldset { - padding-top: 1rem; - } - - .form-row { - margin-left: 20px; + .container-name-icon { + flex-grow: 0; + flex-shrink: 0; display: flex; flex-direction: row; justify-content: stretch; - .bmd-form-group { - padding-top: 0; + .container-icon-select { + position: relative; + + height: 2.5em; + border-radius: .2em; + + margin-left: 1em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + cursor: pointer; + background-color: #121213; + border: 1px solid #0d0d0d; + + .icon-preview { + height: 100%; + width: 3em; + + border: none; + border-right: 1px solid #0d0d0d; + + display: flex; + flex-direction: column; + justify-content: space-around; + + > div { + align-self: center; + } + + @include transition(border-color $button_hover_animation_time ease-in-out); + } + + .container-dropdown { + position: relative; + cursor: pointer; + + display: flex; + flex-direction: column; + justify-content: space-around; + + height: 100%; + width: 1.5em; + + .button { + text-align: center; + + .arrow { + border-color: #999999; + } + } + + .dropdown { + display: none; + position: absolute; + width: max-content; + + top: calc(2.5em - 1px); + + flex-direction: column; + justify-content: flex-start; + + background-color: #121213; + border: 1px solid #0d0d0d; + border-radius: .2em 0 .2em .2em; + + right: -1px; + + .entry { + padding: .5em; + + &:not(:last-of-type) { + border: none; + border-bottom: 1px solid #0d0d0d; + } + + &:hover { + background-color: #17171a; + } + } + } + + &:hover { + border-bottom-right-radius: 0; + .dropdown { + display: flex; + } + } + } + + &:hover { + background-color: #17171a; + border-color: hsla(0, 0%, 20%, 1); + + .icon-preview { + border-color: hsla(0, 0%, 20%, 1); + } + } + + @include transition(border-color $button_hover_animation_time ease-in-out); + } + } + + .container-description { + position: relative; + + flex-grow: 1!important; + flex-shrink: 1!important; + + min-height: 5em; + max-height: 22.5em; + + border-radius: .2em; + border: 1px solid #111112; + + overflow: hidden; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .toolbar { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + width: 100%; + height: 2.5em; + + background-color: #17171a; + font-size: .8em; + + padding: .25em; + + .button { + cursor: pointer; + + padding: .5em; + &:not(:first-child) { + margin-left: .25em; + } + + border-radius: .2em; + border: 1px solid #111112; + + background-color: #121213; + + height: 2em; + width: 2em; + + display: flex; + flex-direction: column; + justify-content: center; + + text-align: center; + align-self: center; + + &.button-bold { + font-weight: bold; + } + + &.button-italic { + font-style: italic; + } + + &.button-underline { + text-decoration: underline; + } + + &.button-color { + input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + } + } + + &:hover { + background-color: #0f0f0f; + @include transition(background-color $button_hover_animation_time); + } + } } - label { - width: 100px; + > .input-boxed { + flex-shrink: 1; + flex-grow: 1; + + min-height: 2.5em; + height: 5em; + max-height: 20em; + + border: none; + border-radius: 0; + border-top: 1px solid #111112; + + + overflow-x: hidden;; + overflow-y: auto; + + resize: vertical; + + @include chat-scrollbar-vertical(); + } + + &:focus-within { + background-color: #131b22; + //border-color: #284262; } } } + + .mode-container { + flex-grow: 1; + flex-shrink: 1; + + min-height: min-content; + + display: flex; + position: relative; + @include transition(.25s ease-in-out); + } + + .container-advanced, .container-simple { + flex-grow: 1; + flex-shrink: 1; + + margin-top: 1em; + min-width: 20em; + + width: 50em; + + &.hidden { + position: absolute; + top: 0; + } + &.container-simple.hidden { + transform: translate(-100%, -100%); + } + + &.container-advanced.hidden { + transform: translate(100%, 100%); + } + @include transition(.25s ease-in-out); + + .header { + text-align: center; + color: #548abc; + } + + fieldset { + padding: 0; + width: 100%; + } + + label { + display: flex; + flex-direction: row; + justify-content: stretch; + + /* total height 2.5em */ + margin-top: .5em; + margin-bottom: .5em; + height: 1.5em; + + cursor: pointer; + + * { + align-self: center; + } + + a { + margin-left: .5em; + margin-right: .5em; + } + + .form-group { + margin: -.5em 0!important; + + padding: 0!important; + + input { + height: 1.5em!important; + } + } + } + + + /* radio buttons */ + $icon_width: 1.7em; /* equal to the label height */ + + .input-boxed { + position: relative; + + height: 1.7em; + margin-left: 2.5em; + + flex-grow: 1; + flex-shrink: 1; + + min-width: 4em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-tooltip { + + flex-shrink: 0; + flex-grow: 0; + + position: relative; + width: $icon_width; + + display: flex; + flex-direction: column; + justify-content: center; + + img { + height: 1em; + width: 1em; + + align-self: center; + font-size: 1.2em; + } + + .tooltip { + display: none; + } + } + } + + .container-type, .container-codec, .container-sort { + padding-top: .5em; + } + + .container-talk { + .input-boxed { + margin-left: 0!important; + height: 2.5em; + + .container-tooltip { + width: 2.5em!important; + } + } + } + } + + .container-advanced { + display: flex; + flex-direction: column; + justify-content: stretch; + + min-height: 5em; + + border-radius: .2em; + border: 1px solid #111112; + + background-color: #17171a; + + .categories { + height: 2.5em; + + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + border-bottom: 1px solid #1d1d1d; + + .entry { + padding: .5em; + + text-align: center; + + flex-grow: 1; + flex-shrink: 1; + + cursor: pointer; + + &:hover { + color: #b6c4d6; + } + + &.selected { + border-bottom: 3px solid #245184; + margin-bottom: -1px; + + color: #245184; + } + + @include transition(color $button_hover_animation_time, border-bottom-color $button_hover_animation_time); + } + } + + .bodies { + position: relative; + + flex-shrink: 1; + flex-grow: 1; + display: flex; + justify-content: stretch; + + min-height: 12em; + height: 20em; + + .body { + position: absolute; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + padding: .5em; + + display: flex; + justify-content: stretch; + + overflow: auto; + @include chat-scrollbar-vertical(); + + &.hidden { + display: none; + } + + &.container-standard { + flex-direction: column; + overflow: visible; + + .container-top, .container-bottom { + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: row; + justify-content: stretch; + + min-height: 5em; + } + + .container-right, .container-left { + flex-shrink: 1; + flex-grow: 1; + + min-width: 3em; + width: 50%; + + display: flex; + flex-direction: column; + justify-content: start; + } + + .container-top { + border-bottom: 2px solid #111113; + .container-left, .container-right { + padding-bottom: .5em; + } + } + + .container-bottom { + .container-left, .container-right { + padding-top: .5em; + } + } + + .container-left { + border-right: 2px solid #111113; + padding-right: .5em; + } + + .container-right { + border: none; + padding-left: .5em; + } + + .container-perm-default { + display: flex; + flex-direction: row; + justify-content: space-between; + + > * { + margin-bottom: 0; + margin-top: 0; + align-self: center; + } + + .container-default-channel { + display: inline-flex; + flex-direction: row; + justify-content: flex-end; + } + } + } + + &.container-permissions { + flex-direction: row; + overflow: visible; + + .container-right, .container-left { + flex-shrink: 1; + flex-grow: 1; + + min-width: 3em; + width: 50%; + + display: flex; + flex-direction: column; + justify-content: start; + } + + .container-left { + padding-right: .5em; + border-right: 2px solid #111113; + } + + .container-right { + padding-left: .5em; + } + + + .container-permission { + display: flex; + flex-direction: row; + justify-content: stretch; + + margin-top: .5em; + margin-bottom: .5em; + + .name { + flex-grow: 0; + flex-shrink: 0; + width: 8em; + + align-self: center; + } + + .input-boxed { + align-self: center; + margin-left: 0!important; + } + } + } + + &.container-audio { + overflow: visible; + flex-direction: column; + + .container-top { + width: 100%; + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-right, .container-left { + border-bottom: 2px solid #111113; + padding-bottom: .5em; + } + + .container- { + border-right: 2px solid #111113; + } + } + + .container-bottom { + width: 100%; + + padding-top: .5em; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + text-align: center; + + .container-needed-bandwidth { + padding-left: .5em; + font-weight: bold; + } + + .hint { + color: #383838; + font-size: .8em; + } + } + + .container-right, .container-left { + flex-shrink: 1; + flex-grow: 1; + + width: 50%; + min-width: 3em; + height: unset; + + display: flex; + flex-direction: column; + justify-content: start; + } + + .container-left { + padding-right: .5em; + + border-right: 2px solid #111113; + } + + .container-right { + border: none; + padding-left: .5em; + } + } + + &.container-misc { + flex-direction: column; + overflow: visible; + + + .container-other { + display: flex; + flex-direction: column; + justify-content: flex-start; + + .container-phonetic, .container-delay, .container-encrypt { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + padding-top: .5em; + padding-bottom: .5em; + + > a { + flex-grow: 0; + flex-shrink: 0; + + width: 10em; + align-self: center; + } + + > button { + flex-grow: 0; + flex-shrink: 0; + + width: 5em; + + /* results in a height of 1.7em */ + height: 2em; + font-size: .85em; + + align-self: center; + margin-left: 1em; + } + + > input, .input-boxed { + flex-grow: 1; + flex-shrink: 1; + align-self: center; + margin-left: 0; + } + } + } + } + } + } + } + + .container-simple { + display: flex; + flex-direction: row; + justify-content: stretch; + + min-height: 5em; + border-radius: 0.2em; + border: 1px solid #111112; + background-color: #17171a; + padding: .5em; + + .container-left, .container-right { + flex-grow: 1; + flex-shrink: 1; + + width: 50%; + } + + .container-left { + padding-right: .5em; + border-right: 2px solid #111113; + } + + .container-right { + padding-left: .5em; + } + + .container-perm-default { + display: flex; + flex-direction: row; + justify-content: space-between; + + > * { + margin-bottom: 0; + margin-top: 0; + align-self: center; + } + + .container-default-channel { + display: inline-flex; + flex-direction: row; + justify-content: flex-end; + } + } + + .container-talk { + padding-top: .5em; + } + } + + .container-buttons { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + flex-shrink: 0; + flex-grow: 0; + + .spacer { + flex-grow: 1; + flex-shrink: 1; + } + + > *:not(.spacer) { + flex-grow: 0; + flex-shrink: 0; + } + + label { + cursor: pointer; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + > * { + align-self: center; + } + } + + button { + &:not(:last-of-type) { + margin-right: 1em; + } + } + + a { + padding-left: .25em; + } + } } \ No newline at end of file diff --git a/shared/css/static/modal-connect.scss b/shared/css/static/modal-connect.scss index 8ab6907e..55bd8693 100644 --- a/shared/css/static/modal-connect.scss +++ b/shared/css/static/modal-connect.scss @@ -1,73 +1,349 @@ +@import "mixin"; + .modal .modal-connect { + @include user-select(none); - /* - margin-top: 5px; + font-size: 1rem; + max-width: 100000px; /* max 100000px width, else we shrink the modal */ + padding: 0!important; /* override the default padding */ - > div:not(:first-of-type) { - margin-top: 5px; - } + display: flex; + flex-direction: column; + justify-content: stretch; - .profile-select-container { - display: flex; - flex-direction: row; - justify-content: space-between; + .container-connect-input { + flex-grow: 0; + flex-shrink: 0; - select { - width: 150px; + /* apply the default padding */ + padding: .75em 24px; + + border-left: 2px solid #0073d4; + + > .row { + display: flex; + flex-direction: row; + justify-content: stretch; + + > *:not(:last-of-type) { + margin-right: 3em; + } + } + + .container-address-password { + .container-address { + flex-grow: 1; + flex-shrink: 1; + } + + .container-password { + flex-grow: 0; + flex-shrink: 4; + + min-width: 21.5em; + } + } + + .container-profile-manage { + flex-grow: 0; + flex-shrink: 4; + + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-select-profile { + flex-grow: 1; + flex-shrink: 1; + + min-width: 14em; + + > .invalid-feedback { + width: max-content; /* allow overflow here */ + } + } + + .container-manage { + flex-grow: 0; + flex-shrink: 4; + + margin-left: 15px; + } + + .button-manage-profiles { + min-width: 7em; + margin-left: 0.5em; + } + } + + .container-nickname { + flex-grow: 1; + flex-shrink: 1; + } + + .container-buttons { + padding-top: 1em; + + display: flex; + flex-direction: row; + justify-content: space-between; + + .container-buttons-connect { + display: flex; + flex-direction: row; + } + + .button-right { + min-width: 7em; + margin-left: 0.5em; + } + + .button-left { + min-width: 14em; + } + } + + .arrow { + border-color: #7a7a7a; + margin-left: .5em; } } - .profile-invalid { + .container-last-servers { + flex-grow: 0; + flex-shrink: 1; + display: flex; flex-direction: column; - justify-content: start; - - > div { - display: inline-flex; - flex-direction: row; - } - - color: red; - } - */ - - .container-address-password { - display: flex; - flex-direction: row; justify-content: stretch; - .container-address { - flex-grow: 1; - flex-shrink: 1; + max-height: 0; + opacity: 0; + overflow: hidden; + padding: 0; + + min-width: 0; + + + border: none; + border-left: 2px solid #7a7a7a; + + @include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out); + &.shown { + /* apply the default padding */ + padding: 0 24px 24px; + + max-height: 100%; + opacity: 1; + + @include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out) } - .container-password { - flex-grow: 0; - flex-shrink: 4; + hr { + height: 0; + width: calc(100% + 46px); + min-width: 0; - margin-left: 15px; + margin: 0 0 0 -23px; + + padding: 0; + + border: none; + border-top: 1px solid #090909; + + margin-bottom: .75em; + } + + color: #7a7a7a; + + /* general table class */ + .table { + width: 100em; + max-width: 100%; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .head { + display: flex; + flex-direction: row; + justify-content: stretch; + + flex-grow: 0; + flex-shrink: 0; + + border: none; + border-bottom: 1px solid #161618; + } + + + .body { + flex-grow: 0; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + overflow: auto; + + .row { + cursor: pointer; + + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + &:hover { + background-color: #202022; + } + + &.selected { + background-color: #131315; + } + } + + .body-empty { + height: 3em; + text-align: center; + display: flex; + flex-direction: column; + justify-content: space-around; + font-size: 1.25em; + color: #7979797F; + } + } + + .column { + flex-grow: 1; + flex-shrink: 1; + + overflow: hidden; + white-space: nowrap; + + padding-right: .25em; + padding-left: .25em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + &:not(:last-of-type) { + border-right: 1px solid #161618; + } + + > a { + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; + } + } + } + + /* connect table */ + .table { + margin-left: -1.5em; /* the delete row */ + + .head { + margin-left: 1.5em; /* the delete row */ + .column.delete { + display: none; + } + } + + .column { + align-self: center; + .country, .icon-container { + align-self: center; + margin-right: 0.1em; + } + + + @mixin fixed-column($name, $width) { + &.#{$name} { + flex-grow: 0; + flex-shrink: 0; + + width: $width; + } + } + + @include fixed-column(delete, 1.5em); + @include fixed-column(password, 5em); + @include fixed-column(country-name, 7em); + @include fixed-column(clients, 4em); + @include fixed-column(connections, 6.5em); + + &.delete { + opacity: 0; + border-right: none; + border-bottom: none; + + text-align: center; + @include transition(opacity .25 ease-in-out); + + &:hover { + opacity: 1; + @include transition(opacity .25 ease-in-out); + } + } + + &.address { + flex-grow: 1; + flex-shrink: 1; + + width: 40%; + } + + &.name { + flex-grow: 1; + flex-shrink: 1; + + width: 60%; + } + } } } +} - .container-profile-manage { - display: flex; - flex-direction: row; - justify-content: stretch; +@media all and (max-width: 55rem) { + .modal .modal-connect { + min-width: calc(21.25em + 24px * 2)!important; + width: 1000em; /* allocate space */ - .container-select-profile { - flex-grow: 1; - flex-shrink: 1; + .container-address-password { + .container-password { + min-width: unset!important; + margin-left: 1em!important; + } } - .container-manage { - flex-grow: 0; - flex-shrink: 4; + .container-buttons { + justify-content: flex-end!important; - margin-left: 15px; + .button-toggle-last-servers { + display: none; + } } - } - .invalid-feedback { - position: absolute; + .container-profile-name { + flex-direction: column!important; + } + + .container-connect-input { + > .row { + > div { + margin-right: 0!important; + } + } + } + + .container-last-servers { + display: none; + } } } \ No newline at end of file diff --git a/shared/css/static/modal-group-assignment.scss b/shared/css/static/modal-group-assignment.scss new file mode 100644 index 00000000..b97e7090 --- /dev/null +++ b/shared/css/static/modal-group-assignment.scss @@ -0,0 +1,149 @@ +@import "mixin"; +@import "properties"; + +.modal-server-group-assignments { + @include user-select(none); + + min-width: 25em; + max-height: calc(100vh - 10rem); + + display: flex; + flex-direction: column; + justify-content: stretch; + + .group-assignment-list { + flex-grow: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + color: #999999; + + a { + flex-shrink: 0; + flex-grow: 0; + + .htmltag-client { + display: inline; + color: #999999; + } + } + + .group-list { + flex-shrink: 1; + flex-grow: 1; + + border: none; + border-radius: $border_radius_middle; + padding: 3px; + overflow-y: auto; + + @include chat-scrollbar-vertical(); + + .group-entry { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + height: max-content; + } + + .icon-container { + align-self: center; + margin-right: 4px; + margin-left: 2px; + margin-top: -2px; + } + + a { + align-self: center; + } + + .checkbox { + align-self: center; + height: 8px; + + margin-top: 1px; + margin-left: 1px; + display: block; + position: relative; + padding-left: 18px; + margin-bottom: 12px; + cursor: pointer; + font-size: 22px; + + /* Hide the browser's default checkbox */ + input { + position: absolute; + opacity: 0; + cursor: pointer; + display: none; + } + + .checkmark { + position: absolute; + top: 0; + left: 0; + height: 16px; + width: 16px; + background-color: #eee; + margin-right: 4px; + + &:after { + content: ""; + position: absolute; + display: none; + + left: 5px; + top: 1px; + width: 6px; + height: 12px; + border: solid white; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } + } + + &:hover:not(.disabled) input ~ .checkmark { + background-color: #ccc; + } + + input:checked ~ .checkmark { + background-color: #2196F3; + } + + input:checked ~ .checkmark:after { + display: block; + } + + &.disabled { + user-select: none; + pointer-events: none; + cursor: not-allowed; + + .checkmark { + background-color: #00000055; + &:after { + border-color: #00000055; + } + } + } + } + } + } + + .container-buttons { + flex-grow: 0; + flex-shrink: 0; + + padding-top: 1em; + + display: flex; + flex-direction: row; + justify-content: space-between; + } +} \ No newline at end of file diff --git a/shared/css/static/modal-keyselect.scss b/shared/css/static/modal-keyselect.scss new file mode 100644 index 00000000..f37be98a --- /dev/null +++ b/shared/css/static/modal-keyselect.scss @@ -0,0 +1,50 @@ +.modal-body.modal-keyselect { + width: max-content!important; + + .body { + display: flex; + flex-direction: column; + justify-content: flex-start; + + .container-select { + margin-top: .5em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + a { + align-self: center; + margin-right: .5em; + } + + .container-key { + background-color: #272626; + border-radius: 0.15em; + -webkit-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + -moz-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + + min-width: 12em; + height: 2em; + padding: 0 .5em; + + display: flex; + flex-direction: row; + justify-content: center; + } + } + } + + .container-buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + + margin-top: 1em; + + button { + margin-left: 1em; + } + } +} \ No newline at end of file diff --git a/shared/css/static/modal-permissions.scss b/shared/css/static/modal-permissions.scss index 651636f6..9d8ef1b7 100644 --- a/shared/css/static/modal-permissions.scss +++ b/shared/css/static/modal-permissions.scss @@ -1,30 +1,464 @@ -permission-editor { +@import "mixin"; +@import "properties"; + +.modal-body.modal-permission-editor { + padding: 0!important; + + display: flex; + flex-direction: row; + justify-content: stretch; + + width: 1000000em; /* get us some width */ + + @include user-select(none); + + .container { + display: flex; + flex-direction: row; + justify-content: stretch; + + padding: 0!important; + + width: 100%; + max-height: 90vh; + height: 100000000px; /* enforce max height */ + } + + .header { + height: 4em; + background-color: #19191b; + color: #e1e1e1; + + display: flex; + flex-direction: row; + justify-content: stretch; + + > .entry { + flex-grow: 1; + flex-shrink: 1; + + text-align: center; + + height: 100%; + + display: flex; + flex-direction: column; + justify-content: space-around; + } + } + + .container > .left, .container > .right { + max-height: 100%; + + display: flex; + flex-direction: column; + justify-content: stretch; + + > .header { + flex-shrink: 0; + flex-grow: 0; + } + + > .body { + flex-shrink: 1; + flex-grow: 1; + } + } + + .container >.right { + z-index: 2; /* because the left container overlaps the right container once */ + + width: 75%; + min-width: 30em; + + background-color: #303036; + + .header { + > .entry { + position: relative; + overflow: hidden; + + cursor: pointer; + padding-bottom: 2px; + + a { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &:hover { + border: none; + border-bottom: 2px solid #4e4e4e; + + padding-bottom: 0; + + &:before { + position: absolute; + content: ''; + + margin-right: -10em; + margin-left: -10em; + margin-bottom: -.2em; + bottom: 0; + + height: 100%; + width: calc(100% + 20em); + + box-shadow: inset 0px -1.2em 3em -20px #424242; + } + } + + &.selected { + border: none; + border-bottom: 2px solid #0073d4; + + padding-bottom: 0; + + &:before { + position: absolute; + content: ''; + + margin-right: -10em; + margin-left: -10em; + margin-bottom: -.2em; + bottom: 0; + + height: 100%; + width: calc(100% + 20em); + + box-shadow: inset 0px -1.2em 3em -20px #0073d4; + } + } + } + } + + .body { + display: flex; + flex-direction: column; + justify-content: stretch; + min-height: 16em; + + > .container { /* container permission editor */ + height: 100%; + width: 100%; + + flex-grow: 1; + flex-shrink: 1; + + min-width: 30em; + + .permission-editor { + display: flex; + flex-direction: column; + justify-content: stretch; + width: 100%; + padding: 5px; + } + } + } + } + + .container >.left { + width: 25%; + min-width: 10em; + + background-color: #222226; + + .header { + font-weight: bold; + + > .entry { + overflow: hidden; + + a { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + > * { + font-size: 1.5em; + } + } + + .body { + display: flex; + flex-direction: column; + justify-content: stretch; + min-height: 16em; + + .container { + flex-grow: 1; + flex-shrink: 1; + + flex-direction: column; + } + } + + + /* server group left list layout */ + .container-view-server-groups, .container-view-channel-groups, .container-view-channel-permissions, .container-view-client-channel-permissions, .container-view-client-channel-permissions { + height: 100%; + width: 100%; + + .list-groups, .list-channel, .list-clients { + color: #999999; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + overflow: auto; + @include chat-scrollbar-vertical(); + @include chat-scrollbar-horizontal(); + + width: 100%; + + flex-grow: 1; + flex-shrink: 1; + + .entries { + display: flex; + flex-direction: column; + justify-content: flex-start; + + height: max-content; + + min-width: 100%; + width: max-content; + + .group, .channel, .client { + padding-left: .25em; + + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + cursor: pointer; + + width: 100%; + + &:hover { + background-color: #28282c; + } + + &.selected { + background-color: #111111; + + &.client { + background-color: #1a1b1e; + } + } + + @include transition(background-color .25s ease-in-out); + + .icon-container, .icon { + align-self: center; + margin-right: .25em; + } + } + } + } + + .container-buttons { + position: relative; + + display: flex; + flex-direction: row; + justify-content: stretch; + + flex-grow: 0; + flex-shrink: 0; + + height: 2.5em; + width: 100%; + + .button { + display: flex; + flex-direction: row; + justify-content: space-around; + + flex-grow: 1; + flex-shrink: 1; + + cursor: pointer; + + background-color: #1b1b1b; + + &:hover { + background-color: #262626; + } + + &:disabled { + background-color: hsla(0, 0%, 9%, 1); + } + @include transition(background-color .25s ease-in-out); + + img { + width: 2.2em; + height: 2.2em; + + align-self: center; + } + } + } + } + + .container-view-server-groups { + position: relative; + + overflow: hidden; + + $animation_length: .3s; + .container-group-list { + flex-grow: 1; + flex-shrink: 1; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: flex; + flex-direction: column; + justify-content: stretch; + + &.hidden { + @include transform(translateX(-100%)); + } + @include transition($animation_length ease-in-out); + } + + .container-client-list { + flex-grow: 1; + flex-shrink: 1; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .container-current-group { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + background-color: #101012; + color: #999999; + padding-left: .25em; + + + height: 1.5em; + font-size: 1.125em; + + > .icon-container { + display: flex; + flex-direction: column; + justify-content: space-around; + + height: 100%; + margin-right: .25em; + } + + .name { + flex-grow: 1; + flex-shrink: 1; + + height: 1.5em; + } + } + + &.hidden { + @include transform(translateX(100%)); + } + @include transition($animation_length ease-in-out); + } + } + + .container-view-client-permissions, .container-view-client-channel-permissions { + .client-info { + width: 100%; + padding: .25em; + } + + hr { + border: none; + border-top: 2px solid #1e1e1e; + } + } + + .container-view-client-channel-permissions { + display: flex; + flex-direction: column; + justify-content: stretch; + + } + } + + .container-seperator { + width: 3px; + height: unset!important; + background-color: #222224!important; + } +} + +/* canvas permission editor */ +//TODO: Some styling needed +.container-permissions-canvas { display: flex; flex-direction: column; - - flex-grow: 1; - flex-shrink: 1; - - min-height: 0; -} - - -.container-permissions { - flex-grow: 1; - flex-shrink: 1; - - display: flex; - height: 100%; -} - -.permission-explorer { + justify-content: stretch; width: 100%; + .container-permissions { + padding: .5em; + overflow-x: auto; + overflow-y: hidden; + } + + .permission-explorer { + min-width: 750px; + } + + .switch { + width: 5em; + } + + .column-name { + align-self: center; + padding-left: 1em; + } + + .entry-editor-container { + @include chat-scrollbar-vertical(); + } +} + +/* html permission editor */ +.container-permissions-html { + flex-grow: 1; + flex-shrink: 1; + display: flex; flex-direction: column; justify-content: stretch; - user-select: none; + min-height: 10em; + width: 100%; .container-filter, .container-footer { flex-grow: 0; @@ -32,338 +466,248 @@ permission-editor { display: flex; flex-direction: row; - justify-content: stretch; - - .container-input { - flex-grow: 1; - flex-shrink: 1; - - margin-right: 10px; - } } - .container-permission-list { - flex-grow: 1; - flex-shrink: 1; + .container-filter { + justify-content: stretch; - display: flex; - flex-direction: column; - - .header { - border: solid 1px lightgray; - - display: flex; - flex-direction: row; - - .column-granted { - width: 75px + 15px !important; /* because of the scroll bar */ - } - - .column-name { - padding-left: 4px; - } - } - - /* legacy */ - .entries { - flex-grow: 1; - - overflow-y: scroll; - overflow-x: hidden; - - padding-left: 3px; /* because of the arrow */ - padding-right: 3px; /* because of the scroll bar */ - } - - .entry { + .button-toggle-clients { flex-grow: 0; flex-shrink: 0; - width: 100%; + width: 16em; + height: 2.5em; + + margin-right: 1em; + + align-self: flex-end; + margin-bottom: 1rem; + } + + .container-input { + flex-grow: 5; + flex-shrink: 1; + } + + .container-granted-switch { + margin-left: 1em; + position: relative; display: flex; flex-direction: row; - justify-content: stretch; + justify-content: flex-start; - .column-name { - flex-grow: 1; - flex-shrink: 1; - } + flex-shrink: 1; + flex-grow: 1; + color: #999999; + min-width: 8em; - .column-value, .column-granted { - flex-grow: 0; - flex-shrink: 0; - - width: 75px; - text-align: center; - align-self: center; - + > label { display: flex; flex-direction: row; - justify-content: space-around; + justify-content: flex-start; - input[type=number] { - width: 68px; - } - } + position: absolute; + bottom: 0; + left: 0; - .column-skip, .column-negate { - flex-grow: 0; - flex-shrink: 0; + pointer-events: all; + cursor: pointer; - width: 75px; - - text-align: center; - align-self: center; - - display: flex; - flex-direction: row; - justify-content: space-around; - } - - &.value-unset { - .column-value, .column-skip, .column-negate { - .checkbox, input { - visibility: hidden; - } - } - } - - &.grant-unset { - .column-granted { - .checkbox, input { - visibility: hidden; - } - } - } - - .checkbox { - display: flex; - flex-direction: row; - justify-content: center; - margin-bottom: 0!important; - } - - .form-group { - margin-bottom: 0px; - } - - &.group { - display: flex; - flex-direction: column; - - .group-entries { - padding-left: 30px; - } - } - - .bmd-form-group { - padding-top: 0; - } - - &.permission { - height: 33px; - - &:hover { - background: #00000011; - - - .checkbox { - .checkmark { - background-color: #bbb; - } - - &:hover input ~ .checkmark { - background-color: #aaa; - } - - input:checked ~ .checkmark { - background-color: #2196F3; - } - } + > * { + align-self: flex-end; } - .checkbox { - height: 16px; - width: 16px; - - display: block; - position: relative; - cursor: pointer; - font-size: 22px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - /* Hide the browser's default checkbox */ - input { - position: absolute; - opacity: 0; - cursor: pointer; - - left: 0; - top: 0; - } - - .checkmark { - position: absolute; - top: 0; - left: 0; - height: 16px; - width: 16px; - background-color: #eee; - - &:after { - content: ""; - position: absolute; - display: none; - - left: 6px; - top: 2px; - width: 5px; - height: 10px; - border: solid white; - border-width: 0 3px 3px 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - } - } - - &:hover input ~ .checkmark { - background-color: #ccc; - } - - input:checked ~ .checkmark { - background-color: #2196F3; - } - - input:checked ~ .checkmark:after { - display: block; - } + a { + padding-left: .25em; + font-size: 1.45em; } } } - - .arrow { - cursor: pointer; - margin-right: 5px; - } - } - - .entry-editor-container { - display: flex; - flex-direction: column; - justify-content: stretch; - overflow-y: auto; - - min-height: 100px; - min-width: 100px; - } - - .container-footer { - margin-top: 10px; - - justify-content: flex-end; } .container-mode { - display: flex; - flex-grow: 1; flex-shrink: 1; - min-height: 0; + min-height: 5em; - &.container-mode-unset { - background-color: lightgray; - } + &.container-mode-permissions { + .container-permission-list { + width: 100%; + color: #999999; - &.container-mode-no-permissions { - background-color: lightgray; - text-align: center; + display: flex; + flex-direction: column; + justify-content: stretch; - justify-content: space-around; - display: flex; - flex-direction: column; - } - } -} - -.tab-client, .tab-client-channel { - .client-select { - padding-bottom: 20px; /* for the error message */ - - .invalid-feedback { - position: absolute; - } - } -} - -.tab-client-channel { - .container-client-channel { - display: flex; - flex-direction: column; - justify-content: stretch; - - .list-channel { - flex-grow: 1; - flex-shrink: 1; - - width: 100%; - } - } -} - -.layout-group-server { - .list-group-server { } - - .permission-explorer { - flex-grow: 70; - } - - .container-clients { - display: flex; - flex-direction: column; - justify-content: stretch; - - width: 175px; - min-width: 125px; - - - .container-filter { - flex-grow: 0; - flex-shrink: 0; - } - - .container-list-clients { - flex-grow: 1; - flex-shrink: 1; - - overflow-x: auto; - overflow-y: auto; - - border: grey solid 1px; - position: relative; - - width: 100%; - - .list-clients { - width: fit-content; + min-height: 5em; .entry { - white-space : nowrap; - cursor: pointer; - padding: 2px; width: 100%; - &.selected { - background: blue; - color: whitesmoke; + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 2em; + border: none; + border-bottom: 1px solid #1e2025; + + color: #535455; + + @mixin fixed-column($name, $width) { + .column-#{$name} { + display: flex; + flex-direction: row; + justify-content: stretch; + + flex-grow: 0; + flex-shrink: 0; + + width: $width; + + align-items: center; + + padding-left: 1em; + + border: none; + border-right: 1px solid #1e2025; + } + } + + @include fixed-column(name, 6em); + @include fixed-column(value, 6em); + @include fixed-column(skip, 5em); + @include fixed-column(negate, 5em); + @include fixed-column(granted, 6em); + + .column-name { + flex-grow: 1; + flex-shrink: 1; + + .arrow { + cursor: pointer; + border-color: #e1e1e1; + } + + .group-name { + margin-left: .5em; + } + } + + .column-granted { + border-right: none; + } + + + &.active { + color: #e1e1e1; + } + + &.group { + color: #e1e1e1; + font-weight: bold; + } + + input { + color: #e1e1e1; + + outline: none; + background: transparent; + border: none; + + height: 1.5em; + width: 5em; /* the column width minus one */ + + /* fix the column padding */ + padding-left: 1em; + margin-left: -.5em; /* have a bit of space on both sides */ + + border-bottom: 2px solid transparent; + + &:focus { + border-bottom-color: #3f7dbf; + } + @include transition(border-bottom-color $button_hover_animation_time ease-in-out); + } + } + + .body { + flex-grow: 1; + flex-shrink: 1; + + min-height: 6em; /* TODO: Width */ + + display: flex; + flex-direction: column; + justify-content: stretch; + + overflow-y: scroll; + overflow-x: auto; + + @include chat-scrollbar-vertical(); + @include chat-scrollbar-horizontal(); + + .entry { + &.even { + background-color: #25252a; + } + &:hover { + background-color: #343a47; + } + /* We cant use this effect here because the odd/even effect would be a bit crazy then */ + //@include transition(background-color $button_hover_animation_time ease-in-out); + } + } + + .header { + background-color: unset; + + color: #e1e1e1; + font-weight: bold; + + .column-granted { + margin-right: .5em; /* scroll bar */ + -moz-margin-end: 12px; /* moz scroll bar */ } } } } + + &.container-mode-no-permissions { + display: flex; + flex-direction: column; + justify-content: space-around; + + text-align: center; + font-size: 2em; + + color: #222226; + } + } + + .container-footer { + justify-content: flex-end; + margin-top: .5em; + } +} + +.modal-group-add { + display: flex; + flex-direction: column; + justify-content: flex-start; + + .buttons { + display: flex; + flex-direction: row; + justify-content: space-between; + + button { + min-width: 6em; + } } } \ No newline at end of file diff --git a/shared/css/static/modal-server.scss b/shared/css/static/modal-server.scss index a5be004d..62a68329 100644 --- a/shared/css/static/modal-server.scss +++ b/shared/css/static/modal-server.scss @@ -1,156 +1,1149 @@ -$required_notab_height: 950px; +@import "mixin"; +@import "properties"; -.container-server-settings-general, .tab-server-settings-general { +.modal-body.modal-server-edit { + display: flex; flex-direction: column; + justify-content: stretch; - .container-server-settings-slots { - display: flex; - flex-direction: row; - justify-content: stretch; + max-height: calc(100vh - 10em); + padding: 1em!important; - margin-right: 0; + min-width: 35em!important; + width: 60em; /* recommend width */ + + @include user-select(none); + + input, textarea, select { + width: 100%; + } + + select { margin-left: 0; - - .form-group:not(:first-of-type) { - margin-left: 10px; - - flex-grow: 30; - flex-shrink: 30; - } - - .form-group:first-of-type { - flex-grow: 70; - flex-shrink: 70; - } + height: 2.5em; } - .container-name-icon { + textarea { + padding: .5em; + } + + label { display: flex; flex-direction: row; justify-content: stretch; - .container-name { - flex-grow: 1; - flex-shrink: 1; + /* total height 2.5em */ + margin-top: .5em; + margin-bottom: .5em; + height: 1.5em; + + cursor: pointer; + + * { + align-self: center; } - .container-icon { - flex-grow: 0; - flex-shrink: 0; + a { + margin-left: .5em; + margin-right: .5em; } - } - .container-icon { - width: 30px; + .form-group { + margin: -.5em 0!important; - margin-left: 10px; + padding: 0!important; - .button-select-icon { - left: 0; - right: 0; - top: 0; - bottom: 0; - - position: absolute; - - .icon-node { - cursor: pointer; - - height: 100%; - width: 100%; - - &:hover { - background-color: #00000011; - } - - > div { - vertical-align: middle; - text-align: center; - } + input { + height: 1.5em!important; } } } -} -.tab-server-settings-general { - padding: 5px; - display: none; -} + /* radio buttons */ + $icon_width: 1.7em; /* equal to the label height */ -.tab-tag-server-settings-general { - display: none!important; -} + @mixin tooltip-size($lines, $line_length) { + $tooltip_height: $lines * 1.6; + $tooltip_width: $line_length + 1; + .tooltip { + top: -($tooltip_height + .6em); + left: ($icon_width - $tooltip_width) / 2; -.container-server-settings-general { - display: flex; - flex-shrink: 0; -} + height: $tooltip_height * 1em; + width: $tooltip_width * 1em; + + + &:before { + left: $tooltip_width / 2 - .5em; + } + } + } + + .input-boxed { + position: relative; + + flex-grow: 1; + flex-shrink: 1; + + min-width: 4em; -@media (max-height: $required_notab_height) { - .tab-server-settings-general { display: flex; + flex-direction: row; + justify-content: stretch; + + .container-tooltip { + flex-shrink: 0; + flex-grow: 0; + + position: relative; + width: $icon_width; + + display: flex; + flex-direction: column; + justify-content: center; + + img { + height: 1em; + width: 1em; + + align-self: center; + font-size: 1.2em; + } + + .tooltip { + /* only a container for the actual JS generated tooltip */ + display: none; + } + } } - .tab-tag-server-settings-general { - display: inline-block!important; - } + .container-general { + display: flex; + flex-direction: column; + justify-content: stretch; - .container-server-settings-general { - display: none; - } -} + flex-shrink: 0; + flex-grow: 0; -.container-server-settings-host { - padding: 5px; + > div:not(:first-of-type) { + flex-grow: 0; + flex-shrink: 0; - .properties-hostbanner, .properties-hostbutton { - .form-row { - margin-left: 5px; - margin-right: 5px; + margin-top: 1em; + } + + .container-name-icon { + flex-grow: 0; + flex-shrink: 0; display: flex; flex-direction: row; justify-content: stretch; - > .form-group { - flex-grow: 1; - flex-shrink: 1; + .container-icon-select { + position: relative; + + height: 2.5em; + border-radius: .2em; + + margin-left: 1em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + cursor: pointer; + background-color: #121213; + border: 1px solid #0d0d0d; + + .icon-preview { + height: 100%; + width: 3em; + + border: none; + border-right: 1px solid #0d0d0d; + + display: flex; + flex-direction: column; + justify-content: space-around; + + > div { + align-self: center; + } + + @include transition(border-color $button_hover_animation_time ease-in-out); + } + + .container-dropdown { + position: relative; + cursor: pointer; + + display: flex; + flex-direction: column; + justify-content: space-around; + + height: 100%; + width: 1.5em; + + .button { + text-align: center; + + .arrow { + border-color: #999999; + } + } + + .dropdown { + display: none; + position: absolute; + width: max-content; + + top: calc(2.5em - 1px); + + flex-direction: column; + justify-content: flex-start; + + background-color: #121213; + border: 1px solid #0d0d0d; + border-radius: .2em 0 .2em .2em; + + right: -1px; + + z-index: 2; + + .entry { + padding: .5em; + + &:not(:last-of-type) { + border: none; + border-bottom: 1px solid #0d0d0d; + } + + &:hover { + background-color: #17171a; + } + } + } + + &:hover { + border-bottom-right-radius: 0; + .dropdown { + display: flex; + } + } + } + + &:hover { + background-color: #17171a; + border-color: hsla(0, 0%, 20%, 1); + + .icon-preview { + border-color: hsla(0, 0%, 20%, 1); + } + } + + @include transition(border-color $button_hover_animation_time ease-in-out); + } + } + + .container-slots { + display: flex; + flex-direction: row; + justify-content: stretch; + + > div { + width: 50%; + + &:not(:first-child) { + margin-left: 1em; + } + } + } + + .container-welcome-message { + position: relative; + + flex-grow: 1!important; + flex-shrink: 1!important; + + min-height: 5em; + max-height: 22.5em; + + border-radius: .2em; + border: 1px solid #111112; + + overflow: hidden; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .toolbar { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + width: 100%; + height: 2.5em; + + background-color: #17171a; + font-size: .8em; + + padding: .25em; + + .button { + cursor: pointer; + + padding: .5em; + &:not(:first-child) { + margin-left: .25em; + } + + border-radius: .2em; + border: 1px solid #111112; + + background-color: #121213; + + height: 2em; + width: 2em; + + display: flex; + flex-direction: column; + justify-content: center; + + text-align: center; + align-self: center; + + &.button-bold { + font-weight: bold; + } + + &.button-italic { + font-style: italic; + } + + &.button-underline { + text-decoration: underline; + } + + &.button-color { + input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + } + } + + &:hover { + background-color: #0f0f0f; + @include transition(background-color $button_hover_animation_time); + } + } } - > .form-group:not(:first-of-type) { - margin-left: 10px; + > .input-boxed { + flex-shrink: 1; + flex-grow: 1; + + min-height: 2.5em; + height: 5em; + max-height: 20em; + + border: none; + border-radius: 0; + border-top: 1px solid #111112; + + + overflow-x: hidden;; + overflow-y: auto; + + resize: vertical; + + @include chat-scrollbar-vertical(); + } + + &:focus-within { + background-color: #131b22; + //border-color: #284262; } } } - .virtualserver_hostbanner_gfx_interval { - height: calc(2.4375rem + 2px); - } -} + .container-categories { + margin-top: 1em; -.container-server-settings-file-transfer, .container-server-settings-anti-flood, .container-server-settings-security { - padding: 5px; -} - -.container-server-settings-misc { - padding: 5px; - - .container-complains { display: flex; - flex-direction: row; + flex-direction: column; justify-content: stretch; - > div { - flex-grow: 1; - flex-shrink: 1; + min-height: 14em; + + border-radius: .2em; + border: 1px solid #111112; + + background-color: #17171a; + + fieldset { + padding: 0; + width: 100%; } - > div:not(:first-of-type) { - padding-left: 10px; + label { + display: flex; + flex-direction: row; + justify-content: stretch; + + /* total height 2.5em */ + margin-top: .5em; + margin-bottom: .5em; + height: 1.5em; + + cursor: pointer; + + * { + align-self: center; + } + + a { + margin-left: .5em; + margin-right: .5em; + } + + .form-group { + margin: -.5em 0!important; + + padding: 0!important; + + input { + height: 1.5em!important; + } + } + } + + .input-boxed:not(textarea), input, select { + height: 1.7em; + } + + textarea { + height: 3.4em; /* double the input height */ + max-height: 17em; + min-height: 1.7em; + } + + .categories { + height: 2.5em; + + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + border-bottom: 1px solid #1d1d1d; + + .entry { + padding: .5em; + + text-align: center; + + flex-grow: 1; + flex-shrink: 1; + + cursor: pointer; + + &:hover { + color: #b6c4d6; + } + + &.selected { + border-bottom: 3px solid #245184; + margin-bottom: -1px; + + color: #245184; + } + + @include transition(color $button_hover_animation_time, border-bottom-color $button_hover_animation_time); + } + } + + .bodies { + position: relative; + + flex-shrink: 1; + flex-grow: 1; + display: flex; + justify-content: stretch; + + min-height: 10em; + height: 30em; + + .body { + position: absolute; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + padding: .5em; + + display: flex; + justify-content: stretch; + + overflow: auto; /* else the tooltip will trigger the scrollbar */ + @include chat-scrollbar-vertical(); + + &.hidden { + display: none; + } + + .header { + flex-shrink: 0; + flex-grow: 0; + + text-align: center; + color: #548abc; + } + + .content { + flex-grow: 1; + flex-shrink: 1; + } + + &.container-host { + flex-direction: column; + + .container-top, .container-bottom { + flex-grow: 1; + flex-shrink: 1; + min-height: min-content; + + display: flex; + flex-direction: row; + justify-content: stretch; + } + + .container-left, .container-right { + flex-grow: 1; + flex-shrink: 1; + + min-width: 8em; + width: 50%; + } + + .container-top { + flex-direction: column; + flex-grow: 0; + + padding-bottom: .5em; + + border-bottom: 2px solid #111113; + } + + .container-bottom .container-left { + padding-top: .5em; + padding-right: .5em; + + border-right: 2px solid #111113; + } + + .container-bottom .container-right { + padding-top: .5em; + padding-left: .5em; + } + + .container-host-message { + .container-message, .container-mode { + padding-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-title { + width: 8em; + flex-grow: 0; + flex-shrink: 0; + + height: 1.7em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + a { + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + } + } + + textarea, select { + flex-grow: 1; + flex-shrink: 1; + } + + textarea { + resize: vertical; + } + } + } + + .container-host-banner { + .container-url, .container-gfx-url, .container-refresh, .container-resize { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 1.7em; + + a { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + width: 6em; + } + + &.container-refresh, &.container-resize { + a { + width: 9em; + } + } + } + + .container-gfx-preview { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-title { + width: 8em; + flex-grow: 0; + flex-shrink: 0; + + height: 1.7em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + a { + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .container-image { + flex-grow: 1; + flex-shrink: 1; + + min-width: 0; + max-height: 6em; + + display: flex; + flex-direction: column; + justify-content: center; + + > img { + flex-grow: 0; + flex-shrink: 0; + + max-height: 100%; + max-width: 100%; + + object-fit: contain; + } + } + } + } + + .container-host-button { + .container-url, .container-gfx-url, .container-tooltip-button { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 1.7em; + + > a { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + width: 6em; + } + } + + .container-gfx-preview { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: space-between; + + .container-title { + width: 8em; + flex-grow: 0; + flex-shrink: 0; + + height: 1.7em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + a { + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .container-image { + flex-grow: 0; + flex-shrink: 0; + + height: 2em; + width: 2em; + + display: flex; + flex-direction: column; + justify-content: center; + + > img { + flex-grow: 0; + flex-shrink: 0; + + height: 100%; + width: 100%; + } + } + } + } + } + + &.container-network { + flex-direction: column; + + .container-top, .container-bottom { + flex-grow: 1; + flex-shrink: 1; + min-height: min-content; + + display: flex; + flex-direction: row; + justify-content: stretch; + } + + .container-left, .container-right { + flex-grow: 1; + flex-shrink: 1; + + min-width: 8em; + width: 50%; + } + + .container-top { + flex-direction: column; + flex-grow: 0; + + padding-bottom: .5em; + + border-bottom: 2px solid #111113; + } + + .container-bottom .container-left { + padding-top: .5em; + padding-right: .5em; + + border-right: 2px solid #111113; + } + + .container-bottom .container-right { + padding-top: .5em; + padding-left: .5em; + } + + .container-binding { + .container-host, .container-port { + padding-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + a { + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + flex-grow: 0; + flex-shrink: 0; + + width: 6em; + } + } + + .container-weblist { + padding-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + } + } + + .container-download, .container-upload { + .container-bandwidth, .container-quota, .container-used-quota { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + a { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + width: 12em; + + &.unit { + width: 4em; + text-align: right; + } + } + + .value { + flex-grow: 1; + flex-shrink: 1; + } + } + } + } + + &.container-security { + flex-direction: column; + + .container-top, .container-bottom { + flex-grow: 0; + flex-shrink: 0; + + min-height: min-content; + + display: flex; + flex-direction: column; + justify-content: stretch; + } + + .container-top { + border-bottom: 2px solid #111113; + + padding-bottom: .5em; + } + + .container-bottom { + padding-top: .5em; + } + + .container-antiflood { + .container-reduce, .container-block-commands, .container-block-ip { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 1.7em; + + a { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + width: 17em; + } + } + } + + .container-security { + .container-encryption, .container-security-level, .container-security-level-description { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 1.7em; + + a { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + width: 17em; + } + + &.container-refresh, &.container-resize { + a { + width: 9em; + } + } + } + + .container-description { + display: flex; + flex-direction: column; + justify-content: flex-start; + + a { + flex-grow: 0; + flex-shrink: 0; + + font-size: .85em; + color: #3c3c3c; + } + } + } + } + + &.container-messages { + flex-direction: column; + + .container-top, .container-bottom { + flex-grow: 0; + flex-shrink: 0; + + min-height: min-content; + + display: flex; + flex-direction: column; + justify-content: stretch; + } + + .container-top { + border-bottom: 2px solid #111113; + + padding-bottom: .5em; + } + + .container-bottom { + padding-top: .5em; + } + + .container-channel { + .container-description, .container-topic { + padding-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-title { + width: 8em; + flex-grow: 0; + flex-shrink: 0; + + height: 1.7em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + a { + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + } + } + + textarea, select { + flex-grow: 1; + flex-shrink: 1; + } + + textarea { + resize: vertical; + } + } + } + + .container-client { + .container-description { + padding-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + + a { + width: 8em; + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } + + &.container-misc { + flex-direction: column; + + .container-top, .container-bottom { + flex-grow: 1; + flex-shrink: 1; + min-height: min-content; + + display: flex; + flex-direction: row; + justify-content: stretch; + } + + .container-left, .container-right { + flex-grow: 1; + flex-shrink: 1; + + min-width: 8em; + width: 50%; + } + + .container-top { + flex-direction: column; + flex-grow: 0; + + padding-bottom: .5em; + + border-bottom: 2px solid #111113; + } + + .container-bottom .container-left { + padding-top: .5em; + padding-right: .5em; + + border-right: 2px solid #111113; + } + + .container-bottom .container-right { + padding-top: .5em; + padding-left: .5em; + } + + .container-default-groups { + .container-default-group { + padding-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + a { + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + flex-grow: 0; + flex-shrink: 0; + + width: 12em; + } + + select { + flex-grow: 1; + flex-shrink: 1; + } + } + } + + .container-complains { + .container-ban-threshold, .container-ban-time, .container-cooldown { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + a { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + width: 12em; + } + } + } + + .container-others { + .container-silence, .container-dim, .container-gfx-url { + margin-top: 1em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + > a { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + + width: 12em; + } + } + } + } + } } } -} -.container-server-settings-messages { - padding: 5px; + .container-buttons { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: flex-end; + + margin-top: 1em; + + button { + margin-left: 1em; + } + } } \ No newline at end of file diff --git a/shared/css/static/modal-settings.scss b/shared/css/static/modal-settings.scss index 280d9f1c..c8f25d37 100644 --- a/shared/css/static/modal-settings.scss +++ b/shared/css/static/modal-settings.scss @@ -1,963 +1,1413 @@ -$small_device: 800px; /* tested out via audio tab */ +@import "properties"; +@import "mixin"; +$color_list_border: #161616; +$color_list_background: #28292b; +$color_list_hover: #2c2d2f; +$color_list_selected: #1a1a1b; + +.modal-body.modal-settings { + padding: 0!important; -.modal .settings_audio { display: flex; flex-direction: column; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + justify-content: stretch; - margin: 3px; + @include user-select(none); + width: 10000em; /* allocate some space */ - > div { - margin: 2px; - } + .inner-container { + flex-grow: 1; + flex-shrink: 1; - a { - align-self: center; - } + height: 50em; + max-height: calc(100vh - 10em); - .settings-device-error { - display: none; - } - - .group_box { display: flex; - flex-direction: column; - flex-shrink: 0; - - &.sound { - flex-shrink: 1; - - .content { - display: block; - flex-direction: column; - } - } - } - - .settings-device { - display: flex; - flex-direction: row; - justify-items: stretch; - - .settings-device-select { - flex-grow: 1; - flex-shrink: 1; - - display: flex; - flex-direction: row; - justify-content: stretch; - - margin-right: 5px; - - > div { - flex-grow: 1; - flex-shrink: 1; - } - - select { - flex-grow: 1; - margin-left: 5px; - width: 100%; - } - } - } - - .settings-speaker .container-master-volume { - display: flex; - flex-direction: row; + flex-direction: row !important; justify-content: stretch; - .key { - flex-grow: 0; - max-width: 150px; - margin-right: 5px; - } - - .value { - flex-grow: 1; - flex-shrink: 1; - + > .left, > .right { display: flex; - flex-direction: row; + flex-direction: column; justify-content: stretch; - a { + padding: .5em; + + overflow: auto; + + @include chat-scrollbar-horizontal(); + @include chat-scrollbar-vertical(); + } + + .container-seperator { + height: unset !important; + background-color: #222224!important; + } + + > .left { + width: 25%; + min-width: 10em; + + height: 100%; + + justify-content: flex-start; + + background-color: #212125; + + .entry { flex-grow: 0; flex-shrink: 0; - margin-left: 5px; - } - - /* added by materialize */ - span { - min-width: 100px; - - flex-grow: 1; - flex-shrink: 1; - - padding: 0; - } - - input { - width: 100%; - } - } - } - - .settings-vad-container { - display: flex; - margin-top: 5px; - min-height: 150px; - flex-direction: column; - width: 100%; - - > div { - width: 100%; - - flex-grow: 1; - flex-shrink: 1; - } - - /* for "normal" devices */ - @media (min-width: $small_device) { - flex-direction: row; - - > div { - width: unset; - } - } - - .group_box { - min-width: 250px; - } - - .content { - display: flex; - flex-direction: column; - justify-content: space-around; - } - - fieldset { - input { - vertical-align: text-bottom; - } - } - - .settings-vad { - display: flex; - flex-direction: column; - } - - .settings-vad-impl { - .setting-vad-ppt { - @media (min-width: $small_device) { - margin-bottom: -35px; - } - } - - display: flex; - justify-content: space-around; - padding: 5px; - - > div { - align-self: center; - } - - .settings-vad-impl-entry { - display: none; - } - - .setting-vad-vad { - .vad_vad_bar { - position: relative; - width: 100%; - height: 20px; - - background-image: linear-gradient(to right, green, yellow, red); - background-repeat: no-repeat; - background-size: 100%; - background-position: 0 100%; - - display: flex; - flex-direction: column; - - .bmd-form-group { - display: flex; - padding: 0px; - } - - .container-hider { - position: absolute; - height: 100%; - width: 100%; - - display: flex; - flex-direction: row-reverse; - - .hider { - width: 50%; - height: 100%; - - background-color: grey; - } - } - } - - /* The slider itself */ - .vad_vad_slider { - margin: 0; - background-color: gray; - -webkit-appearance: none; /* Override default CSS styles */ - appearance: none; - width: 100%; - height: 100%; - outline: none; - opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */ - -webkit-transition: .2s; /* 0.2 seconds transition on hover */ - transition: opacity .2s; - } - - /* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */ - .vad_vad_slider::-webkit-slider-thumb { - -webkit-appearance: none; /* Override default look */ - appearance: none; - width: 2px; /* Set a specific slider handle width */ - height: 20px; /* Slider handle height */ - background: #000000FF; /* Green background */ - cursor: pointer; /* Cursor on hover */ - } - - .vad_vad_slider::-moz-range-thumb { - width: 2px; /* Set a specific slider handle width */ - height: 100%; /* Slider handle height */ - background: #000000FF; /* Green background */ - cursor: pointer; /* Cursor on hover */ - } - } - } - - .property { - display: flex; - flex-direction: row; - - .key { - width: 120px; - margin-right: 5px; - } - - &.ppt-key { - .key { - align-self: center; - } - - button { - min-width: 100px; - } - } - - &.ppt-delay { - margin-top: 5px; - - .value { - display: inline-block; - position: relative; - - &:after { - position: absolute; - top: 1px; - right: .5em; - transition: all .05s ease-in-out; - } - - &:hover::after { - right: 1.5em; - } - - &::after { - content: 'ms'; - } - } - } - } - } - - .sound-settings { - display: flex; - flex-direction: column; - justify-content: stretch; - - .property { - width: 100%; - flex-shrink: 0; - flex-grow: 0; - - display: flex; - flex-direction: row; - justify-content: stretch; - - .key { - /* - width: 250px; - - &.muted-sounds { - width: 230px; - } - */ - margin-right: 10px; - } - - .value { display: flex; flex-direction: row; justify-content: stretch; - flex-shrink: 1; - flex-grow: 1; + padding: .5em; - &.master-volume { - input { - width: 100%; - } + border-radius: $border_radius_middle; - a { - margin-left: 5px; - width: 50px; - text-align: right; - } + color: #e0e0e0; + + &.group { + font-size: 1.3em; + text-transform: uppercase; + + color: #565656; } - .bmd-form-group { - padding: 0; - - label { - margin: 0; - top: -7px; - } - - .bmd-switch-track { - top: 0; - } - } - } - } - - .sound-list { - margin-top: 5px; - - display: flex; - flex-grow: 1; - flex-direction: column; - justify-content: stretch; - - .column { - &.sound-name { - width: calc(100% - 150px); - } - - &.sound-activated { - width: 150px; - flex-grow: 0; - - .bmd-form-group { - padding: 0; - - label { - margin: 0 0 0 75px; - top: -7px; - } - - .bmd-switch-track { - top: 0; - } - } - } - } - - .sound-list-header { - flex-grow: 0; - flex-shrink: 0; - display: flex; - flex-direction: row; - align-items: center; - - .column { - border: 1px solid lightgray; - text-align: center; - } - } - - .sound-list-entries-container { - flex-grow: 1; - display: flex; - flex-direction: column; - justify-content: start; - overflow-y: auto; - - min-height: 400px; - max-height: 400px; - - .entry { - display: flex; - flex-direction: row; - - .column { - margin-left: 2px; - } - + &:not(.group) { cursor: pointer; + &:hover { + background-color: #2c2d2f; + } + &.selected { - background-color: blue; - } - - &:hover { - background-color: #00000022; - } - - .button-playback:hover { - background-color: #00000022; + background-color: #1a1a1b; } } + } + } - &.scrollbar { - .column { - &.sound-name { - width: calc(100% - 150px + 60px) + > .right { + width: 75%; + min-width: 12em; + + position: relative; + + background-color: #2f2f35; + + > .container { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + + display: flex; + flex-direction: column; + justify-content: stretch; + + min-height: min-content; + min-width: 30em; + + @include chat-scrollbar-horizontal(); + + &.general-chat, &.general-application, &.audio-sounds { + label { + display: flex; + flex-direction: row; + justify-content: flex-start; + + > * { + align-self: center; + } + + a { + margin-left: .5em; } } } - } - } - /* - .sound-list-filter { - margin-top: 3px; + &.general-application { + .container-font-size { + display: flex; + flex-direction: row; + justify-content: stretch; - display: flex; - flex-direction: row; - justify-content: stretch; + a { + align-self: center; + margin-right: 1em; + } - input { - flex-grow: 1; - flex-shrink: 1; - } - - a { - margin-right: 8px; - } - } - */ - } -} - -.modal .container-tabname-translations { - display: flex; - flex-direction: row; - - .country { - align-self: center; - margin-left: 3px; - } -} - -.modal .settings-translations { - margin: 5px; - - .setting-list { - user-select: none; - - display: flex; - flex-direction: column; - - .list { - display: flex; - flex-direction: column; - justify-content: start; - - overflow-y: auto; - - border: solid 1px lightgray; - padding: 2px; - background: #33333318; - - height: 50%; - min-height: 50%; - max-height: 50%; - - .entry { - display: flex; - flex-direction: row; - justify-content: stretch; - - .default { - } - - .name { - flex-grow: 1; - flex-shrink: 1; - } - - &.translation:not(.default) { - padding-left: 15px; - } - - &.translation { - cursor: pointer; - } - - &.repository { - .name { - font-weight: bold; + select { + height: 1.7em; + width: 12em; + } } } - &.selected { - background: #0000FF77; - } - - - .button { - cursor: pointer; - - &:hover { - background-color: #00000010; - } - } - - .country { - align-self: center; - margin-right: 5px; - margin-left: 5px; - } - } - } - - .management { - width: 100%; - - display: flex; - flex-direction: row; - justify-content: stretch; - - margin-top: 5px; - float: right; - - .space { - flex-grow: 1; - } - } - - .restart-note { - width: 100%; - - display: flex; - flex-direction: row; - justify-content: space-between; - - margin-top: 5px; - - p { - margin: 0; - } - } - } -} - -/* The info modal for the translations */ -.entry-info-container { - display: flex; - flex-direction: column; - - .property { - display: flex; - flex-direction: row; - justify-content: stretch; - - .key { - width: 100px; - } - - .value { - display: flex; - flex-direction: row; - flex-grow: 1; - } - - &.property-repository { - p { - margin: 0; - } - - .button { - cursor: pointer; - - display: flex; - flex-direction: column; - justify-content: center; - - margin-right: 5px; - - &:hover { - background: #00000011; - } - } - } - - &.property-contributors { - .value { - display: flex; - flex-direction: column; - } - - .contributor { - display: block; - } - } - } -} - -.modal .settings-general { - padding: 5px; - - .not-connected { - /* display: none; */ - } - - .connected { - display: flex; - flex-direction: column; - justify-content: stretch; - - .connected-info { - color: green; - } - - .property { - display: flex; - flex-direction: row; - justify-content: stretch; - - .key { - flex-grow: 0; - flex-shrink: 0; - width: 140px; - } - - &.premium { - .premium { - color: green; - } - - .non-premium { - color: red; - } - } - } - - .container-info-action { - display: flex; - flex-direction: row; - justify-content: stretch; - - .divider { - flex-grow: 0; - flex-shrink: 0; - width: 2px; - - background: lightgray; - } - - .container-info, .container-actions { - flex-grow: 1; - flex-shrink: 1; - width: 50%; - - display: flex; - flex-direction: column; - justify-content: stretch; - } - - .container-actions { - display: flex; - flex-direction: column; - justify-content: center; - - button { - width: 150px; - - align-self: center; - } - } - } - } -} - -.modal .settings-profiles { - margin: 5px; - - > div:not(:first-of-type) { - margin-top: 5px; - } - - .profile-status-container { - display: flex; - flex-direction: row; - - justify-content: space-between; - } - - .error-message { - color: red; - } - - .profile-list { - user-select: none; - - display: flex; - flex-direction: column; - - .list { - display: flex; - flex-direction: column; - justify-content: start; - - overflow-y: auto; - - border: solid 1px lightgray; - padding: 2px; - background: #33333318; - - height: 50%; - min-height: 50%; - max-height: 50%; - - .entry { - display: flex; - flex-direction: row; - justify-content: stretch; - cursor: pointer; - - &.default { - .name { - font-weight: bold; - } - } - - .name { - flex-grow: 1; - flex-shrink: 1; - } - - &.selected { - background: #0000FF77; - } - - - .button { - cursor: pointer; - - &:hover { - background-color: #00000010; - } - } - } - } - - .management { - width: 100%; - - display: flex; - flex-direction: row; - justify-content: stretch; - - margin-top: 5px; - float: right; - - .space { - flex-grow: 1; - } - - button:not(:first-of-type) { - margin-left: 5px; - } - } - } - - .general-settings { - display: flex; - flex-direction: column; - justify-content: start; - - .setting { - &:not(:first-of-type) { - margin-top: 5px; - } - - display: flex; - flex-direction: row; - justify-content: stretch; - - .key { - flex-grow: 0; - flex-shrink: 0; - width: 200px; - } - - input, div { - flex-grow: 1; - flex-shrink: 1; - } - } - } - - .identity-settings { - display: none; - - &.active { - display: block; - } - - &.identity-settings-teaforo { - /* - .connected, .disconnected { - display: none - } - */ - } - - &.identity-settings-teamspeak { - .level { - padding-right: 5px; - padding-left: 5px; - - display: flex; - flex-direction: row; - justify-content: stretch; - - .container-input { - flex-grow: 1; - flex-shrink: 1; - + &.general-language { display: flex; + flex-direction: column; + justify-content: stretch; + + .container-selected { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + a, div { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + a { + align-self: center; + padding-right: .75em; + } + + div { + display: flex; + flex-direction: row; + justify-content: flex-start; + + > .country { + align-self: center; + margin-right: .3em; + } + } + } + + .container-list { + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + min-height: 6em; + + background-color: $color_list_background; + border: 1px $color_list_border solid; + + border-radius: $border_radius_large; + + .entries { + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + overflow-x: hidden; + overflow-y: auto; + + padding-top: .5em; + padding-bottom: .5em; + + @include chat-scrollbar-vertical(); + + .entry { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + height: 1.5em; + + padding-left: .5em; + padding-right: .5em; + + cursor: pointer; + + &.translation { + padding-left: 1.5em; + } + + .country { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + margin-right: .25em; + margin-bottom: .1em; + } + + .name { + flex-grow: 1; + flex-shrink: 1; + + + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .button { + display: flex; + flex-direction: column; + justify-content: space-around; + + width: 1.2em; + height: 1.2em; + + cursor: pointer; + + align-self: center; + + border-radius: $border_radius_middle; + + > div { + align-self: center; + } + + &:hover { + background-color: #3c3d40; + } + + > *:not(.spacer) { + flex-grow: 0; + flex-shrink: 1; + } + + .spacer { + flex-grow: 1; + flex-shrink: 1; + + width: 0; + } + + @include transition(background-color $button_hover_animation_time); + } + + &:hover { + background-color: $color_list_hover; + } + + &.selected { + background-color: $color_list_selected; + } + } + } + + .buttons { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: space-between; + + background-color: #242527; + + padding: .5em; + + border: none; + border-top: 1px solid #161616; + } + } + } + + &.audio-microphone, &.audio-speaker, &.audio-sounds, &.identity-forum { flex-direction: row; justify-content: stretch; - margin-right: 10px; - } - } + .left, .right, .fill { + flex-grow: 1; + flex-shrink: 1; + + width: calc(50% - .5em); /* the .5em for the padding/margin */ + &.fill { + width: calc(100% - 1em); + } + + display: flex; + flex-direction: column; + justify-content: stretch; + + .header { + height: 3em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + padding-bottom: .5em; + + a { + flex-grow: 1; + flex-shrink: 1; + + align-self: flex-end; + + font-weight: bold; + color: #e0e0e0; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .btn { + flex-shrink: 0; + flex-grow: 0; + + margin-left: 1em; + min-width: 8em; + } + } + } + + .container-activity-bar { + $bar_height: 1em; + + $thumb_width: .6em; + $thumb_height: 2em; + + position: relative; + align-self: center; + + overflow: hidden; + + display: flex; + flex-direction: column; + justify-content: space-around; + + height: $bar_height; + border-radius: $border_radius_large; + + cursor: pointer; + + .bar-hider { + position: absolute; + + top: 0; + right: 0; + bottom: 0; + + background-color: #242527; + + -webkit-box-shadow: inset 0px 0px 2px 0px rgba(0,0,0,0.75); + -moz-box-shadow: inset 0px 0px 2px 0px rgba(0,0,0,0.75); + box-shadow: inset 0px 0px 2px 0px rgba(0,0,0,0.75); + + border-bottom-right-radius: $border_radius_large; + border-top-right-radius: $border_radius_large; + } + + &[value] { + overflow: visible; /* for the thumb */ + + border-bottom-left-radius: $border_radius_large; + border-top-left-radius: $border_radius_large; + } + + .bar-error { + z-index: 2; + width: 100%; + text-align: center; + + line-height: 1em; + font-size: .8em; + color: #a10000; + + padding-left: .2em; + padding-right: .2em; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .thumb { + position: absolute; + + top: 0; + right: 0; + + height: $thumb_height; + width: $thumb_width; + + margin-left: -($thumb_width / 2); + margin-right: -($thumb_width / 2); + + margin-top: -($thumb_height - $bar_height) / 2; + margin-bottom: -($thumb_height - $bar_height) / 2; + + background-color: #808080; + + .tooltip { + display: none; + } + } + + -webkit-box-shadow: inset 0px 0px 2px 0px rgba(0,0,0,0.25); + -moz-box-shadow: inset 0px 0px 2px 0px rgba(0,0,0,0.25); + box-shadow: inset 0px 0px 2px 0px rgba(0,0,0,0.25); + + /* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#70407e+0,45407e+100 */ + background: rgb(112,64,126); /* Old browsers */ + background: -moz-linear-gradient(left, rgba(112,64,126,1) 0%, rgba(69,64,126,1) 100%); /* FF3.6-15 */ + background: -webkit-linear-gradient(left, rgba(112,64,126,1) 0%,rgba(69,64,126,1) 100%); /* Chrome10-25,Safari5.1-6 */ + background: linear-gradient(to right, rgba(112,64,126,1) 0%,rgba(69,64,126,1) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#70407e', endColorstr='#45407e',GradientType=1 ); /* IE6-9 */ + } + + .left { + margin-right: 1em; + + .body { + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + border: 1px $color_list_border solid; + border-radius: $border_radius_large; + + background-color: $color_list_background; + + &.container-devices, .container-devices { + flex-grow: 1; + flex-shrink: 1; + + min-height: 3em; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + overflow-x: hidden; + overflow-y: auto; + + @include chat-scrollbar-vertical(); + + .device { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + cursor: pointer; + + height: 3em; + width: 100%; + + .container-selected { + /* the selected border */ + margin-top: 1px; + margin-bottom: 1px; + + flex-shrink: 0; + flex-grow: 0; + + padding: .5em; + + border: none; + border-right: 1px solid #242527; + + > .icon_em { + font-size: 2em; + opacity: 0; + } + } + + .container-name { + /* the selected border */ + margin-top: 1px; + margin-bottom: 1px; + + flex-shrink: 1; + flex-grow: 1; + + min-width: 4em; + + padding: .5em; + + display: flex; + flex-direction: column; + justify-content: space-around; + + border: none; + + .device-driver { + font-size: .8em; + line-height: 1em; + + color: #6a6a6a; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .device-name { + line-height: 1em; + + color: #999999; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .container-activity { + /* the selected border */ + margin-top: 1px; + margin-bottom: 1px; + + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: column; + justify-content: space-around; + + padding: .5em; + + width: 10em; + + border: none; + border-left: 1px solid #242527; + + .container-activity-bar { + flex-grow: 0; + flex-shrink: 0; + + width: 8em; + } + } + + &:hover { + background-color: $color_list_hover; + } + + &.selected { + .container-selected { + > .icon_em { + opacity: 1; + } + + margin-top: 0; + margin-bottom: 0; + + border-bottom: 1px solid #242527; + border-top: 1px solid #242527; + } + .container-name, .container-activity { + margin-top: 0; + margin-bottom: 0; + + border-bottom: 1px solid #242527; + border-top: 1px solid #242527; + } + } + } + } + + .buttons { + flex-grow: 0; + flex-shrink: 0; + + height: 3.5em; + padding: .5em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + border: none; + border-top: 1px $color_list_border solid; + + .spacer { + flex-grow: 1; + flex-shrink: 1; + } + + :not(.spacer) { + flex-grow: 0; + flex-shrink: 0; + } + + .container-error { + color: #a10000; + align-self: center; + } + + button { + min-width: 8em; + height: 2.5em; + } + } + } + } + + .right, .fill { + padding-right: .5em; /* for the sliders etc*/ + justify-content: flex-start; + + .body { + flex-grow: 0; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + /* microphone */ + .container-volume { + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + height: 3em; + width: 100%; + } + + /* microphone */ + .container-select-vad { + width: 100%; + + .fieldset { + padding: 0; + margin: 0; + + flex-shrink: 1; + flex-grow: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + > .container { + padding: 0; + + display: flex; + flex-direction: row; + justify-content: space-between; + + > label { + flex-shrink: 0; + min-width: 5em; + + cursor: pointer; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + height: 1.7em; + + .ratio-button { + align-self: center; + margin-right: .5em; + } + + a { + align-self: center; + line-height: 1.2em; + } + } + + button { + width: 100%; + + height: 2em; + font-size: .75em; + + align-self: center; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .container-button { + flex-shrink: 1; + margin-left: .5em; + + min-width: 3em; + width: 15em; + } + } + } + } + + /* microphone */ + .container-sensitivity { + width: 100%; + + display: flex; + flex-direction: row; + justify-content: stretch; + + .container-activity-bar { + flex-grow: 1; + flex-shrink: 1; + } + + + + .container-activity-bar .thumb { + @include transition(background-color $button_hover_animation_time ease-in-out); + } + + &.disabled { + pointer-events: none; + + .container-activity-bar { + .bar-hider { + width: 100%!important; + } + + .thumb { + background-color: #4d4d4d!important; + .tooltip { + opacity: 0!important; + } + } + } + } + } + + /* microphone */ + .container-advanced { + display: flex; + flex-direction: column; + justify-content: flex-start; + + .container-ppt-delay { + display: flex; + flex-direction: row; + justify-content: space-between; + + label { + cursor: pointer; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + margin-right: .5em; + + .checkbox { + margin-right: .5em; + } + + a { + line-height: 1.2em; + } + } + + .container-input { + display: flex; + flex-direction: row; + justify-content: stretch; + + cursor: text; + + border-radius: $border_radius_middle; + overflow: hidden; + + width: 5em; + + height: 1.8em; + font-size: 0.75em; + + + align-self: center; + + color: #464646; + background-color: #17171a; + + input { + flex-shrink: 1; + flex-grow: 1; + + min-width: 2em; + text-align: right; + + position: relative; + outline: none; + border: none; + + color: #464646; + padding: 0 .3em 0 .5em; + + background-color: transparent; + + -webkit-box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.25); + -moz-box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.25); + box-shadow: inset 0 0 2px 0 rgba(0,0,0,0.25); + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + } + + label { + flex-shrink: 0; + flex-grow: 0; + + align-self: center; + } + + &.disabled { + cursor: unset; + pointer-events: none; + + background-color: #222227; + } + } + } + } + + /* speaker */ + .container-volume-master { + .filler { + background-color: #2b8541; + } + } + + .container-volume-soundpack { + padding-top: .75em; + } + } + } - .property { - &:not(:first-of-type) { - margin-top: 5px; } - display: flex; - flex-direction: row; - justify-content: stretch; + &.identity-profiles { + flex-direction: row; + justify-content: stretch; + + .left, .right { + flex-grow: 1; + flex-shrink: 1; + + width: 50%; + min-width: 25em; + min-height: min-content; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .header { + height: 3em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + padding-bottom: .5em; + + a { + flex-grow: 1; + flex-shrink: 1; + + align-self: flex-end; + + font-weight: bold; + color: #e0e0e0; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .btn { + flex-shrink: 0; + flex-grow: 0; + + margin-left: 1em; + min-width: 8em; + } + } + } + + .left { + margin-right: 1em; + + .body { + flex-grow: 1; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: stretch; + + border: 1px $color_list_border solid; + border-radius: $border_radius_large; + + background-color: $color_list_background; + + .container-profiles { + flex-grow: 1; + flex-shrink: 1; + + min-height: 3em; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + overflow-x: hidden; + overflow-y: auto; + + @include chat-scrollbar-vertical(); + + .profile { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + cursor: pointer; + + height: 3em; + width: 100%; + + .container-avatar { + flex-shrink: 0; + flex-grow: 0; + + height: 3em; + width: 3em; + } + + .container-info { + flex-shrink: 1; + flex-grow: 1; + + margin-left: .5em; + min-width: 4em; + + display: flex; + flex-direction: column; + justify-content: center; + + .container-type { + width: 100%; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + text-transform: uppercase; + font-weight: bold; + + font-size: 0.8em; + + line-height: 1em; + + color: #6a6a6a; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + > *:not(:first-of-type) { + margin-left: .25em; + } + .icon-status { + margin-bottom: .2em; /* push it a bit higher than the center */ + } + + div { + align-self: center; + } + } + + .profile-name { + line-height: 1.2em; + + color: #999999; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &:hover { + background-color: $color_list_hover; + } + + &.selected { + background-color: $color_list_selected; + } + } + } + + .buttons { + flex-grow: 0; + flex-shrink: 0; + + height: 3.5em; + padding: .5em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + border: none; + border-top: 1px $color_list_border solid; + + .spacer { + flex-grow: 1; + flex-shrink: 1; + } + + :not(.spacer) { + flex-grow: 0; + flex-shrink: 0; + } + + .container-error { + color: #a10000; + align-self: center; + } + + button { + min-width: 8em; + height: 2.5em; + } + } + } + } + + .right { + padding-right: .5em; /* for the sliders etc*/ + justify-content: flex-start; + + .body { + flex-grow: 0; + flex-shrink: 1; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + .container-teamspeak { + .container-invalid { + padding: 1em; + text-align: center; + } + .container-valid { + .container-level { + display: flex; + flex-direction: row; + justify-content: stretch; + + .form-group { + flex-grow: 1; + flex-shrink: 1; + min-width: 4em; + } + + button { + height: 2em; + + align-self: center; + margin-left: 1em; + } + } + } + + .buttons { + display: flex; + flex-direction: row; + justify-content: space-between; + + > div { + text-align: right; + + width: max-content; + margin-left: 1em; + } + + button { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + text-align: end; + + margin-top: 1em; + height: 2.5em; + } + } + } + + .container-teaforo { + .container-valid, .container-invalid { + padding: 1em; + text-align: center; + + button { + margin-top: .5em; + } + } + } + } + } - .key { - width: 200px; } - .value { - flex-grow: 1; - flex-shrink: 1; + &.identity-forum { + .container-login { + flex-grow: 0; + flex-shrink: 0; - input { - width: 100%; + max-width: 25em; + + .container-button { + display: flex; + flex-direction: row; + justify-content: flex-end; + + button { + min-width: 8em; + } + } + + .container-error { + display: block; + margin-bottom: -1em; + color: red; + opacity: 0; + + &.shown { + opacity: 1; + } + + @include transform(opacity $button_hover_animation_time ease-in-out); + } } } - } - .identity-undefined { - text-align: center; - } + &.audio-sounds { + flex-direction: row; - .manage { - display: flex; - flex-direction: row; - justify-content: space-between; - margin-top: 5px; + min-height: 6em; + width: 100%; + + .left { + flex-shrink: 1; + flex-grow: 1; + + width: 75%; + margin-right: 1em; + + .header { + flex-shrink: 0; + flex-grow: 0; + + height: 3em; + + display: flex; + flex-direction: row; + justify-content: stretch; + + padding-bottom: .5em; + + a { + flex-grow: 1; + flex-shrink: 1; + + align-self: flex-end; + + font-weight: bold; + color: #e0e0e0; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .btn { + flex-shrink: 0; + flex-grow: 0; + + margin-left: 1em; + min-width: 8em; + } + } + + + .body { + flex-grow: 1; + flex-shrink: 1; + + min-height: 6em; + + display: flex; + flex-direction: column; + justify-content: stretch; + + border: 1px $color_list_border solid; + border-radius: $border_radius_large; + + background-color: $color_list_background; + + .container-sounds { + flex-grow: 1; + flex-shrink: 1; + + min-height: 3em; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + overflow-x: hidden; + overflow-y: auto; + + @include chat-scrollbar-vertical(); + + .sound { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + cursor: pointer; + + width: 100%; + + padding-left: .5em; + padding-right: 1em; + + font-size: .9em; + + .container-button-play_pause { + /* the selected border */ + margin-top: 1px; + margin-bottom: 1px; + + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: column; + justify-content: space-around; + + align-self: center; + margin-right: .25em; + + padding: .25em; + + border: none; + + /* copy checkmark */ + position: relative; + + width: 1.3em; + height: 1.3em; + + cursor: pointer; + pointer-events: all; + + overflow: hidden; + + background-color: #272626; + border-radius: $border_radius_middle; + + -webkit-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + -moz-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + + img { + height: 100%; + width: 100%; + + align-self: center; + } + } + + .container-name { + /* the selected border */ + margin-top: 1px; + margin-bottom: 1px; + + flex-shrink: 1; + flex-grow: 1; + + min-width: 4em; + + padding: .25em; + line-height: 1.2em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + border: none; + text-align: right; + } + + .container-button-toggle { + font-size: .8em; + } + + &:hover { + background-color: $color_list_hover; + } + + label { + display: flex; + flex-direction: column; + justify-content: space-around; + } + } + } + + .container-filter { + border: none; + border-top: 1px $color_list_border solid; + + padding-right: 1em; + padding-left: 1em; + } + } + } + + .right { + flex-grow: 0; + flex-shrink: 1; + + width: 25%; + } + } + + &.hidden { + display: none; + } } } } -} - -.container-teamspeak-import { - .error { - color: red; - /* margin-bottom: 5px; */ - } - - .success { - color: green; - } - - .input-file { - display: none; - } - - .load-data { - display: flex; - flex-direction: column; - justify-content: stretch; - - .buttons { - flex-grow: 1; - flex-shrink: 1; - display: flex; - flex-direction: row; - justify-content: end; - - button:not(:first-of-type) { - margin-left: 20px; - } - } - } - - .footer { - margin-top: 5px; - text-align: right; - margin-bottom: 5px; - - .button-import { - width: 100px; - } - } -} - -.container-teamspeak-improve { - display: flex; - flex-direction: column; - - .bmd-label-static { - display: flex; - flex-direction: row; - } - - .form-row { - margin-right: 0!important; - margin-left: 0!important; - justify-content: space-between; - } - - .buttons { - margin-top: 5px; - margin-bottom: 5px; - display: flex; - flex-direction: row; - justify-content: space-between; - - button { - width: 100px; - } - } - - .help-tip-container { - margin-left: 5px; - } } \ No newline at end of file diff --git a/shared/css/static/modal.scss b/shared/css/static/modal.scss new file mode 100644 index 00000000..ca109ec1 --- /dev/null +++ b/shared/css/static/modal.scss @@ -0,0 +1,938 @@ +@import "properties"; +@import "mixin"; + +.modal { + color: #999999; /* base color */ + + overflow: auto; /* allow scrolling if a modal is too big */ + + background-color: rgba(0, 0, 0, 0.8); + + padding-right: 5%; + padding-left: 5%; + + z-index: 1000; + position: fixed; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: none; + + margin-top: -7em; + opacity: 0; + + $animation_length: .3s; + @include transition(opacity $animation_length ease-in, margin-top $animation_length ease-in); + &.shown { + display: flex; + flex-direction: column; + justify-content: center; + + margin-top: 0; + opacity: 1; + + @include transition(opacity $animation_length ease-out, margin-top $animation_length ease-out); + } + + .modal-dialog { + display: block; + + margin: 1.75rem 0; + + /* width calculations */ + align-items: center; + + /* height stuff */ + max-height: calc(100% - 3.5em); + + .modal-content { + background: #19191b; + + border: 1px solid black; + border-radius: $border_radius_middle; + + width: max-content; + max-width: 100%; + min-width: 20em; + + min-height: min-content; + + /* align us in the center */ + margin-right: auto; + margin-left: auto; + + flex-shrink: 1; + flex-grow: 0; /* we dont want a grow over the limit set within the content, but we want to shrink the content if necessary */ + align-self: center; + + display: flex; + flex-direction: column; + justify-content: stretch; + + .modal-header, .modal-footer { + flex-grow: 0; + flex-shrink: 0; + } + + .modal-header { + background-color: #222224; + + display: flex; + flex-direction: row; + justify-content: stretch; + + padding: .25em; + + .container-icon, .container-close { + flex-grow: 0; + flex-shrink: 0; + } + + .container-close { + height: 1.4em; + width: 1.4em; + + padding: .2em; + border-radius: .2em; + + cursor: pointer; + + &:hover { + background-color: #1b1b1c; + } + } + + .container-icon { + margin-right: .25em; + + img { + height: 1em; + width: 1em; + } + } + + .modal-title, modal-header { + flex-grow: 1; + flex-shrink: 1; + + color: #9d9d9e; + } + + h5 { + margin: 0; + padding: 0; + } + } + + .modal-body { + max-width: 100%; + min-width: 20em; /* may adjust if needed */ + + overflow-y: auto; + overflow-x: auto; + } + } + } +} + +.modal { + //General style + .properties { + display: grid; + grid-template-columns: minmax(min-content, max-content) auto; + grid-column-gap: 10px; + grid-row-gap: 3px; + box-sizing: border-box; + } + + hr { + border-top: 3px double #8c8b8b; + width: 100%; + } + + + .input_error { + border-radius: 1px; + border: solid red; + } + + .properties_misc { + .complains { + display: grid; + grid-template-columns: auto auto auto; + grid-template-rows: auto auto; + grid-column-gap: 5px; + margin-bottom: 10px; + } + } + + .container { + padding: 6px; + } + + .modal-dialog { + display: flex; + flex-direction: column; + justify-content: stretch; + + &.modal-dialog-centered { + justify-content: stretch; + } + } + + .modal-content { + /* max-height: 500px; */ + min-height: 0; /* required for moz */ + flex-direction: column; + justify-content: stretch; + + .modal-header { + flex-shrink: 0; + flex-grow: 0; + + &.modal-header-error { + background-color: #ce0000; + } + + &.modal-header-info { + background-color: #03a9f4; + } + + &.modal-header-warning, &.modal-header-info, &.modal-header-error { + border-top-left-radius: .125rem; + border-top-right-radius: .125rem; + } + } + + .modal-body { + padding: 20px 24px 24px; + + flex-grow: 1; + flex-shrink: 1; + display: flex; + flex-direction: column; + min-height: 0; + + input.is-invalid { + background-image: linear-gradient(0deg, #d50000 2px, rgba(213, 0, 0, 0) 0), linear-gradient(0deg, rgba(241, 1, 1, 0.61) 1px, transparent 0); + } + } + + .modal-footer { + flex-shrink: 0; + flex-grow: 0; + + &.modal-footer-button-group { + button { + min-width: 100px; + } + + button:not(:first-of-type) { + margin-left: 15px; + }; + } + } + } +} + +/* special general modals */ +.modal { + .modal-body.modal-blue { + border-left: 2px solid #0a73d2; + } + .modal-body.modal-green { + border-left: 2px solid #00d400; + } + + .modal-body.modal-body-input { + color: #999999; + + width: 100%; + + .form-group:not(.with-title) { + padding-top: .75rem; + } + + input.is-invalid ~ .container-help-feedback > .invalid-feedback { + display: block; + } + + .container-help-feedback { + position: absolute; + } + + .buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + + button { + width: 6em; + + &:not(:last-of-type) { + margin-right: 1em; + } + } + } + } + + .modal-body.modal-body-yesno { + color: #999999; + + border: none; + border-left: 2px solid #d50000; + + width: 100%; + + .buttons { + padding-top: 2em; + + display: flex; + flex-direction: row; + justify-content: flex-end; + + button { + width: 6em; + + &:not(:last-of-type) { + margin-right: 1em; + } + } + } + } +} + + +/* Input group */ +.form-group { + position: relative; + + padding-top: 1.75rem; /* the label above (might be floating) */ + margin-bottom: 1rem; /* for invalid label/help label */ + + .form-control { + display: block; + width: 100%; + padding: .4375rem 0; + font-size: 1rem; + line-height: 1.5; + color: #cdd1d0; + background-color: transparent; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, .26); + border-radius: 0; + box-shadow: none; + + @include transition(border-color .15s ease-in-out, box-shadow .15s ease-in-out); + } + + label { + color: #999999; + + top: 1rem; + left: 0; + font-size: .75rem; + + position: absolute; + pointer-events: none; + transition: all .3s ease; + + line-height: 1; + + &.bmd-label-floating { + will-change: left, top, contents; + color: #999999; + top: 2.42rem; + font-size: 1rem; + } + + @include transition(color $button_hover_animation_time ease-in-out, top $button_hover_animation_time ease-in-out, font-size $button_hover_animation_time ease-in-out); + } + + + &:focus-within { + label { + color: #3c74a2; + + &.bmd-label-floating { + //color: #343434; + } + } + } + + &:focus-within, &.is-filled { + label.bmd-label-floating { + top: 1rem; + font-size: .75rem; + color: #3c74a2; + } + } + + .form-control { + height: 2.25em; + + background: no-repeat bottom, 50% calc(100% - 1px); + background-size: 0 100%, 100% 100%; + border: 0; + transition: background 0s ease-out; + padding-left: 0; + padding-right: 0; + + + background-image: linear-gradient(0deg, #008aff 2px, rgba(0, 150, 136, 0) 0), linear-gradient(0deg, #393939 1px, transparent 0); + + &:focus { + background-size: 100% 100%, 100% 100%; + transition-duration: .3s; + + color: #ced3d3; + background-color: transparent; + outline: 0; + } + + &.is-invalid { + background-image: linear-gradient(0deg, #d50000 2px,rgba(213,0,0,0) 0),linear-gradient(0deg,rgba(241,1,1,.61) 1px,transparent 0); + } + } + + .invalid-feedback { + position: absolute; + opacity: 0; + width: 100%; + margin-top: .25rem; + font-size: 80%; + color: #f44336; + + @include transition(opacity .25s ease-in-out); + } + + .form-control.is-invalid ~ .invalid-feedback { + opacity: 1; + } + + + &.is-invalid { + .form-control { + background-image: linear-gradient(0deg, #d50000 2px,rgba(213,0,0,0) 0),linear-gradient(0deg,rgba(241,1,1,.61) 1px,transparent 0); + } + + .invalid-feedback { + opacity: 1; + } + + label { + color: #f44336!important; + } + } + + .bmd-help { + position: absolute; + opacity: 0; + width: 100%; + margin-top: .25rem; + + font-size: .75em; + + @include transition(opacity .25s ease-in-out); + } + + .form-control:focus-within ~ .bmd-help { + opacity: 1; + } +} + +/* button look */ +.btn { + cursor: pointer; + + background-color: #0000007F; + + border-width: 0; + border-radius: $border_radius_middle; + border-style: solid; + + color: #7c7c7c; + + padding: .25em 1em; + + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); + + &:hover { + background-color: #151515; + } + + &:disabled { + box-shadow: none; + background-color: #00000045; + + &:hover { + background-color: #00000045; + } + } + + &.btn-success { + border-bottom-width: 2px; + border-bottom-color: #389738; + } + + &.btn-info { + border-bottom-width: 2px; + border-bottom-color: #386896; + } + + &.btn-warning, &.btn-danger { + border-bottom-width: 2px; + border-bottom-color: #973838; + } + + @include transition(background-color $button_hover_animation_time ease-in-out); +} + +/* general switch look */ +.switch { + $ball_outer_width: 1.5em; /* 1.5? */ + $ball_inner_width: .4em; + + $slider_height: .8em; + $slider_width: 2em; + + $slider_border_size: .1em; + + position: relative; + display: inline-block; + outline: none; + + width: $slider_width; + height: $slider_height; + + /* "allocate" space for the slider */ + margin-top: ($ball_outer_width - $slider_height) / 2; + margin-bottom: ($ball_outer_width - $slider_height) / 2; + margin-left: $ball_outer_width / 2; + margin-right: $ball_outer_width / 2; + + /* fix size */ + flex-shrink: 0; + flex-grow: 0; + + input { + /* "hide" the actual input node */ + opacity: 0; + width: 0; + height: 0; + outline: none; + } + + .slider { + pointer-events: all!important; + position: absolute; + cursor: pointer; + outline: none; + + top: -$slider_border_size; + left: -$slider_border_size; + right: -$slider_border_size; + bottom: -$slider_border_size; + + background-color: #252424; + + border: $slider_border_size solid #262628; + border-radius: 5px; + + &:before { + position: absolute; + content: ""; + + height: $ball_outer_width; + width: $ball_outer_width; + + left: - $ball_outer_width / 2; + bottom: -($ball_outer_width - $slider_height) / 2; + + background-color: #3d3a3a; + + @include transition(.4s); + border-radius: 50%; + + box-shadow: 0 0 .2em 1px #00000044; + } + + .dot { + position: absolute; + + height: $ball_inner_width; + width: $ball_inner_width; + + left: -($ball_inner_width / 2); + bottom: $slider_height / 2 - $ball_inner_width / 2; + + background-color: #a5a5a5; + box-shadow: 0 0 1em 1px #a5a5a566; + border-radius: 50%; + + @include transition(.4s); + } + } + + + + input:focus + .slider { + } + + input:checked + .slider { + &:before { + @include transform(translateX($slider_width)); + } + + .dot { + @include transform(translateX($slider_width)); + background-color: #46c0ec; + box-shadow: 0 0 1em 1px #46c0ec; + } + } +} + +/* general ratio button look */ +.ratio-button { + $button_size: 1.2em; + $mark_size: .6em; + + position: relative; + + width: $button_size; + height: $button_size; + + cursor: pointer; + + overflow: hidden; + + background-color: #272626; + border-radius: 50%; + + input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + } + + //#07d1fe + .mark { + position: absolute; + opacity: 0; + + top: ($button_size - $mark_size) / 2; + bottom: ($button_size - $mark_size) / 2; + right: ($button_size - $mark_size) / 2; + left: ($button_size - $mark_size) / 2; + + background-color: #46c0ec; + box-shadow: 0 0 .5em 1px #46c0ec66; + border-radius: 50%; + + @include transition(.4s); + } + + input:checked + .mark { + opacity: 1; + } + + @include transition(background-color $button_hover_animation_time); + + -webkit-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + -moz-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); +} + +label:hover > .ratio-button, .ratio-button:hover { + &.ratio-button, > .ratio-button { + background-color: #2c2b2b; + } +} + +label.disabled > .ratio-button, .ratio-button.disabled, .ratio-button:disabled { + &.ratio-button, > .ratio-button { + pointer-events: none!important; + background-color: #1a1919!important; + } +} + + +/* + + */ +.checkbox { + flex-shrink: 0; + flex-grow: 0; + + position: relative; + + width: 1.3em; + height: 1.3em; + + cursor: pointer; + pointer-events: all; + + overflow: hidden; + + background-color: #272626; + border-radius: $border_radius_middle; + + input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + } + + //#07d1fe + .mark { + position: absolute; + opacity: 0; + + height: .5em; + width: .8em; + + margin-left: 0.25em; + margin-top: .3em; + + border: none; + border-bottom: .2em solid #46c0ec; + border-left: .2em solid #46c0ec; + + transform: rotateY(0deg) rotate(-45deg); /* needs Y at 0 deg to behave properly*/ + @include transition(.4s); + } + + input:checked + .mark { + opacity: 1; + } + + -webkit-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + -moz-box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); + box-shadow: inset 0 0 4px 0 rgba(0, 0, 0, 0.5); +} + +label.disabled > .checkbox, .checkbox:disabled, .checkbox.disabled { + &.checkbox, > .checkbox { + pointer-events: none!important; + background-color: #222227; + } +} + +/* slider */ +$track_height: .6em; + +$thumb_width: .6em; +$thumb_height: 2em; + +$tooltip_width: 4em; +$tooltip_height: 1.8em; + +.container-slider { + font-size: .8em; + + position: relative; + + margin-top: .5em; /* for the track */ + + width: 100%; + height: $track_height; + + cursor: pointer; + + background-color: #242527; + border-radius: $border_radius_large; + + overflow: visible; + + .filler { + position: absolute; + + left: 0; + top: 0; + bottom: 0; + + background-color: #4370a2; + border-radius: $border_radius_large; + } + + .thumb { + position: absolute; + + top: 0; + right: 0; + + height: $thumb_height; + width: $thumb_width; + + margin-left: -($thumb_width / 2); + margin-right: -($thumb_width / 2); + + margin-top: -($thumb_height - $track_height) / 2; + margin-bottom: -($thumb_height - $track_height) / 2; + + background-color: #808080; + + .tooltip { + display: none; + + /* + position: absolute; + top: -($tooltip_height + .6em); + left: -($tooltip_width - $thumb_width) / 2; + + line-height: 1em; + + height: $tooltip_height; + width: $tooltip_width; + + background-color: #232222; + border-radius: $border_radius_middle; + + text-align: center; + display: flex; + flex-direction: column; + justify-content: space-around; + + opacity: 0; + @include transition(opacity .5s ease-in-out); + + &:before { + content: ''; + + position: absolute; + + left: ($tooltip_width - $thumb_width) / 2 - .25em; + right: 0; + bottom: -.4em; + + width: 0; + height: 0; + + border-style: solid; + border-width: .5em .5em 0 .5em; + border-color: #232222 transparent transparent transparent; + } + */ + } + } + + &:hover, &.active { + .tooltip { + opacity: 1; + } + } +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ +} + +input[type=number] { + -moz-appearance:textfield; /* Firefox */ +} + +/* "Boxed input" Used in channeledit & serveredit */ +.input-boxed { + height: 2.5em; + + border-radius: .2em; + border: 1px solid #111112; + + background-color: #121213; + + display: flex; + flex-direction: row; + justify-content: stretch; + + color: #b3b3b3; + + @include placeholder(&) { + color: #606060; + }; + + .prefix { + flex-grow: 0; + flex-shrink: 0; + + margin: 0; + + line-height: initial; + align-self: center; + padding: 0 .5em; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + opacity: 1; + + @include transition($button_hover_animation_time ease-in-out); + } + + &.is-invalid { + background-color: #180d0d; + border-color: #721c1c; + + background-image: unset!important; + } + + &:focus, &:focus-within { + background-color: #131b22; + border-color: #284262; + + color: #e1e2e3; + + .prefix { + width: 0; + padding-left: 0; + padding-right: 0; + opacity: 0; + } + } + + input, select { + flex-grow: 1; + flex-shrink: 1; + + padding: 0 0.5em; + + background: transparent; + border: none; + outline: none; + margin: 0; + + color: #b3b3b3; + } + + .prefix + input { + padding-left: 0; + } + + + &:focus, &:focus-within { + .prefix + input { + padding-left: .5em; + } + } + + &.disabled, &:disabled { + background-color: #1a1819; + } + + @include transition($button_hover_animation_time ease-in-out); +} + +input.input-boxed { + padding: 0.5em; +} \ No newline at end of file diff --git a/shared/css/static/modals.scss b/shared/css/static/modals.scss index e11f9292..6d6366b1 100644 --- a/shared/css/static/modals.scss +++ b/shared/css/static/modals.scss @@ -1,132 +1,3 @@ -/* backdrop fix */ -.modal-backdrop { - visibility: hidden !important; -} -.modal { - background-color: rgba(0,0,0,0.5); - padding-right: 8% !important; -} - -modal-body { - display: flex; - flex-direction: column; - - min-height: 10px; -} - -.modal { - //General style - .properties { - display: grid; - grid-template-columns: minmax(min-content, max-content) auto; - grid-column-gap: 10px; - grid-row-gap: 3px; - box-sizing: border-box; - } - - hr { - border-top: 3px double #8c8b8b; - width: 100%; - } - - - .input_error { - border-radius: 1px; - border: solid red; - } - - .properties_misc { - .complains { - display: grid; - grid-template-columns: auto auto auto; - grid-template-rows: auto auto; - grid-column-gap: 5px; - margin-bottom: 10px; - } - } - - .container { - padding: 6px; - } - - .modal-dialog { - max-height: 80%; - display: flex; - flex-direction: column; - justify-content: stretch; - - &.modal-dialog-centered { - justify-content: stretch; - } - } - - .modal-content { - /* max-height: 500px; */ - min-height: 0; /* required for moz */ - flex-direction: column; - justify-content: stretch; - - .modal-header { - flex-shrink: 0; - flex-grow: 0; - - &.modal-header-error { - background-color: #ce0000; - } - - &.modal-header-info { - background-color: #03a9f4; - } - - &.modal-header-warning, &.modal-header-info, &.modal-header-error { - border-top-left-radius: .125rem; - border-top-right-radius: .125rem; - } - } - - .modal-body { - flex-grow: 1; - flex-shrink: 1; - display: flex; - flex-direction: column; - min-height: 0; - - input.is-invalid { - background-image: linear-gradient(0deg, #d50000 2px, rgba(213, 0, 0, 0) 0), linear-gradient(0deg, rgba(241, 1, 1, 0.61) 1px, transparent 0); - } - - &.modal-body-input { - .form-group:not(.with-title) { - padding-top: .75rem; - } - - input.is-invalid ~ .container-help-feedback > .invalid-feedback { - display: block; - } - - .container-help-feedback { - position: absolute; - } - } - } - - .modal-footer { - flex-shrink: 0; - flex-grow: 0; - - &.modal-footer-button-group { - button { - min-width: 100px; - } - - button:not(:first-of-type) { - margin-left: 15px; - }; - } - } - } -} - .channel_perm_tbl .value { width: 60px; } @@ -217,9 +88,13 @@ modal-body { .arrow { display: inline-block; border: solid black; - border-width: 0 3px 3px 0; - padding: 3px; - height: 10px; + //border-width: 0 3px 3px 0; + //padding: 3px; + //height: 10px; + + border-width: 0 .2em .2em 0; + padding: .21em; + height: .5em; &.right { transform: rotate(-45deg); @@ -327,106 +202,4 @@ modal-body { } } } -} - -.group-assignment-list { - .group-list { - border: lightgray solid 1px; - padding: 3px; - overflow-y: auto; - - .group-entry { - display: flex; - flex-direction: row; - height: max-content; - } - - .icon-container { - align-self: center; - margin-right: 4px; - margin-left: 2px; - margin-top: -2px; - } - - a { - align-self: center; - } - - .checkbox { - align-self: center; - height: 8px; - - margin-top: 1px; - margin-left: 1px; - display: block; - position: relative; - padding-left: 18px; - margin-bottom: 12px; - cursor: pointer; - font-size: 22px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - /* Hide the browser's default checkbox */ - input { - position: absolute; - opacity: 0; - cursor: pointer; - display: none; - } - - .checkmark { - position: absolute; - top: 0; - left: 0; - height: 16px; - width: 16px; - background-color: #eee; - margin-right: 4px; - - &:after { - content: ""; - position: absolute; - display: none; - - left: 5px; - top: 1px; - width: 6px; - height: 12px; - border: solid white; - border-width: 0 3px 3px 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - } - } - - &:hover:not(.disabled) input ~ .checkmark { - background-color: #ccc; - } - - input:checked ~ .checkmark { - background-color: #2196F3; - } - - input:checked ~ .checkmark:after { - display: block; - } - - &.disabled { - user-select: none; - pointer-events: none; - cursor: not-allowed; - - .checkmark { - background-color: #00000055; - &:after { - border-color: #00000055; - } - } - } - } - } } \ No newline at end of file diff --git a/shared/css/static/properties.scss b/shared/css/static/properties.scss index 5c81d39f..f0a0a877 100644 --- a/shared/css/static/properties.scss +++ b/shared/css/static/properties.scss @@ -1,4 +1,10 @@ $channel_tree_entry_selected: #2d2d2d; $channel_tree_entry_hovered: #393939; -$channel_tree_entry_text_color: #828282; \ No newline at end of file +$channel_tree_entry_text_color: #828282; + +$border_radius_small: .1em; +$border_radius_middle: .15em; +$border_radius_large: .2em; + +$button_hover_animation_time: .25s \ No newline at end of file diff --git a/shared/css/static/server-log.scss b/shared/css/static/server-log.scss index 3a7a95b0..1f97ac77 100644 --- a/shared/css/static/server-log.scss +++ b/shared/css/static/server-log.scss @@ -1,3 +1,5 @@ +@import "mixin"; + .container-log { display: block; overflow-y: auto; @@ -5,6 +7,9 @@ height: 100%; width: 100%; + @include chat-scrollbar-vertical(); + @include chat-scrollbar-horizontal(); + .container-messages { width: 100%; line-height: 16px; @@ -33,7 +38,7 @@ font-family: sans-serif; font-size: 13px; - line-height: 1; + line-height: initial; } > .timestamp { diff --git a/shared/css/static/ts/country.scss b/shared/css/static/ts/country.scss index 4515f67b..6151d276 100644 --- a/shared/css/static/ts/country.scss +++ b/shared/css/static/ts/country.scss @@ -5,6 +5,8 @@ flex-shrink: 0; flex-grow: 0; + + background-position: 0 -2717px; /* by default use global flag */ } .country.flag-ad { diff --git a/shared/css/static/ts/icons_em.scss b/shared/css/static/ts/icons_em.scss new file mode 100644 index 00000000..6fd9ecef --- /dev/null +++ b/shared/css/static/ts/icons_em.scss @@ -0,0 +1,607 @@ +/* sprite bounds (px): width="496" height="400" */ +.icon_em { + display: inline-block; + background: url('../../../img/client_icon_sprite.svg'), url('../../img/client_icon_sprite.svg') no-repeat; + background-size: calc(496em / 16) calc(400em / 16); + height: 1em; + width: 1em; +} + +/* Icons 1em */ +.icon_em.client-d_sound { + background-position: calc(0em / 16) calc(0em / 16); +} +.icon_em.client-d_sound_me { + background-position: calc(-32em / 16) calc(0em / 16); +} +.icon_em.client-d_sound_user { + background-position: calc(-64em / 16) calc(0em / 16); +} +.icon_em.client-about { + background-position: calc(-96em / 16) calc(0em / 16); +} +.icon_em.client-activate_microphone { + background-position: calc(-128em / 16) calc(0em / 16); +} +.icon_em.client-add { + background-position: calc(-160em / 16) calc(0em / 16); +} +.icon_em.client-add_foe { + background-position: calc(-192em / 16) calc(0em / 16); +} +.icon_em.client-add_folder { + background-position: calc(-224em / 16) calc(0em / 16); +} +.icon_em.client-add_friend { + background-position: calc(-256em / 16) calc(0em / 16); +} +.icon_em.client-addon { + background-position: calc(-288em / 16) calc(0em / 16); +} +.icon_em.client-addon-collection { + background-position: calc(-320em / 16) calc(0em / 16); +} +.icon_em.client-apply { + background-position: calc(-352em / 16) calc(0em / 16); +} +.icon_em.client-arrow_down { + background-position: calc(-384em / 16) calc(0em / 16); +} +.icon_em.client-arrow_left { + background-position: calc(-416em / 16) calc(0em / 16); +} +.icon_em.client-arrow_right { + background-position: calc(-448em / 16) calc(0em / 16); +} +.icon_em.client-arrow_up { + background-position: calc(-480em / 16) calc(0em / 16); +} +.icon_em.client-away { + background-position: calc(0em / 16) calc(-32em / 16); +} +.icon_em.client-ban_client { + background-position: calc(-32em / 16) calc(-32em / 16); +} +.icon_em.client-ban_list { + background-position: calc(-64em / 16) calc(-32em / 16); +} +.icon_em.client-bookmark_add { + background-position: calc(-96em / 16) calc(-32em / 16); +} +.icon_em.client-bookmark_add_folder { + background-position: calc(-128em / 16) calc(-32em / 16); +} +.icon_em.client-bookmark_duplicate { + background-position: calc(-160em / 16) calc(-32em / 16); +} +.icon_em.client-bookmark_manager { + background-position: calc(-192em / 16) calc(-32em / 16); +} +.icon_em.client-bookmark_remove { + background-position: calc(-224em / 16) calc(-32em / 16); +} +.icon_em.client-broken_image { + background-position: calc(-256em / 16) calc(-32em / 16); +} +.icon_em.client-browse-addon-online { + background-position: calc(-288em / 16) calc(-32em / 16); +} +.icon_em.client-capture { + background-position: calc(-320em / 16) calc(-32em / 16); +} +.icon_em.client-changelog { + background-position: calc(-352em / 16) calc(-32em / 16); +} +.icon_em.client-change_nickname { + background-position: calc(-384em / 16) calc(-32em / 16); +} +.icon_em.client-channel_chat { + background-position: calc(-416em / 16) calc(-32em / 16); +} +.icon_em.client-channel_collapse_all { + background-position: calc(-448em / 16) calc(-32em / 16); +} +.icon_em.client-channel_commander { + background-position: calc(-480em / 16) calc(-32em / 16); +} +.icon_em.client-channel_create { + background-position: calc(0em / 16) calc(-64em / 16); +} +.icon_em.client-channel_create_sub { + background-position: calc(-32em / 16) calc(-64em / 16); +} +.icon_em.client-channel_default { + background-position: calc(-64em / 16) calc(-64em / 16); +} +.icon_em.client-channel_delete { + background-position: calc(-96em / 16) calc(-64em / 16); +} +.icon_em.client-channel_edit { + background-position: calc(-128em / 16) calc(-64em / 16); +} +.icon_em.client-channel_expand_all { + background-position: calc(-160em / 16) calc(-64em / 16); +} +.icon_em.client-channel_green { + background-position: calc(-192em / 16) calc(-64em / 16); +} +.icon_em.client-channel_green_subscribed { + background-position: calc(-224em / 16) calc(-64em / 16); +} +.icon_em.client-channel_private { + background-position: calc(-256em / 16) calc(-64em / 16); +} +.icon_em.client-channel_red { + background-position: calc(-288em / 16) calc(-64em / 16); +} +.icon_em.client-channel_red_subscribed { + background-position: calc(-320em / 16) calc(-64em / 16); +} +.icon_em.client-channel_switch { + background-position: calc(-352em / 16) calc(-64em / 16); +} +.icon_em.client-channel_unsubscribed { + background-position: calc(-384em / 16) calc(-64em / 16); +} +.icon_em.client-channel_yellow { + background-position: calc(-416em / 16) calc(-64em / 16); +} +.icon_em.client-channel_yellow_subscribed { + background-position: calc(-448em / 16) calc(-64em / 16); +} +.icon_em.client-check_update { + background-position: calc(-480em / 16) calc(-64em / 16); +} +.icon_em.client-client_hide { + background-position: calc(0em / 16) calc(-96em / 16); +} +.icon_em.client-client_show { + background-position: calc(-32em / 16) calc(-96em / 16); +} +.icon_em.client-close_button { + background-position: calc(-64em / 16) calc(-96em / 16); +} +.icon_em.client-complaint_list { + background-position: calc(-96em / 16) calc(-96em / 16); +} +.icon_em.client-conflict-icon { + background-position: calc(-128em / 16) calc(-96em / 16); +} +.icon_em.client-connect { + background-position: calc(-160em / 16) calc(-96em / 16); +} +.icon_em.client-contact { + background-position: calc(-192em / 16) calc(-96em / 16); +} +.icon_em.client-copy { + background-position: calc(-224em / 16) calc(-96em / 16); +} +.icon_em.client-copy_url { + background-position: calc(-256em / 16) calc(-96em / 16); +} +.icon_em.client-default { + background-position: calc(-288em / 16) calc(-96em / 16); +} +.icon_em.client-default_for_all_bookmarks { + background-position: calc(-320em / 16) calc(-96em / 16); +} +.icon_em.client-delete { + background-position: calc(-352em / 16) calc(-96em / 16); +} +.icon_em.client-delete_avatar { + background-position: calc(-384em / 16) calc(-96em / 16); +} +.icon_em.client-disconnect { + background-position: calc(-416em / 16) calc(-96em / 16); +} +.icon_em.client-down { + background-position: calc(-448em / 16) calc(-96em / 16); +} +.icon_em.client-download { + background-position: calc(-480em / 16) calc(-96em / 16); +} +.icon_em.client-edit { + background-position: calc(0em / 16) calc(-128em / 16); +} +.icon_em.client-edit_friend_foe_status { + background-position: calc(-32em / 16) calc(-128em / 16); +} +.icon_em.client-emoticon { + background-position: calc(-64em / 16) calc(-128em / 16); +} +.icon_em.client-error { + background-position: calc(-96em / 16) calc(-128em / 16); +} +.icon_em.client-file_home { + background-position: calc(-128em / 16) calc(-128em / 16); +} +.icon_em.client-file_refresh { + background-position: calc(-160em / 16) calc(-128em / 16); +} +.icon_em.client-filetransfer { + background-position: calc(-192em / 16) calc(-128em / 16); +} +.icon_em.client-find { + background-position: calc(-224em / 16) calc(-128em / 16); +} +.icon_em.client-folder { + background-position: calc(-256em / 16) calc(-128em / 16); +} +.icon_em.client-folder_up { + background-position: calc(-288em / 16) calc(-128em / 16); +} +.icon_em.client-group_100 { + background-position: calc(-320em / 16) calc(-128em / 16); +} +.icon_em.client-group_200 { + background-position: calc(-352em / 16) calc(-128em / 16); +} +.icon_em.client-group_300 { + background-position: calc(-384em / 16) calc(-128em / 16); +} +.icon_em.client-group_500 { + background-position: calc(-416em / 16) calc(-128em / 16); +} +.icon_em.client-group_600 { + background-position: calc(-448em / 16) calc(-128em / 16); +} +.icon_em.client-guisetup { + background-position: calc(-480em / 16) calc(-128em / 16); +} +.icon_em.client-hardware_input_muted { + background-position: calc(0em / 16) calc(-160em / 16); +} +.icon_em.client-hardware_output_muted { + background-position: calc(-32em / 16) calc(-160em / 16); +} +.icon_em.client-hoster_button { + background-position: calc(-64em / 16) calc(-160em / 16); +} +.icon_em.client-hotkeys { + background-position: calc(-96em / 16) calc(-160em / 16); +} +.icon_em.client-icon-pack { + background-position: calc(-128em / 16) calc(-160em / 16); +} +.icon_em.client-iconsview { + background-position: calc(-160em / 16) calc(-160em / 16); +} +.icon_em.client-iconviewer { + background-position: calc(-192em / 16) calc(-160em / 16); +} +.icon_em.client-identity_default { + background-position: calc(-224em / 16) calc(-160em / 16); +} +.icon_em.client-identity_export { + background-position: calc(-256em / 16) calc(-160em / 16); +} +.icon_em.client-identity_import { + background-position: calc(-288em / 16) calc(-160em / 16); +} +.icon_em.client-identity_manager { + background-position: calc(-320em / 16) calc(-160em / 16); +} +.icon_em.client-info { + background-position: calc(-352em / 16) calc(-160em / 16); +} +.icon_em.client-input_muted { + background-position: calc(-384em / 16) calc(-160em / 16); +} +.icon_em.client-input_muted_local { + background-position: calc(-416em / 16) calc(-160em / 16); +} +.icon_em.client-invite_buddy { + background-position: calc(-448em / 16) calc(-160em / 16); +} +.icon_em.client-is_talker { + background-position: calc(-480em / 16) calc(-160em / 16); +} +.icon_em.client-kick_channel { + background-position: calc(0em / 16) calc(-192em / 16); +} +.icon_em.client-kick_server { + background-position: calc(-32em / 16) calc(-192em / 16); +} +.icon_em.client-listview { + background-position: calc(-64em / 16) calc(-192em / 16); +} +.icon_em.client-loading_image { + background-position: calc(-96em / 16) calc(-192em / 16); +} +.icon_em.client-message_incoming { + background-position: calc(-128em / 16) calc(-192em / 16); +} +.icon_em.client-message_info { + background-position: calc(-160em / 16) calc(-192em / 16); +} +.icon_em.client-message_outgoing { + background-position: calc(-192em / 16) calc(-192em / 16); +} +.icon_em.client-messages { + background-position: calc(-224em / 16) calc(-192em / 16); +} +.icon_em.client-moderated { + background-position: calc(-256em / 16) calc(-192em / 16); +} +.icon_em.client-move_client_to_own_channel { + background-position: calc(-288em / 16) calc(-192em / 16); +} +.icon_em.client-music { + background-position: calc(-320em / 16) calc(-192em / 16); +} +.icon_em.client-new_chat { + background-position: calc(-352em / 16) calc(-192em / 16); +} +.icon_em.client-notifications { + background-position: calc(-384em / 16) calc(-192em / 16); +} +.icon_em.client-offline_messages { + background-position: calc(-416em / 16) calc(-192em / 16); +} +.icon_em.client-on_whisperlist { + background-position: calc(-448em / 16) calc(-192em / 16); +} +.icon_em.client-output_muted { + background-position: calc(-480em / 16) calc(-192em / 16); +} +.icon_em.client-permission_channel { + background-position: calc(0em / 16) calc(-224em / 16); +} +.icon_em.client-permission_client { + background-position: calc(-32em / 16) calc(-224em / 16); +} +.icon_em.client-permission_overview { + background-position: calc(-64em / 16) calc(-224em / 16); +} +.icon_em.client-permission_server_groups { + background-position: calc(-96em / 16) calc(-224em / 16); +} +.icon_em.client-phoneticsnickname { + background-position: calc(-128em / 16) calc(-224em / 16); +} +.icon_em.client-ping_1 { + background-position: calc(-160em / 16) calc(-224em / 16); +} +.icon_em.client-ping_2 { + background-position: calc(-192em / 16) calc(-224em / 16); +} +.icon_em.client-ping_3 { + background-position: calc(-224em / 16) calc(-224em / 16); +} +.icon_em.client-ping_4 { + background-position: calc(-256em / 16) calc(-224em / 16); +} +.icon_em.client-ping_calculating { + background-position: calc(-288em / 16) calc(-224em / 16); +} +.icon_em.client-ping_disconnected { + background-position: calc(-320em / 16) calc(-224em / 16); +} +.icon_em.client-play { + background-position: calc(-352em / 16) calc(-224em / 16); +} +.icon_em.client-player_chat { + background-position: calc(-384em / 16) calc(-224em / 16); +} +.icon_em.client-player_commander_off { + background-position: calc(-416em / 16) calc(-224em / 16); +} +.icon_em.client-player_commander_on { + background-position: calc(-448em / 16) calc(-224em / 16); +} +.icon_em.client-player_off { + background-position: calc(-480em / 16) calc(-224em / 16); +} +.icon_em.client-player_on { + background-position: calc(0em / 16) calc(-256em / 16); +} +.icon_em.client-player_whisper { + background-position: calc(-32em / 16) calc(-256em / 16); +} +.icon_em.client-plugins { + background-position: calc(-64em / 16) calc(-256em / 16); +} +.icon_em.client-poke { + background-position: calc(-96em / 16) calc(-256em / 16); +} +.icon_em.client-present { + background-position: calc(-128em / 16) calc(-256em / 16); +} +.icon_em.client-recording_start { + background-position: calc(-160em / 16) calc(-256em / 16); +} +.icon_em.client-recording_stop { + background-position: calc(-192em / 16) calc(-256em / 16); +} +.icon_em.client-refresh { + background-position: calc(-224em / 16) calc(-256em / 16); +} +.icon_em.client-register { + background-position: calc(-256em / 16) calc(-256em / 16); +} +.icon_em.client-reload { + background-position: calc(-288em / 16) calc(-256em / 16); +} +.icon_em.client-remove_foe { + background-position: calc(-320em / 16) calc(-256em / 16); +} +.icon_em.client-remove_friend { + background-position: calc(-352em / 16) calc(-256em / 16); +} +.icon_em.client-security { + background-position: calc(-384em / 16) calc(-256em / 16); +} +.icon_em.client-selectfolder { + background-position: calc(-416em / 16) calc(-256em / 16); +} +.icon_em.client-send_complaint { + background-position: calc(-448em / 16) calc(-256em / 16); +} +.icon_em.client-server_green { + background-position: calc(-480em / 16) calc(-256em / 16); +} +.icon_em.client-server_log { + background-position: calc(0em / 16) calc(-288em / 16); +} +.icon_em.client-server_query { + background-position: calc(-32em / 16) calc(-288em / 16); +} +.icon_em.client-settings { + background-position: calc(-64em / 16) calc(-288em / 16); +} +.icon_em.client-sort_by_name { + background-position: calc(-96em / 16) calc(-288em / 16); +} +.icon_em.client-soundpack { + background-position: calc(-128em / 16) calc(-288em / 16); +} +.icon_em.client-sound-pack { + background-position: calc(-160em / 16) calc(-288em / 16); +} +.icon_em.client-stop { + background-position: calc(-192em / 16) calc(-288em / 16); +} +.icon_em.client-subscribe_mode { + background-position: calc(-224em / 16) calc(-288em / 16); +} +.icon_em.client-subscribe_to_all_channels { + background-position: calc(-256em / 16) calc(-288em / 16); +} +.icon_em.client-subscribe_to_channel { + background-position: calc(-288em / 16) calc(-288em / 16); +} +.icon_em.client-subscribe_to_channel_family { + background-position: calc(-320em / 16) calc(-288em / 16); +} +.icon_em.client-switch_advanced { + background-position: calc(-352em / 16) calc(-288em / 16); +} +.icon_em.client-switch_standard { + background-position: calc(-384em / 16) calc(-288em / 16); +} +.icon_em.client-sync-disable { + background-position: calc(-416em / 16) calc(-288em / 16); +} +.icon_em.client-sync-enable { + background-position: calc(-448em / 16) calc(-288em / 16); +} +.icon_em.client-sync-icon { + background-position: calc(-480em / 16) calc(-288em / 16); +} +.icon_em.client-tab_close_button { + background-position: calc(0em / 16) calc(-320em / 16); +} +.icon_em.client-talk_power_grant { + background-position: calc(-32em / 16) calc(-320em / 16); +} +.icon_em.client-talk_power_grant_next { + background-position: calc(-64em / 16) calc(-320em / 16); +} +.icon_em.client-talk_power_request { + background-position: calc(-96em / 16) calc(-320em / 16); +} +.icon_em.client-talk_power_request_cancel { + background-position: calc(-128em / 16) calc(-320em / 16); +} +.icon_em.client-talk_power_revoke { + background-position: calc(-160em / 16) calc(-320em / 16); +} +.icon_em.client-talk_power_revoke_all_grant_next { + background-position: calc(-192em / 16) calc(-320em / 16); +} +.icon_em.client-temp_server_password { + background-position: calc(-224em / 16) calc(-320em / 16); +} +.icon_em.client-temp_server_password_add { + background-position: calc(-256em / 16) calc(-320em / 16); +} +.icon_em.client-textformat { + background-position: calc(-288em / 16) calc(-320em / 16); +} +.icon_em.client-textformat_bold { + background-position: calc(-320em / 16) calc(-320em / 16); +} +.icon_em.client-textformat_foreground { + background-position: calc(-352em / 16) calc(-320em / 16); +} +.icon_em.client-textformat_italic { + background-position: calc(-384em / 16) calc(-320em / 16); +} +.icon_em.client-textformat_underline { + background-position: calc(-416em / 16) calc(-320em / 16); +} +.icon_em.client-theme { + background-position: calc(-448em / 16) calc(-320em / 16); +} +.icon_em.client-toggle_server_query_clients { + background-position: calc(-480em / 16) calc(-320em / 16); +} +.icon_em.client-toggle_whisper { + background-position: calc(0em / 16) calc(-352em / 16); +} +.icon_em.client-token { + background-position: calc(-32em / 16) calc(-352em / 16); +} +.icon_em.client-token_use { + background-position: calc(-64em / 16) calc(-352em / 16); +} +.icon_em.client-translation { + background-position: calc(-96em / 16) calc(-352em / 16); +} +.icon_em.client-unsubscribe_from_all_channels { + background-position: calc(-128em / 16) calc(-352em / 16); +} +.icon_em.client-unsubscribe_from_channel_family { + background-position: calc(-160em / 16) calc(-352em / 16); +} +.icon_em.client-unsubscribe_mode { + background-position: calc(-192em / 16) calc(-352em / 16); +} +.icon_em.client-up { + background-position: calc(-224em / 16) calc(-352em / 16); +} +.icon_em.client-upload { + background-position: calc(-256em / 16) calc(-352em / 16); +} +.icon_em.client-upload_avatar { + background-position: calc(-288em / 16) calc(-352em / 16); +} +.icon_em.client-urlcatcher { + background-position: calc(-320em / 16) calc(-352em / 16); +} +.icon_em.client-user-account { + background-position: calc(-352em / 16) calc(-352em / 16); +} +.icon_em.client-virtualserver_edit { + background-position: calc(-384em / 16) calc(-352em / 16); +} +.icon_em.client-volume { + background-position: calc(-416em / 16) calc(-352em / 16); +} +.icon_em.client-warning { + background-position: calc(-448em / 16) calc(-352em / 16); +} +.icon_em.client-warning_external_link { + background-position: calc(-480em / 16) calc(-352em / 16); +} +.icon_em.client-warning_info { + background-position: calc(0em / 16) calc(-384em / 16); +} +.icon_em.client-warning_question { + background-position: calc(-32em / 16) calc(-384em / 16); +} +.icon_em.client-weblist { + background-position: calc(-64em / 16) calc(-384em / 16); +} +.icon_em.client-whisper { + background-position: calc(-96em / 16) calc(-384em / 16); +} +.icon_em.client-whisperlists { + background-position: calc(-128em / 16) calc(-384em / 16); +} +.icon_em.client-channel_green_subscribed2 { + background-position: calc(-160em / 16) calc(-384em / 16); +} +.icon_em.client-home { + background-position: calc(-192em / 16) calc(-384em / 16); +} \ No newline at end of file diff --git a/shared/generate_voice.py b/shared/generate_voice.py index c14ea389..ed6c7cde 100644 --- a/shared/generate_voice.py +++ b/shared/generate_voice.py @@ -1,8 +1,14 @@ """ This should be executed with python 2.7 (because of pydub) + +Used voice: UK-Graham """ import os +import os.path +import string +import base64 +import sys import requests import json import csv @@ -12,7 +18,8 @@ from pydub import AudioSegment TARGET_DIRECTORY = "audio/speech" SOURCE_FILE = "audio/speech_sentences.csv" - +""" +We cant use the automated way because this now requires a security token and the AWS server does bot exists anymore def tts(text, file): voice_id = 4 language_id = 1 @@ -43,33 +50,102 @@ def tts(text, file): sound.export(file, format="wav") os.remove(file + ".mp3") +""" + def main(): - if os.path.exists(TARGET_DIRECTORY): - print("Deleting old speach directory (%s)!" % TARGET_DIRECTORY) - try: - shutil.rmtree(TARGET_DIRECTORY) - except e: - print("Cant delete old dir!") - os.makedirs(TARGET_DIRECTORY) + if False: + if os.path.exists(TARGET_DIRECTORY): + print("Deleting old speach directory (%s)!" % TARGET_DIRECTORY) + try: + shutil.rmtree(TARGET_DIRECTORY) + except e: + print("Cant delete old dir!") + try: + os.makedirs(TARGET_DIRECTORY) + except: + pass + mapping_file = 'audio/speech/mapping.json' mapping = [] + + with open(mapping_file, "r") as fstream: + mapping = json.loads(fstream.read()) + + tts_queue = [] with open(SOURCE_FILE, 'r') as input: reader = csv.reader(filter(lambda row: len(row) != 0 and row[0] != '#', input), delimiter=';', quotechar='#') for row in reader: if len(row) != 2: continue - print("Generating speech for {}: {}".format(row[0], row[1])) - try: - file = "{}.wav".format(row[0]) - tts(row[1], TARGET_DIRECTORY + "/" + file) - mapping.append({'key': row[0], 'file': file}) - except e: - print(e) - print("Failed to generate {}", row[0]) + file = TARGET_DIRECTORY + "/" + "{}.wav".format(row[0]) - with open("audio/speech/mapping.json", "w") as fstream: + _object = filter(lambda e: e["key"] == row[0], mapping) + if len(_object) > 0: + _object = _object[0] + if os.path.exists(TARGET_DIRECTORY + "/" + _object["file"]): + print("Skipping speech generation for {} ({}). File already exists".format(row[0], file)) + continue + + print("Enqueuing speech generation for {} ({}): {}".format(row[0], file, row[1])) + tts_queue.append([row[0], file, row[1]]) + + if len(tts_queue) == 0: + print("No sounds need to be generated!") + return + + print(tts_queue) + print("Please generate HSR file for the following text:") + for entry in tts_queue: + print(entry[2]) + print("") + + print("-" * 30) + print("Enter the HSR file path") + file = "" # /home/wolverindev/Downloads/www.naturalreaders.com.har + while True: + if len(file) > 0: + if os.path.exists(file): + break + print("Invalid file try again") + file = string.strip(sys.stdin.readline()) + print("Testing file {}".format(file)) + + with open(file, "r") as fstream: + data = json.loads(fstream.read()) + entries = data["log"]["entries"] + for entry in entries: + if not entry["request"]["url"].startswith('https://pweb.naturalreaders.com/v0/tts?'): + continue + if not (entry["request"]["method"] == "POST"): + continue + + post_data = json.loads(entry["request"]["postData"]["text"]) + key = post_data["t"] + tts_entry = filter(lambda e: e[2] == key, tts_queue) + if len(tts_entry) == 0: + print("Missing generated speech text handle for: {}".format(key)) + continue + tts_entry = tts_entry[0] + tts_queue.remove(tts_entry) + + print(tts_entry) + with open(tts_entry[1] + ".mp3", "wb") as mp3_tmp: + mp3_tmp.write(base64.decodestring(entry["response"]["content"]["text"])) + mp3_tmp.close() + + sound = AudioSegment.from_mp3(tts_entry[1] + ".mp3") + sound.export(tts_entry[1], format="wav") + os.remove(tts_entry[1] + ".mp3") + + mapping.append({ + 'key': tts_entry[0], + 'file': "{}.wav".format(tts_entry[0]) + }) + + print("FILE DONE!") + with open(mapping_file, "w") as fstream: fstream.write(json.dumps(mapping)) fstream.close() diff --git a/shared/html/index.php b/shared/html/index.php index 4be230a8..3c03dc4b 100644 --- a/shared/html/index.php +++ b/shared/html/index.php @@ -1,46 +1,17 @@ - - - @@ -50,6 +21,7 @@ echo "TeaClient"; } else { echo "TeaSpeak-Web"; + echo ''; } ?> @@ -64,13 +36,6 @@ spawn_property('connect_default_host', $localhost ? "localhost" : "ts.TeaSpeak.de"); spawn_property('localhost_debug', $localhost ? "true" : "false"); - if(isset($_COOKIE)) { - if(array_key_exists("COOKIE_NAME_USER_DATA", $GLOBALS) && array_key_exists($GLOBALS["COOKIE_NAME_USER_DATA"], $_COOKIE)) - spawn_property('forum_user_data', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_DATA"]]); - if(array_key_exists("COOKIE_NAME_USER_SIGN", $GLOBALS) && array_key_exists($GLOBALS["COOKIE_NAME_USER_SIGN"], $_COOKIE)) - spawn_property('forum_user_sign', $_COOKIE[$GLOBALS["COOKIE_NAME_USER_SIGN"]]); - } - spawn_property('forum_path', authPath()); $version = file_get_contents("./version"); if ($version === false) @@ -171,8 +136,23 @@
+ + + - + + + + + + + + + + + + +
@@ -191,28 +171,10 @@
+
+ +
- logout"; - } else { - $footer_forum = "Login via the TeaSpeak forum."; - } - } - ?> - - +
\ No newline at end of file diff --git a/shared/html/templates.html b/shared/html/templates.html index 284b8f98..dc1aa5ad 100644 --- a/shared/html/templates.html +++ b/shared/html/templates.html @@ -12,36 +12,72 @@
-
-
-
- - - - @@ -3019,24 +4289,27 @@
{{tr "URL resolver:"/}}
-
{{if song_url_loader}}{{>song_url_loader}}{{else}}{{tr "unset" /}}{{/if}}
+
{{if song_url_loader}}{{>song_url_loader}}{{else}}{{tr "unset" /}}{{/if}} +
{{tr "Song loaded:" /}}
{{if song_loaded}}{{tr "yes" /}}{{else}}{{tr "no" /}}{{/if}}
{{if song_loaded}} - {{if metadata && false}} - {{tr "Display metdata here!" /}} - {{else}} -
-
-
{{tr "Metadata:" /}}
-
-
- + {{if metadata && false}} + {{tr "Display metdata here!" /}} + {{else}} + + {{/if}} {{/if}}
@@ -3047,15 +4320,16 @@
{{tr "URL:" /}}
- +
{{tr "URL loader:" /}}
- + - + +
@@ -3079,8 +4353,12 @@
- - + + @@ -3091,28 +4369,37 @@
- +
- - + +
{{tr "Server Properties" /}}
- +
- - + +
- - + +
@@ -3144,7 +4431,8 @@
- +
@@ -3161,7 +4449,7 @@ {{else type == "directory" }}
{{>name}}
-
+
{{/if}} @@ -3236,7 +4524,9 @@
{{if enable_select}} - + {{/if}}
@@ -3245,7 +4535,7 @@ + + + + - - - +
+ {{tr "Press any key which you want to use." /}} +
+ Current key: +
+
- - \ No newline at end of file + + + \ No newline at end of file diff --git a/shared/i18n/info.json b/shared/i18n/info.json index bea71089..3101cc43 100644 --- a/shared/i18n/info.json +++ b/shared/i18n/info.json @@ -74,7 +74,7 @@ ] }, { "key": "ru_gt", - "country_code": "gt", + "country_code": "ru", "path": "ru_google_translate.translation", "name": "Auto translated messages for language ru", diff --git a/shared/img/c_background.svg b/shared/img/c_background.svg new file mode 100644 index 00000000..2e2f92d9 --- /dev/null +++ b/shared/img/c_background.svg @@ -0,0 +1,3 @@ + + + diff --git a/shared/img/icon_conversation_message_delete.svg b/shared/img/icon_conversation_message_delete.svg new file mode 100644 index 00000000..5a334ed5 --- /dev/null +++ b/shared/img/icon_conversation_message_delete.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/shared/img/icon_group_add.svg b/shared/img/icon_group_add.svg new file mode 100644 index 00000000..c2ad6c9c --- /dev/null +++ b/shared/img/icon_group_add.svg @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/shared/img/icon_group_delete.svg b/shared/img/icon_group_delete.svg new file mode 100644 index 00000000..c776f4f4 --- /dev/null +++ b/shared/img/icon_group_delete.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/shared/img/icon_group_duplicate.svg b/shared/img/icon_group_duplicate.svg new file mode 100644 index 00000000..c63b12de --- /dev/null +++ b/shared/img/icon_group_duplicate.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/shared/img/icon_group_rename.svg b/shared/img/icon_group_rename.svg new file mode 100644 index 00000000..decd438e --- /dev/null +++ b/shared/img/icon_group_rename.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/shared/img/icon_sound_pause.svg b/shared/img/icon_sound_pause.svg new file mode 100644 index 00000000..9789c620 --- /dev/null +++ b/shared/img/icon_sound_pause.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/shared/img/icon_sound_play.svg b/shared/img/icon_sound_play.svg new file mode 100644 index 00000000..6b61f0d7 --- /dev/null +++ b/shared/img/icon_sound_play.svg @@ -0,0 +1,2 @@ + +play-glyph \ No newline at end of file diff --git a/shared/img/icon_tooltip.svg b/shared/img/icon_tooltip.svg new file mode 100644 index 00000000..8d4a77d3 --- /dev/null +++ b/shared/img/icon_tooltip.svg @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/shared/img/photo-camera.svg b/shared/img/photo-camera.svg new file mode 100644 index 00000000..34267caa --- /dev/null +++ b/shared/img/photo-camera.svg @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/shared/img/style/urls.txt b/shared/img/style/urls.txt new file mode 100644 index 00000000..73c51c53 --- /dev/null +++ b/shared/img/style/urls.txt @@ -0,0 +1,17 @@ +18:56:34> "Nieme": http://puu.sh/E0Qut/3092386510.png < +18:59:26> "Nieme": http://puu.sh/E0QxG/3b4d7286ec.png < +19:00:16> "Nieme": http://puu.sh/E0QyI/8b66f7cf3b.png < +19:02:09> "Nieme": süß das er denkt das du das nicht weisst :D < +19:02:20> "Another TeaSpeak user": xD < +19:02:53> "Nieme": http://puu.sh/E0QBN/a49973e13f.png < +19:03:05> "Nieme": http://puu.sh/E0QC0/a668c9500c.png < +19:03:50> "Nieme": http://puu.sh/E0QCZ/e78dc1b3c0.png < +19:06:47> "Nieme": http://puu.sh/E0QGx/b21ace2d9a.png < +19:18:29> "Nieme": -> http://puu.sh/E0QUb/ce5e3f93ae.png < +19:20:45> "Nieme": http://puu.sh/E0QX2/af62f28320.png < +19:23:02> "Nieme": 82qtx5 < +19:23:06> "Nieme": 1 280 349 948 < +20:09:34> Your chat partner has disconnected < +20:10:08> Your chat partner has reconnected + +https://www.iconfinder.com/iconsets/evil-icons-user-interface \ No newline at end of file diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 4d0103f9..978b14c4 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -24,6 +24,7 @@ enum DisconnectReason { HANDSHAKE_BANNED, SERVER_CLOSED, SERVER_REQUIRES_PASSWORD, + SERVER_HOSTMESSAGE, IDENTITY_TOO_LOW, UNKNOWN } @@ -88,14 +89,15 @@ class ConnectionHandler { permissions: PermissionManager; groups: GroupManager; - chat_frame: chat.Frame; + side_bar: chat.Frame; select_info: InfoBar; - chat: ChatBox; settings: ServerSettings; sound: sound.SoundManager; - readonly tag_connection_handler: JQuery; + hostbanner: Hostbanner; + + tag_connection_handler: JQuery; private _clientId: number = 0; private _local_client: LocalClientEntry; @@ -126,42 +128,50 @@ class ConnectionHandler { this.log = new log.ServerLog(this); this.select_info = new InfoBar(this); this.channelTree = new ChannelTree(this); - this.chat = new ChatBox(this); - this.chat_frame = new chat.Frame(this); + this.side_bar = new chat.Frame(this); this.sound = new sound.SoundManager(this); + this.hostbanner = new Hostbanner(this); this.serverConnection = connection.spawn_server_connection(this); this.serverConnection.onconnectionstatechanged = this.on_connection_state_changed.bind(this); this.fileManager = new FileManager(this); this.permissions = new PermissionManager(this); + this.side_bar.channel_conversations().initialize_needed_listener(); + this.groups = new GroupManager(this); this._local_client = new LocalClientEntry(this); - this.channelTree.registerClient(this._local_client); - //settings.static_global(Settings.KEY_DISABLE_VOICE, false) - this.chat.initialize(); - this.tag_connection_handler = $.spawn("div").addClass("connection-container"); - $.spawn("div").addClass("server-icon icon client-server_green").appendTo(this.tag_connection_handler); - $.spawn("div").addClass("server-name").text(tr("Not connected")).appendTo(this.tag_connection_handler); - $.spawn("div").addClass("button-close icon client-tab_close_button").appendTo(this.tag_connection_handler); - this.tag_connection_handler.on('click', event => { - if(event.isDefaultPrevented()) - return; + /* initialize connection handler tab entry */ + { + this.tag_connection_handler = $.spawn("div").addClass("connection-container"); + $.spawn("div").addClass("server-icon icon client-server_green").appendTo(this.tag_connection_handler); + $.spawn("div").addClass("server-name").appendTo(this.tag_connection_handler); + $.spawn("div").addClass("button-close icon client-tab_close_button").appendTo(this.tag_connection_handler); + this.tag_connection_handler.on('click', event => { + if(event.isDefaultPrevented()) + return; - server_connections.set_active_connection_handler(this); - }); - this.tag_connection_handler.find(".button-close").on('click', event => { - server_connections.destroy_server_connection_handler(this); - event.preventDefault(); - }); + server_connections.set_active_connection_handler(this); + }); + this.tag_connection_handler.find(".button-close").on('click', event => { + server_connections.destroy_server_connection_handler(this); + event.preventDefault(); + }); + this.tab_set_name(tr("Not connected")); + } + } + + tab_set_name(name: string) { + this.tag_connection_handler.toggleClass('cutoff-name', name.length > 30); + this.tag_connection_handler.find(".server-name").text(name); } setup() { } - async startConnection(addr: string, profile: profiles.ConnectionProfile, parameters: ConnectParameters) { - this.tag_connection_handler.find(".server-name").text(tr("Connecting")); - this.cancel_reconnect(); + async startConnection(addr: string, profile: profiles.ConnectionProfile, user_action: boolean, parameters: ConnectParameters) { + this.tab_set_name(tr("Connecting")); + this.cancel_reconnect(false); this._reconnect_attempt = false; if(this.serverConnection) this.handleDisconnect(DisconnectReason.REQUESTED); @@ -172,8 +182,9 @@ class ConnectionHandler { port: -1 }; { + let _v6_end = addr.indexOf(']'); let idx = addr.lastIndexOf(':'); - if(idx != -1) { + if(idx != -1 && idx > _v6_end) { server_address.port = parseInt(addr.substr(idx + 1)); server_address.host = addr.substr(0, idx); } else { @@ -203,7 +214,14 @@ class ConnectionHandler { createErrorModal(tr("Error while hashing password"), tr("Failed to hash server password!
") + error).open(); } } + if(parameters.password) { + connection_log.update_address_password({ + hostname: server_address.host, + port: server_address.port + }, parameters.password.password); + } + const original_address = {host: server_address.host, port: server_address.port}; if(dns.supported() && !server_address.host.match(Modals.Regex.IP_V4) && !server_address.host.match(Modals.Regex.IP_V6)) { const id = ++this._connect_initialize_id; this.log.log(log.server.Type.CONNECTION_HOSTNAME_RESOLVE, {}); @@ -229,6 +247,15 @@ class ConnectionHandler { } await this.serverConnection.connect(server_address, new connection.HandshakeHandler(profile, parameters)); + setTimeout(() => { + const connected = this.serverConnection.connected(); + if(user_action && connected) { + connection_log.log_connect({ + hostname: original_address.host, + port: original_address.port + }); + } + }, 50); } @@ -252,7 +279,6 @@ class ConnectionHandler { */ onConnected() { console.log("Client connected!"); - this.channelTree.registerClient(this._local_client); this.permissions.requestPermissionList(); if(this.groups.serverGroups.length == 0) this.groups.requestGroups(); @@ -352,7 +378,7 @@ class ConnectionHandler { const profile = profiles.find_profile(properties.connect_profile) || profiles.default_profile(); const cprops = this.reconnect_properties(profile); - this.startConnection(properties.connect_address, profile, cprops); + this.startConnection(properties.connect_address, profile, true, cprops); }); const url = build_url(properties); @@ -379,10 +405,11 @@ class ConnectionHandler { handleDisconnect(type: DisconnectReason, data: any = {}) { this._connect_initialize_id++; - this.tag_connection_handler.find(".server-name").text(tr("Not connected")); + this.tab_set_name(tr("Not connected")); let auto_reconnect = false; switch (type) { case DisconnectReason.REQUESTED: + case DisconnectReason.SERVER_HOSTMESSAGE: /* already handled */ break; case DisconnectReason.HANDLER_DESTROYED: if(data) @@ -468,7 +495,8 @@ class ConnectionHandler { break; case DisconnectReason.SERVER_CLOSED: - this.chat.serverChat().appendError(tr("Server closed ({0})"), data.reasonmsg); + this.log.log(log.server.Type.SERVER_CLOSED, {message: data.reasonmsg}); + //this.chat.serverChat().appendError(tr("Server closed ({0})"), data.reasonmsg); createErrorModal( tr("Server closed"), "The server is closed.
" + //TODO tr @@ -479,15 +507,23 @@ class ConnectionHandler { auto_reconnect = true; break; case DisconnectReason.SERVER_REQUIRES_PASSWORD: - this.chat.serverChat().appendError(tr("Server requires password")); + this.log.log(log.server.Type.SERVER_REQUIRES_PASSWORD, {}); + //this.chat.serverChat().appendError(tr("Server requires password")); + createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => { if(!(typeof password === "string")) return; - const cprops = this.reconnect_properties(this.serverConnection.handshake_handler().profile); + const profile = this.serverConnection.handshake_handler().profile; + const cprops = this.reconnect_properties(profile); cprops.password = {password: password as string, hashed: false}; - this.startConnection(this.serverConnection.remote_address().host + ":" + this.serverConnection.remote_address().port, - this.serverConnection.handshake_handler().profile, - cprops); + + connection_log.update_address_info({ + port: this.channelTree.server.remote_address.port, + hostname: this.channelTree.server.remote_address.host + }, { + flag_password: true + } as any); + this.startConnection(this.channelTree.server.remote_address.host + ":" + this.channelTree.server.remote_address.port, profile, false, cprops); }).open(); break; case DisconnectReason.CLIENT_KICKED: @@ -499,15 +535,29 @@ class ConnectionHandler { auto_reconnect = false; break; case DisconnectReason.HANDSHAKE_BANNED: - this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"), - ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]), - data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : ""); + this.log.log(log.server.Type.SERVER_BANNED, { + invoker: { + client_name: data["invokername"], + client_id: parseInt(data["invokerid"]), + client_unique_id: data["invokeruid"] + }, + + message: data["reasonmsg"], + time: parseInt(data["time"]) + }); this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse break; case DisconnectReason.CLIENT_BANNED: - this.chat.serverChat().appendError(tr("You got banned from the server by {0}{1}"), - ClientEntry.chatTag(data["invokerid"], data["invokername"], data["invokeruid"]), - data["reasonmsg"] ? " (" + data["reasonmsg"] + ")" : ""); + this.log.log(log.server.Type.SERVER_BANNED, { + invoker: { + client_name: data["invokername"], + client_id: parseInt(data["invokerid"]), + client_unique_id: data["invokeruid"] + }, + + message: data["reasonmsg"], + time: parseInt(data["time"]) + }); this.sound.play(Sound.CONNECTION_BANNED); //TODO findout if it was a disconnect or a connect refuse break; default: @@ -517,6 +567,7 @@ class ConnectionHandler { break; } + this.channelTree.unregisterClient(this._local_client); /* if we dont unregister our client here the client will be destroyed */ this.channelTree.reset(); if(this.serverConnection) this.serverConnection.disconnect(); @@ -524,7 +575,8 @@ class ConnectionHandler { if(control_bar.current_connection_handler() == this) control_bar.update_connection_state(); this.select_info.setCurrentSelected(null); - this.select_info.update_banner(); + this.side_bar.private_conversations().clear_client_ids(); + this.hostbanner.update(); if(auto_reconnect) { if(!this.serverConnection) { @@ -542,15 +594,15 @@ class ConnectionHandler { this.log.log(log.server.Type.RECONNECT_CANCELED, {}); log.info(LogCategory.NETWORKING, tr("Reconnecting...")); - this.startConnection(server_address.host + ":" + server_address.port, profile, this.reconnect_properties(profile)); + this.startConnection(server_address.host + ":" + server_address.port, profile, false, this.reconnect_properties(profile)); this._reconnect_attempt = true; }, 5000); } } - cancel_reconnect() { + cancel_reconnect(log_event: boolean) { if(this._reconnect_timer) { - this.log.log(log.server.Type.RECONNECT_CANCELED, {}); + if(log_event) this.log.log(log.server.Type.RECONNECT_CANCELED, {}); clearTimeout(this._reconnect_timer); this._reconnect_timer = undefined; } @@ -562,6 +614,8 @@ class ConnectionHandler { } update_voice_status(targetChannel?: ChannelEntry) { + if(!this._local_client) return; /* we've been destroyed */ + targetChannel = targetChannel || this.getClient().currentChannel(); const vconnection = this.serverConnection.voice_connection(); @@ -636,10 +690,21 @@ class ConnectionHandler { if(vconnection && vconnection.voice_recorder() && vconnection.voice_recorder().record_supported) { const active = !this.client_status.input_muted && !this.client_status.output_muted; - if(active) - vconnection.voice_recorder().input.start(); - else + if(active) { + if(vconnection.voice_recorder().input.current_state() === audio.recorder.InputState.PAUSED) { + vconnection.voice_recorder().input.start().then(result => { + if(result != audio.recorder.InputStartResult.EOK) { + console.warn(tr("Failed to start microphone input (%s)."), result); + createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), result)).open(); + } + }).catch(error => { + console.warn(tr("Failed to start microphone input (%s)."), error); + createErrorModal(tr("Failed to start recording"), MessageHelper.formatMessage(tr("Microphone start failed.{:br:}Error: {}"), error)).open(); + }); + } + } else { vconnection.voice_recorder().input.stop(); + } } if(control_bar.current_connection_handler() === this) @@ -665,6 +730,12 @@ class ConnectionHandler { if(this.client_status.away === state) return; + if(state) { + this.sound.play(Sound.AWAY_ACTIVATED); + } else { + this.sound.play(Sound.AWAY_DEACTIVATED); + } + this.client_status.away = state; this.serverConnection.send_command("clientupdate", { client_away: typeof(this.client_status.away) === "string" || this.client_status.away, @@ -707,4 +778,141 @@ class ConnectionHandler { password: this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.password : undefined } } + + update_avatar() { + Modals.spawnAvatarUpload(data => { + if(typeof(data) === "undefined") + return; + if(data === null) { + console.log(tr("Deleting existing avatar")); + this.serverConnection.send_command('ftdeletefile', { + name: "/avatar_", /* delete own avatar */ + path: "", + cid: 0 + }).then(() => { + createInfoModal(tr("Avatar deleted"), tr("Avatar successfully deleted")).open(); + }).catch(error => { + console.error(tr("Failed to reset avatar flag: %o"), error); + + let message; + if(error instanceof CommandResult) + message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Error: {0}"), error.extra_message || error.message); + if(!message) + message = MessageHelper.formatMessage(tr("Failed to delete avatar.{:br:}Lookup the console for more details")); + createErrorModal(tr("Failed to delete avatar"), message).open(); + return; + }); + } else { + console.log(tr("Uploading new avatar")); + (async () => { + let key: transfer.UploadKey; + try { + key = await this.fileManager.upload_file({ + size: data.byteLength, + path: '', + name: '/avatar', + overwrite: true, + channel: undefined, + channel_password: undefined + }); + } catch(error) { + console.error(tr("Failed to initialize avatar upload: %o"), error); + let message; + if(error instanceof CommandResult) { + //TODO: Resolve permission name + //i_client_max_avatar_filesize + if(error.id == ErrorID.PERMISSION_ERROR) { + message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Missing permission {0}"), error["failed_permid"]); + } else { + message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Error: {0}"), error.extra_message || error.message); + } + } + if(!message) + message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details")); + createErrorModal(tr("Failed to upload avatar"), message).open(); + return; + } + + try { + await transfer.spawn_upload_transfer(key).put_data(data); + } catch(error) { + console.error(tr("Failed to upload avatar: %o"), error); + + let message; + if(typeof(error) === "string") + message = MessageHelper.formatMessage(tr("Failed to upload avatar.{:br:}Error: {0}"), error); + + if(!message) + message = MessageHelper.formatMessage(tr("Failed to initialize avatar upload.{:br:}Lookup the console for more details")); + createErrorModal(tr("Failed to upload avatar"), message).open(); + return; + } + try { + await this.serverConnection.send_command('clientupdate', { + client_flag_avatar: guid() + }); + } catch(error) { + console.error(tr("Failed to update avatar flag: %o"), error); + + let message; + if(error instanceof CommandResult) + message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Error: {0}"), error.extra_message || error.message); + if(!message) + message = MessageHelper.formatMessage(tr("Failed to update avatar flag.{:br:}Lookup the console for more details")); + createErrorModal(tr("Failed to set avatar"), message).open(); + return; + } + + createInfoModal(tr("Avatar successfully uploaded"), tr("Your avatar has been uploaded successfully!")).open(); + })(); + + } + }); + } + + destroy() { + this.cancel_reconnect(true); + + this.tag_connection_handler && this.tag_connection_handler.remove(); + this.tag_connection_handler = undefined; + + this.hostbanner && this.hostbanner.destroy(); + this.hostbanner = undefined; + + this._local_client && this._local_client.destroy(); + this._local_client = undefined; + + this.channelTree && this.channelTree.destroy(); + this.channelTree = undefined; + + this.side_bar && this.side_bar.destroy(); + this.side_bar = undefined; + + this.select_info && this.select_info.destroy(); + this.select_info = undefined; + + this.log && this.log.destroy(); + this.log = undefined; + + this.permissions && this.permissions.destroy(); + this.permissions = undefined; + + this.groups && this.groups.destroy(); + this.groups = undefined; + + this.fileManager && this.fileManager.destroy(); + this.fileManager = undefined; + + this.settings && this.settings.destroy(); + this.settings = undefined; + + if(this.serverConnection) { + this.serverConnection.onconnectionstatechanged = undefined; + connection.destroy_server_connection(this.serverConnection); + } + this.serverConnection = undefined; + + this.sound = undefined; + this._local_client = undefined; + } } \ No newline at end of file diff --git a/shared/js/FileManager.ts b/shared/js/FileManager.ts index d8ebe1d8..0be06317 100644 --- a/shared/js/FileManager.ts +++ b/shared/js/FileManager.ts @@ -102,7 +102,7 @@ class RequestFileUpload { this.transfer_key = key; } - async put_data(data: BufferSource | File) { + async put_data(data: BlobPart | File) { const form_data = new FormData(); if(data instanceof File) { @@ -110,6 +110,10 @@ class RequestFileUpload { throw "invalid size"; form_data.append("file", data); + } else if(typeof(data) === "string") { + if(data.length != this.transfer_key.total_size) + throw "invalid size"; + form_data.append("file", new Blob([data], { type: "application/octet-stream" })); } else { const buffer = data; if(buffer.byteLength != this.transfer_key.total_size) @@ -159,6 +163,24 @@ class FileManager extends connection.AbstractCommandHandler { this.connection.command_handler_boss().register_handler(this); } + destroy() { + if(this.connection) { + const hboss = this.connection.command_handler_boss(); + if(hboss) + hboss.unregister_handler(this); + } + + this.listRequests = undefined; + this.pending_download_requests = undefined; + this.pending_upload_requests = undefined; + + this.icons && this.icons.destroy(); + this.icons = undefined; + + this.avatars && this.avatars.destroy(); + this.avatars = undefined; + } + handle_command(command: connection.ServerCommand): boolean { switch (command.command) { case "notifyfilelist": @@ -262,7 +284,7 @@ class FileManager extends connection.AbstractCommandHandler { "clientftfid": transfer_data.client_transfer_id, "seekpos": 0, "proto": 1 - }).catch(reason => { + }, {process_result: false}).catch(reason => { this.pending_download_requests.remove(transfer_data); reject(reason); }) @@ -410,9 +432,9 @@ function media_image_type(type: ImageType, file?: boolean) { } } -function image_type(base64: string | ArrayBuffer) { +function image_type(encoded_data: string | ArrayBuffer, base64_encoded?: boolean) { const ab2str10 = () => { - const buf = new Uint8Array(base64 as ArrayBuffer); + const buf = new Uint8Array(encoded_data as ArrayBuffer); if(buf.byteLength < 10) return ""; @@ -422,7 +444,7 @@ function image_type(base64: string | ArrayBuffer) { return result; }; - const bin = typeof(base64) === "string" ? atob(base64) : ab2str10(); + const bin = typeof(encoded_data) === "string" ? ((typeof(base64_encoded) === "undefined" || base64_encoded) ? atob(encoded_data) : encoded_data) : ab2str10(); if(bin.length < 10) return ImageType.UNKNOWN; if(bin[0] == String.fromCharCode(66) && bin[1] == String.fromCharCode(77)) { @@ -481,8 +503,7 @@ class CacheManager { async resolve_cached(key: string, max_age?: number) : Promise { max_age = typeof(max_age) === "number" ? max_age : -1; - const request = new Request("https://_local_cache/cache_request_" + key); - const cached_response = await this._cache_category.match(request); + const cached_response = await this._cache_category.match("https://_local_cache/cache_request_" + key); if(!cached_response) return undefined; @@ -491,8 +512,6 @@ class CacheManager { } async put_cache(key: string, value: Response, type?: string, headers?: {[key: string]:string}) { - const request = new Request("https://_local_cache/cache_request_" + key); - const new_headers = new Headers(); for(const key of value.headers.keys()) new_headers.set(key, value.headers.get(key)); @@ -501,14 +520,25 @@ class CacheManager { for(const key of Object.keys(headers || {})) new_headers.set(key, headers[key]); - await this._cache_category.put(request, new Response(value.body, { + await this._cache_category.put("https://_local_cache/cache_request_" + key, new Response(value.body, { headers: new_headers })); } + + async delete(key: string) { + const flag = await this._cache_category.delete("https://_local_cache/cache_request_" + key, { + ignoreVary: true, + ignoreMethod: true, + ignoreSearch: true + }); + if(!flag) { + console.warn(tr("Failed to delete key %s from cache!"), flag); + } + } } class IconManager { - private static cache: CacheManager; + private static cache: CacheManager = new CacheManager("icons"); handle: FileManager; private _id_urls: {[id:number]:string} = {}; @@ -516,9 +546,15 @@ class IconManager { constructor(handle: FileManager) { this.handle = handle; + } - if(!IconManager.cache) - IconManager.cache = new CacheManager("icons"); + destroy() { + if(URL.revokeObjectURL) { + for(const id of Object.keys(this._id_urls)) + URL.revokeObjectURL(this._id_urls[id]); + } + this._id_urls = undefined; + this._loading_promises = undefined; } async clear_cache() { @@ -548,7 +584,7 @@ class IconManager { return this.handle.download_file("", "/icon_" + id); } - private async _response_url(response: Response) { + private static async _response_url(response: Response) { if(!response.headers.has('X-media-bytes')) throw "missing media bytes"; @@ -573,14 +609,50 @@ class IconManager { await IconManager.cache.setup(); const response = await IconManager.cache.resolve_cached('icon_' + id); //TODO age! - if(response) + if(response) { + const url = await IconManager._response_url(response); + if(this._id_urls[id]) + URL.revokeObjectURL(this._id_urls[id]); return { id: id, - url: (this._id_urls[id] = await this._response_url(response)) + url: url }; + } return undefined; } + private static _static_id_url: {[icon: number]:string} = {}; + private static _static_cached_promise: {[icon: number]:Promise} = {}; + static load_cached_icon(id: number, ignore_age?: boolean) : Promise | Icon { + if(this._static_id_url[id]) { + return { + id: id, + url: this._static_id_url[id] + }; + } + + if(this._static_cached_promise[id]) + return this._static_cached_promise[id]; + + return (this._static_cached_promise[id] = (async () => { + if(!this.cache.setupped()) + await this.cache.setup(); + + const response = await this.cache.resolve_cached('icon_' + id); //TODO age! + if(response) { + const url = await this._response_url(response); + if(this._static_id_url[id]) + URL.revokeObjectURL(this._static_id_url[id]); + this._static_id_url[id] = url; + + return { + id: id, + url: url + }; + } + })()); + } + private async _load_icon(id: number) : Promise { try { let download_key: transfer.DownloadKey; @@ -604,7 +676,10 @@ class IconManager { const media = media_image_type(type); await IconManager.cache.put_cache('icon_' + id, response.clone(), "image/" + media); - const url = (this._id_urls[id] = await this._response_url(response.clone())); + const url = await IconManager._response_url(response.clone()); + if(this._id_urls[id]) + URL.revokeObjectURL(this._id_urls[id]); + this._id_urls[id] = url; this._loading_promises[id] = undefined; return { @@ -644,6 +719,58 @@ class IconManager { throw "icon not found"; } + static generate_tag(icon: Promise | Icon, options?: { + animate?: boolean + }) : JQuery { + options = options || {}; + + let icon_container = $.spawn("div").addClass("icon-container icon_empty"); + let icon_load_image = $.spawn("div").addClass("icon_loading"); + + const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", ""); + const _apply = (icon) => { + let id = icon ? (icon.id >>> 0) : 0; + if (!icon || id == 0) { + icon_load_image.remove(); + icon_load_image = undefined; + return; + } else if (id < 1000) { + icon_load_image.remove(); + icon_load_image = undefined; + + icon_container.removeClass("icon_empty").addClass("icon_em client-group_" + id); + return; + } + + icon_image.attr("src", icon.url); + icon_container.append(icon_image).removeClass("icon_empty"); + + if (typeof (options.animate) !== "boolean" || options.animate) { + icon_image.css("opacity", 0); + + icon_load_image.animate({opacity: 0}, 50, function () { + icon_load_image.remove(); + icon_image.animate({opacity: 1}, 150); + }); + } else { + icon_load_image.remove(); + icon_load_image = undefined; + } + }; + if(icon instanceof Promise) { + icon.then(_apply).catch(error => { + console.error(tr("Could not load icon. Reason: %s"), error); + icon_load_image.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon"); + }); + } else { + _apply(icon as Icon); + } + + if(icon_load_image) + icon_load_image.appendTo(icon_container); + return icon_container; + } + generateTag(id: number, options?: { animate?: boolean }) : JQuery { @@ -651,44 +778,16 @@ class IconManager { id = id >>> 0; if(id == 0 || !id) - return $.spawn("div").addClass("icon_empty"); + return IconManager.generate_tag({id: id, url: ""}, options); else if(id < 1000) - return $.spawn("div").addClass("icon client-group_" + id); + return IconManager.generate_tag({id: id, url: ""}, options); - const icon_container = $.spawn("div").addClass("icon-container icon_empty"); - const icon_image = $.spawn("img").attr("width", 16).attr("height", 16).attr("alt", ""); - if(this._id_urls[id]) { - icon_image.attr("src", this._id_urls[id]).appendTo(icon_container); - icon_container.removeClass("icon_empty"); + return IconManager.generate_tag({id: id, url: this._id_urls[id]}, options); } else { - const icon_load_image = $.spawn("div").addClass("icon_loading"); - icon_load_image.appendTo(icon_container); - - (async () => { - let icon: Icon = await this.resolve_icon(id); - - icon_image.attr("src", icon.url); - icon_container.append(icon_image).removeClass("icon_empty"); - - if(typeof(options.animate) !== "boolean" || options.animate) { - icon_image.css("opacity", 0); - - icon_load_image.animate({opacity: 0}, 50, function () { - icon_load_image.detach(); - icon_image.animate({opacity: 1}, 150); - }); - } else { - icon_load_image.detach(); - } - })().catch(reason => { - console.error(tr("Could not load icon %o. Reason: %s"), id, reason); - icon_load_image.removeClass("icon_loading").addClass("icon client-warning").attr("tag", "Could not load icon " + id); - }); + return IconManager.generate_tag(this.resolve_icon(id), options); } - - return icon_container; } } @@ -713,6 +812,11 @@ class AvatarManager { AvatarManager.cache = new CacheManager("avatars"); } + destroy() { + this._cached_avatars = undefined; + this._loading_promises = undefined; + } + private async _response_url(response: Response, type: ImageType) : Promise { if(!response.headers.has('X-media-bytes')) throw "missing media bytes"; @@ -725,12 +829,12 @@ class AvatarManager { return URL.createObjectURL(blob); } - async resolved_cached?(client_avatar_id: string, avatar_id?: string) : Promise { - let avatar: Avatar = this._cached_avatars[avatar_id]; + async resolved_cached?(client_avatar_id: string, avatar_version?: string) : Promise { + let avatar: Avatar = this._cached_avatars[avatar_version]; if(avatar) { - if(typeof(avatar_id) !== "string" || avatar.avatar_id == avatar_id) + if(typeof(avatar_version) !== "string" || avatar.avatar_id == avatar_version) return avatar; - this._cached_avatars[avatar_id] = (avatar = undefined); + avatar = undefined; } if(!AvatarManager.cache.setupped()) @@ -740,14 +844,14 @@ class AvatarManager { if(!response) return undefined; - let response_avatar_id = response.headers.has("X-avatar-id") ? response.headers.get("X-avatar-id") : undefined; - if(typeof(avatar_id) === "string" && response_avatar_id != avatar_id) + let response_avatar_version = response.headers.has("X-avatar-version") ? response.headers.get("X-avatar-version") : undefined; + if(typeof(avatar_version) === "string" && response_avatar_version != avatar_version) return undefined; const type = image_type(response.headers.get('X-media-bytes')); return this._cached_avatars[client_avatar_id] = { client_avatar_id: client_avatar_id, - avatar_id: avatar_id || response_avatar_id, + avatar_id: avatar_version || response_avatar_version, url: await this._response_url(response, type), type: type }; @@ -758,49 +862,76 @@ class AvatarManager { return this.handle.download_file("", "/avatar_" + client_avatar_id); } - private async _load_avatar(client_avatar_id: string, avatar_id: string) { - let download_key: transfer.DownloadKey; + private async _load_avatar(client_avatar_id: string, avatar_version: string) { try { - download_key = await this.create_avatar_download(client_avatar_id); - } catch(error) { - console.error(tr("Could not request download for avatar %s: %o"), client_avatar_id, error); - throw "Failed to request icon"; + let download_key: transfer.DownloadKey; + try { + download_key = await this.create_avatar_download(client_avatar_id); + } catch(error) { + console.error(tr("Could not request download for avatar %s: %o"), client_avatar_id, error); + throw "failed to request avatar download"; + } + + const downloader = transfer.spawn_download_transfer(download_key); + let response: Response; + try { + response = await downloader.request_file(); + } catch(error) { + console.error(tr("Could not download avatar %s: %o"), client_avatar_id, error); + throw "failed to download avatar"; + } + + const type = image_type(response.headers.get('X-media-bytes')); + const media = media_image_type(type); + + await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, { + "X-avatar-version": avatar_version + }); + const url = await this._response_url(response.clone(), type); + + return this._cached_avatars[client_avatar_id] = { + client_avatar_id: client_avatar_id, + avatar_id: avatar_version, + url: url, + type: type + }; + } finally { + this._loading_promises[client_avatar_id] = undefined; } - - const downloader = transfer.spawn_download_transfer(download_key); - let response: Response; - try { - response = await downloader.request_file(); - } catch(error) { - console.error(tr("Could not download avatar %s: %o"), client_avatar_id, error); - throw "failed to download avatar"; - } - - const type = image_type(response.headers.get('X-media-bytes')); - const media = media_image_type(type); - - await AvatarManager.cache.put_cache('avatar_' + client_avatar_id, response.clone(), "image/" + media, { - "X-avatar-id": avatar_id - }); - const url = await this._response_url(response.clone(), type); - - this._loading_promises[client_avatar_id] = undefined; - return this._cached_avatars[client_avatar_id] = { - client_avatar_id: client_avatar_id, - avatar_id: avatar_id, - url: url, - type: type - }; } - loadAvatar(client_avatar_id: string, avatar_id: string) : Promise { - return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_id)); + /* loads an avatar by the avatar id and optional with the avatar version */ + load_avatar(client_avatar_id: string, avatar_version: string) : Promise { + return this._loading_promises[client_avatar_id] || (this._loading_promises[client_avatar_id] = this._load_avatar(client_avatar_id, avatar_version)); } generate_client_tag(client: ClientEntry) : JQuery { return this.generate_tag(client.avatarId(), client.properties.client_flag_avatar); } + update_cache(client_avatar_id: string, avatar_id: string) { + const _cached: Avatar = this._cached_avatars[client_avatar_id]; + if(_cached) { + if(_cached.avatar_id === avatar_id) + return; /* cache is up2date */ + + console.log(tr("Deleting cached avatar for client %s. Cached version: %s; New version: %s"), client_avatar_id, _cached.avatar_id, avatar_id); + delete this._cached_avatars[client_avatar_id]; + AvatarManager.cache.delete("avatar_" + client_avatar_id).catch(error => { + log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o: %o"), client_avatar_id, error); + }); + } else { + this.resolved_cached(client_avatar_id).then(avatar => { + if(avatar && avatar.avatar_id !== avatar_id) { + /* this time we ensured that its cached */ + this.update_cache(client_avatar_id, avatar_id); + } + }).catch(error => { + log.error(LogCategory.GENERAL, tr("Failed to delete cached avatar for client %o (cache lookup failed): %o"), client_avatar_id, error); + }); + } + } + generate_tag(client_avatar_id: string, avatar_id?: string, options?: { callback_image?: (tag: JQuery) => any, callback_avatar?: (avatar: Avatar) => any @@ -811,7 +942,9 @@ class AvatarManager { let avatar_image = $.spawn("img").attr("alt", tr("Client avatar")); let cached_avatar: Avatar = this._cached_avatars[client_avatar_id]; - if(cached_avatar && cached_avatar.avatar_id == avatar_id) { + if(avatar_id === "") { + avatar_container.append(this.generate_default_image()); + } else if(cached_avatar && cached_avatar.avatar_id == avatar_id) { avatar_image.attr("src", cached_avatar.url); avatar_container.append(avatar_image); if(options.callback_image) @@ -832,7 +965,7 @@ class AvatarManager { } if(!avatar) - avatar = await this.loadAvatar(client_avatar_id, avatar_id) + avatar = await this.load_avatar(client_avatar_id, avatar_id); if(!avatar) throw "failed to load avatar"; @@ -844,7 +977,7 @@ class AvatarManager { avatar_image.css("opacity", 0); avatar_container.append(avatar_image); loader_image.animate({opacity: 0}, 50, () => { - loader_image.detach(); + loader_image.remove(); avatar_image.animate({opacity: 1}, 150, () => { if(options.callback_image) options.callback_image(avatar_image); @@ -859,4 +992,109 @@ class AvatarManager { return avatar_container; } + + unique_id_2_avatar_id(unique_id: string) { + function str2ab(str) { + let buf = new ArrayBuffer(str.length); // 2 bytes for each char + let bufView = new Uint8Array(buf); + for (let i=0, strLen = str.length; i= '0' && c <= '9') + offset = c.charCodeAt(0) - '0'.charCodeAt(0); + else if(c >= 'A' && c <= 'F') + offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A; + else if(c >= 'a' && c <= 'f') + offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A; + result += String.fromCharCode('a'.charCodeAt(0) + offset); + } + return result; + } catch (e) { //invalid base 64 (like music bot etc) + return undefined; + } + } + + private generate_default_image() : JQuery { + return $.spawn("img").attr("src", "img/style/avatar.png").css({width: '100%', height: '100%'}); + } + + generate_chat_tag(client: { id?: number; database_id?: number; }, client_unique_id: string, callback_loaded?: (successfully: boolean, error?: any) => any) : JQuery { + let client_handle; + if(typeof(client.id) == "number") + client_handle = this.handle.handle.channelTree.findClient(client.id); + if(!client_handle && typeof(client.id) == "number") { + client_handle = this.handle.handle.channelTree.find_client_by_dbid(client.database_id); + } + + if(client_handle && client_handle.clientUid() !== client_unique_id) + client_handle = undefined; + + const container = $.spawn("div").addClass("avatar"); + if(client_handle && !client_handle.properties.client_flag_avatar) + return container.append(this.generate_default_image()); + + + const avatar_id = client_handle ? client_handle.avatarId() : this.unique_id_2_avatar_id(client_unique_id); + if(avatar_id) { + if(this._cached_avatars[avatar_id]) { /* Test if we're may able to load the client avatar sync without a loading screen */ + const cache: Avatar = this._cached_avatars[avatar_id]; + console.log("[AVATAR] Using cached avatar. ID: %o | Version: %o (Cached: %o)", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined, cache.avatar_id); + if(!client_handle || client_handle.properties.client_flag_avatar == cache.avatar_id) { + const image = $.spawn("img").attr("src", cache.url).css({width: '100%', height: '100%'}); + return container.append(image); + } + } + + const image_loading = $.spawn("img").attr("src", "img/loading_image.svg").css({width: '100%', height: '100%'}); + + /* lets actually load the avatar */ + (async () => { + let avatar: Avatar; + let loaded_image = this.generate_default_image(); + + console.log("[AVATAR] Resolving avatar. ID: %o | Version: %o", avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); + try { + //TODO: Cache if avatar load failed and try again in some minutes/may just even consider using the default avatar 'till restart + try { + avatar = await this.resolved_cached(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); + } catch(error) { + console.error(tr("Failed to use cached avatar: %o"), error); + } + + if(!avatar) + avatar = await this.load_avatar(avatar_id, client_handle ? client_handle.properties.client_flag_avatar : undefined); + + if(!avatar) + throw "no avatar present!"; + + loaded_image = $.spawn("img").attr("src", avatar.url).css({width: '100%', height: '100%'}); + } catch(error) { + throw error; + } finally { + container.children().remove(); + container.append(loaded_image); + } + })().then(() => callback_loaded && callback_loaded(true)).catch(error => { + log.warn(LogCategory.CLIENT, tr("Failed to load chat avatar for client %s. Error: %o"), client_unique_id, error); + callback_loaded && callback_loaded(false, error); + }); + + image_loading.appendTo(container); + } else { + this.generate_default_image().appendTo(container); + } + + return container; + } } \ No newline at end of file diff --git a/shared/js/PPTListener.ts b/shared/js/PPTListener.ts index 4a4fede0..b0d32f5d 100644 --- a/shared/js/PPTListener.ts +++ b/shared/js/PPTListener.ts @@ -166,7 +166,11 @@ namespace ppt { if(key.key_windows) result += " + " + tr("Win"); - result += " + " + (key.key_code ? key.key_code : tr("unset")); + if(!result && !key.key_code) + return tr("unset"); + + if(key.key_code) + result += " + " + key.key_code; return result.substr(3); } } \ No newline at end of file diff --git a/shared/js/audio/audio.ts b/shared/js/audio/audio.ts index 619b200d..4314f775 100644 --- a/shared/js/audio/audio.ts +++ b/shared/js/audio/audio.ts @@ -2,6 +2,8 @@ namespace audio { export namespace player { export interface Device { device_id: string; + + driver: string; name: string; } } diff --git a/shared/js/bookmarks.ts b/shared/js/bookmarks.ts index 42d6ccd5..d2bff591 100644 --- a/shared/js/bookmarks.ts +++ b/shared/js/bookmarks.ts @@ -9,6 +9,37 @@ namespace bookmarks { return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } + export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { + const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile(); + if(profile.valid()) { + const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler(); + server_connections.set_active_connection_handler(connection); + connection.startConnection( + mark.server_properties.server_address + ":" + mark.server_properties.server_port, + profile, + true, + { + nickname: mark.nickname, + password: mark.server_properties.server_password_hash ? { + password: mark.server_properties.server_password_hash, + hashed: true + } : mark.server_properties.server_password ? { + hashed: false, + password: mark.server_properties.server_password + } : undefined + } + ); + } else { + Modals.spawnConnectModal({}, { + url: mark.server_properties.server_address + ":" + mark.server_properties.server_port, + enforce: true + }, { + profile: profile, + enforce: true + }) + } + }; + export interface ServerProperties { server_address: string; server_port: number; @@ -35,6 +66,8 @@ namespace bookmarks { default_channel_password?: string; connect_profile: string; + + last_icon_id?: number; } export interface DirectoryBookmark { @@ -88,6 +121,19 @@ namespace bookmarks { return bookmark_config().root_bookmark; } + export function bookmarks_flat() : Bookmark[] { + const result: Bookmark[] = []; + const _flat = (bookmark: Bookmark | DirectoryBookmark) => { + if(bookmark.type == BookmarkType.DIRECTORY) + for(const book of (bookmark as DirectoryBookmark).content) + _flat(book); + else + result.push(bookmark as Bookmark); + }; + _flat(bookmark_config().root_bookmark); + return result; + } + function find_bookmark_recursive(parent: DirectoryBookmark, uuid: string) : Bookmark | DirectoryBookmark { for(const entry of parent.content) { if(entry.unique_id == uuid) @@ -169,4 +215,27 @@ namespace bookmarks { export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) { delete_bookmark_recursive(bookmarks(), bookmark) } + + export function add_current_server() { + const ch = server_connections.active_connection_handler(); + if(ch && ch.connected) { + createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:
"), text => true, result => { + if(result) { + const bookmark = create_bookmark(result as string, bookmarks(), { + server_port: ch.serverConnection.remote_address().port, + server_address: ch.serverConnection.remote_address().host, + + server_password: "", + server_password_hash: "" + }, this.connection_handler.getClient().clientNickName()); + save_bookmark(bookmark); + + control_bar.update_bookmarks(); + top_menu.rebuild_bookmarks(); + } + }).open(); + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open(); + } + } } \ No newline at end of file diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index efc4e581..36f5885b 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -1,6 +1,8 @@ /// namespace connection { + import Conversation = chat.channel.Conversation; + export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { constructor(connection: AbstractServerConnection) { super(connection); @@ -23,6 +25,7 @@ namespace connection { this["notifychannelhide"] = this.handleCommandChannelHide; this["notifychannelshow"] = this.handleCommandChannelShow; + this["notifyserverconnectioninfo"] = this.handleNotifyServerConnectionInfo; this["notifycliententerview"] = this.handleCommandClientEnterView; this["notifyclientleftview"] = this.handleCommandClientLeftView; this["notifyclientmoved"] = this.handleNotifyClientMoved; @@ -45,6 +48,9 @@ namespace connection { this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed; this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed; + + this["notifyconversationhistory"] = this.handleNotifyConversationHistory; + this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete; } proxy_command_promise(promise: Promise, options: connection.CommandOptions) { @@ -56,20 +62,21 @@ namespace connection { if(ex instanceof CommandResult) { let res = ex; if(!res.success) { - if(res.id == 2568) { //Permission error - res.message = tr("Insufficient client permissions. Failed on permission ") + this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number).name; + if(res.id == ErrorID.PERMISSION_ERROR) { //Permission error + const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number); + res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown"); this.connection_handler.log.log(log.server.Type.ERROR_PERMISSION, { permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number) }); this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); - } else { + } else if(res.id != ErrorID.EMPTY_RESULT) { this.connection_handler.log.log(log.server.Type.ERROR_CUSTOM, { message: res.extra_message.length == 0 ? res.message : res.extra_message }); } } } else if(typeof(ex) === "string") { - this.connection_handler.chat.serverChat().appendError(tr("Command execution results in ") + ex); + this.connection_handler.log.log(log.server.Type.CONNECTION_COMMAND_ERROR, {error: ex}); } else { console.error(tr("Invalid promise result type: %o. Result:"), typeof (ex)); console.error(ex); @@ -131,6 +138,8 @@ namespace connection { json = json[0]; //Only one bulk + this.connection_handler.channelTree.registerClient(this.connection_handler.getClient()); + this.connection.client.side_bar.channel_conversations().reset(); this.connection.client.clientId = parseInt(json["aclid"]); this.connection.client.getClient().updateVariables({key: "client_nickname", value: json["acn"]}); @@ -146,8 +155,61 @@ namespace connection { } this.connection.client.channelTree.server.updateVariables(false, ...updates); + const properties = this.connection.client.channelTree.server.properties; + /* host message */ + if(properties.virtualserver_hostmessage_mode > 0) { + if(properties.virtualserver_hostmessage_mode == 1) { + /* show in log */ + this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE, { + message: properties.virtualserver_hostmessage + }); + } else { + /* create modal/create modal and quit */ + createModal({ + header: tr("Host message"), + body: MessageHelper.bbcode_chat(properties.virtualserver_hostmessage), + footer: undefined + }).open(); + + if(properties.virtualserver_hostmessage_mode == 3) { + /* first let the client initialize his stuff */ + setTimeout(() => { + this.connection_handler.log.log(log.server.Type.SERVER_HOST_MESSAGE_DISCONNECT, { + message: properties.virtualserver_welcomemessage + }); + + this.connection.disconnect("host message disconnect"); + this.connection_handler.handleDisconnect(DisconnectReason.SERVER_HOSTMESSAGE); + this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED); + }, 100); + } + } + } + + /* welcome message */ + if(properties.virtualserver_welcomemessage) { + this.connection_handler.log.log(log.server.Type.SERVER_WELCOME_MESSAGE, { + message: properties.virtualserver_welcomemessage + }); + } + + /* priviledge key */ + if(properties.virtualserver_ask_for_privilegekey) { + createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.
Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => { + if(!result) return; + const scon = server_connections.active_connection_handler(); + + if(scon.serverConnection.connected) + scon.serverConnection.send_command("tokenuse", { + token: result + }).then(() => { + createInfoModal(tr("Use privilege key"), tr("Privilege key successfully used!")).open(); + }).catch(error => { + createErrorModal(tr("Use privilege key"), MessageHelper.formatMessage(tr("Failed to use privilege key: {}"), error instanceof CommandResult ? error.message : error)).open(); + }); + }, { field_placeholder: 'Enter Privilege Key' }).open(); + } - this.connection_handler.chat.serverChat().name = this.connection.client.channelTree.server.properties["virtualserver_name"]; this.connection_handler.log.log(log.server.Type.CONNECTION_CONNECTED, { own_client: this.connection_handler.getClient().log_data() }); @@ -155,6 +217,16 @@ namespace connection { this.connection.client.onConnected(); } + handleNotifyServerConnectionInfo(json) { + json = json[0]; + + /* everything is a number, so lets parse it */ + for(const key of Object.keys(json)) + json[key] = parseInt(json[key]); + + this.connection_handler.channelTree.server.set_connection_info(json); + } + private createChannelFromJson(json, ignoreOrder: boolean = false) { let tree = this.connection.client.channelTree; @@ -223,9 +295,11 @@ namespace connection { handleCommandChannelDelete(json) { let tree = this.connection.client.channelTree; + const conversations = this.connection.client.side_bar.channel_conversations(); console.log(tr("Got %d channel deletions"), json.length); for(let index = 0; index < json.length; index++) { + conversations.delete_conversation(parseInt(json[index]["cid"])); let channel = tree.findChannel(json[index]["cid"]); if(!channel) { console.error(tr("Invalid channel onDelete (Unknown channel)")); @@ -237,9 +311,11 @@ namespace connection { handleCommandChannelHide(json) { let tree = this.connection.client.channelTree; + const conversations = this.connection.client.side_bar.channel_conversations(); console.log(tr("Got %d channel hides"), json.length); for(let index = 0; index < json.length; index++) { + conversations.delete_conversation(parseInt(json[index]["cid"])); let channel = tree.findChannel(json[index]["cid"]); if(!channel) { console.error(tr("Invalid channel on hide (Unknown channel)")); @@ -282,8 +358,6 @@ namespace connection { client.properties.client_type = parseInt(entry["client_type"]); client = tree.insertClient(client, channel); } else { - if(client == this.connection.client.getClient()) - this.connection_handler.chat.channelChat().name = channel.channelName(); tree.moveClient(client, channel); } @@ -338,32 +412,25 @@ namespace connection { client.updateVariables(...updates); - { - let client_chat = client.chat(false); - if(!client_chat) { - for(const c of this.connection_handler.chat.open_chats()) { - if(c.owner_unique_id == client.properties.client_unique_identifier && c.flag_offline) { - client_chat = c; - break; - } - } - } - - if(client_chat) { - client_chat.appendMessage( - "{0}", true, - $.spawn("div") - .addClass("event-message event-partner-connect") - .text(tr("Your chat partner has reconnected")) - ); - client_chat.flag_offline = false; - client.initialize_chat(client_chat); - } + if(!old_channel) { + /* client new join */ + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + unique_id: client.properties.client_unique_identifier, + client_id: client.clientId(), + name: client.clientNickName() + }, { + create: false, + attach: true + }); } if(client instanceof LocalClientEntry) { + client.initializeListener(); this.connection_handler.update_voice_status(); - this.connection_handler.chat_frame.info_frame().update_channel_talk(); + this.connection_handler.side_bar.info_frame().update_channel_talk(); + const conversations = this.connection.client.side_bar.channel_conversations(); + conversations.set_current_channel(client.currentChannel().channelId); } } } @@ -390,7 +457,7 @@ namespace connection { this.connection.client.handleDisconnect(DisconnectReason.SERVER_CLOSED, entry); } else this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry); - this.connection_handler.chat_frame.info_frame().update_channel_talk(); + this.connection_handler.side_bar.info_frame().update_channel_talk(); return; } @@ -436,19 +503,19 @@ namespace connection { console.error(tr("Unknown client left reason!")); } - { - const chat = client.chat(false); - if(chat) { - chat.flag_offline = true; - chat.onMessageSend = undefined; - chat.onClose = undefined; - chat.appendMessage( - "{0}", true, - $.spawn("div") - .addClass("event-message event-partner-disconnect") - .text(tr("Your chat partner has disconnected")) - ); - } + if(!channel_to) { + /* client left the server */ + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + unique_id: client.properties.client_unique_identifier, + client_id: client.clientId(), + name: client.clientNickName() + }, { + create: false, + attach: false + }); + if(conversation) + conversation.set_state(chat.PrivateConversationState.DISCONNECTED); } } @@ -478,7 +545,6 @@ namespace connection { let self = client instanceof LocalClientEntry; let current_clients: ClientEntry[]; if(self) { - this.connection_handler.chat.channelChat().name = channel_to.channelName(); current_clients = client.channelTree.clientsByChannel(client.currentChannel()); this.connection_handler.update_voice_status(channel_to); } @@ -488,8 +554,20 @@ namespace connection { if(entry !== client && entry.get_audio_handle()) entry.get_audio_handle().abort_replay(); - if(self) - this.connection_handler.chat_frame.info_frame().update_channel_talk(); + if(self) { + const side_bar = this.connection_handler.side_bar; + side_bar.info_frame().update_channel_talk(); + + const conversation_to = side_bar.channel_conversations().conversation(channel_to.channelId, false); + if(conversation_to) + conversation_to.update_private_state(); + + const conversation_from = side_bar.channel_conversations().conversation(channel_from.channelId, false); + if(conversation_from) + conversation_from.update_private_state(); + + side_bar.channel_conversations().update_chat_box(); + } const own_channel = this.connection.client.getClient().currentChannel(); this.connection_handler.log.log(log.server.Type.CLIENT_VIEW_MOVE, { @@ -603,29 +681,65 @@ namespace connection { let mode = json["targetmode"]; if(mode == 1){ - let invoker = this.connection.client.channelTree.findClient(json["invokerid"]); - let target = this.connection.client.channelTree.findClient(json["target"]); - if(!invoker) { //TODO spawn chat (Client is may invisible) - console.error(tr("Got private message from invalid client!")); + //json["invokerid"], json["invokername"], json["invokeruid"] + const target_client_id = parseInt(json["target"]); + const target_own = target_client_id === this.connection.client.getClientId(); + + if(target_own && target_client_id === json["invokerid"]) { + console.error(tr("Received conversation message from invalid client id. Data: %o", json)); return; } - if(!target) { //TODO spawn chat (Client is may invisible) - console.error(tr("Got private message from invalid client!")); + + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + client_id: target_own ? parseInt(json["invokerid"]) : target_client_id, + unique_id: target_own ? json["invokeruid"] : undefined, + name: target_own ? json["invokername"] : undefined + }, { + create: target_own, + attach: target_own + }); + if(!conversation) { + console.error(tr("Received conversation message for unknown conversation! (%s)"), target_own ? tr("Remote message") : tr("Own message")); return; } - if(invoker == this.connection.client.getClient()) { - this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); - target.chat(true).appendMessage("{0}: {1}", true, this.connection.client.getClient().createChatTag(true), MessageHelper.bbcode_chat(json["msg"])); - } else { + + conversation.append_message(json["msg"], { + type: target_own ? "partner" : "self", + name: json["invokername"], + unique_id: json["invokeruid"], + client_id: parseInt(json["invokerid"]) + }); + + if(target_own) { this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); - invoker.chat(true).appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"])); + } else { + this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); } } else if(mode == 2) { + const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + const own_channel_id = this.connection.client.getClient().currentChannel().channelId; + const channel_id = typeof(json["cid"]) !== "undefined" ? parseInt(json["cid"]) : own_channel_id; + const channel = this.connection_handler.channelTree.findChannel(channel_id); + if(json["invokerid"] == this.connection.client.clientId) this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); - else + else if(channel_id == own_channel_id) { this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); - this.connection_handler.chat.channelChat().appendMessage("{0}: {1}", true, ClientEntry.chatTag(json["invokerid"], json["invokername"], json["invokeruid"], true), MessageHelper.bbcode_chat(json["msg"])) + } + + const conversations = this.connection_handler.side_bar.channel_conversations(); + const conversation = conversations.conversation(channel_id); + conversation.register_new_message({ + sender_database_id: invoker ? invoker.properties.client_database_id : 0, + sender_name: json["invokername"], + sender_unique_id: json["invokeruid"], + + timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), + message: json["msg"] + }); + if(conversation.is_unread()) + channel.flag_text_unread = true; } else if(mode == 3) { this.connection_handler.log.log(log.server.Type.GLOBAL_MESSAGE, { message: json["msg"], @@ -635,6 +749,18 @@ namespace connection { client_id: parseInt(json["invokerid"]) } }); + + const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + const conversations = this.connection_handler.side_bar.channel_conversations(); + const conversation = conversations.conversation(0); + conversation.register_new_message({ + sender_database_id: invoker ? invoker.properties.client_database_id : 0, + sender_name: json["invokername"], + sender_unique_id: json["invokeruid"], + + timestamp: typeof(json["timestamp"]) === "undefined" ? Date.now() : parseInt(json["timestamp"]), + message: json["msg"] + }); } } @@ -646,28 +772,20 @@ namespace connection { //clid: "6" //cluid: "YoWmG+dRGKD+Rxb7SPLAM5+B9tY=" - const client = this.connection.client.channelTree.findClient(json["clid"]); - if(!client) { - log.warn(LogCategory.GENERAL, tr("Received chat close for unknown client")); - return; - } - if(client.properties.client_unique_identifier !== json["cluid"]) { - log.warn(LogCategory.GENERAL, tr("Received chat close for client, but unique ids dosn't match. (expected %o, received %o)"), client.properties.client_unique_identifier, json["cluid"]); - return; - } - - const chat = client.chat(false); - if(!chat) { + const conversation_manager = this.connection_handler.side_bar.private_conversations(); + const conversation = conversation_manager.find_conversation({ + client_id: parseInt(json["clid"]), + unique_id: json["cluid"], + name: undefined + }, { + create: false, + attach: false + }); + if(!conversation) { log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open.")); return; } - chat.flag_offline = true; - chat.appendMessage( - "{0}", true, - $.spawn("div") - .addClass("event-message event-partner-closed") - .text(tr("Your chat partner has close the conversation")) - ); + conversation.set_state(chat.PrivateConversationState.CLOSED); } handleNotifyClientUpdated(json) { @@ -811,5 +929,38 @@ namespace connection { this.connection.client.channelTree.deleteClient(client); } } + + handleNotifyConversationHistory(json: any[]) { + const conversations = this.connection.client.side_bar.channel_conversations(); + const conversation = conversations.conversation(parseInt(json[0]["cid"])); + if(!conversation) { + log.warn(LogCategory.NETWORKING, tr("Received conversation history for invalid or unknown conversation (%o)"), json[0]["cid"]); + return; + } + + for(const entry of json) { + conversation.register_new_message({ + message: entry["msg"], + sender_unique_id: entry["sender_unique_id"], + sender_name: entry["sender_name"], + timestamp: parseInt(entry["timestamp"]), + sender_database_id: parseInt(entry["sender_database_id"]) + }, false); + } + conversation.fix_scroll(true); + } + + handleNotifyConversationMessageDelete(json: any[]) { + let conversation: Conversation; + const conversations = this.connection.client.side_bar.channel_conversations(); + for(const entry of json) { + if(typeof(entry["cid"]) !== "undefined") + conversation = conversations.conversation(parseInt(entry["cid"]), false); + if(!conversation) + continue; + + conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"])); + } + } } } \ No newline at end of file diff --git a/shared/js/connection/CommandHelper.ts b/shared/js/connection/CommandHelper.ts index 16e46bb9..3d1081b3 100644 --- a/shared/js/connection/CommandHelper.ts +++ b/shared/js/connection/CommandHelper.ts @@ -12,7 +12,14 @@ namespace connection { initialize() { this.connection.command_handler_boss().register_handler(this); - /* notifyquerylist */ + } + + destroy() { + if(this.connection) { + const hboss = this.connection.command_handler_boss(); + hboss && hboss.unregister_handler(this); + } + this._awaiters_unique_ids = undefined; } handle_command(command: connection.ServerCommand): boolean { diff --git a/shared/js/connection/ConnectionBase.ts b/shared/js/connection/ConnectionBase.ts index ecdda9f8..59a3910f 100644 --- a/shared/js/connection/ConnectionBase.ts +++ b/shared/js/connection/ConnectionBase.ts @@ -39,6 +39,11 @@ namespace connection { abstract remote_address() : ServerAddress; /* only valid when connected */ abstract handshake_handler() : HandshakeHandler; /* only valid when connected */ + + abstract ping() : { + native: number, + javascript?: number + }; } export namespace voice { @@ -128,6 +133,11 @@ namespace connection { this.connection = connection; } + destroy() { + this.command_handlers = undefined; + this.single_command_handler = undefined; + } + register_handler(handler: AbstractCommandHandler) { if(!handler.volatile_handler_boss && handler.handler_boss) throw "handler already registered"; diff --git a/shared/js/connection/ServerConnectionDeclaration.ts b/shared/js/connection/ServerConnectionDeclaration.ts index 00d17908..c027e480 100644 --- a/shared/js/connection/ServerConnectionDeclaration.ts +++ b/shared/js/connection/ServerConnectionDeclaration.ts @@ -4,6 +4,12 @@ enum ErrorID { PLAYLIST_IS_IN_USE = 0x2103, FILE_ALREADY_EXISTS = 2050, + + CLIENT_INVALID_ID = 0x0200, + + CONVERSATION_INVALID_ID = 0x2200, + CONVERSATION_MORE_DATA = 0x2201, + CONVERSATION_IS_PRIVATE = 0x2202 } class CommandResult { diff --git a/shared/js/events.ts b/shared/js/events.ts new file mode 100644 index 00000000..fc058be7 --- /dev/null +++ b/shared/js/events.ts @@ -0,0 +1,7 @@ + +/* TODO: Use a global event bus as event distribute system */ +namespace event { + namespace global { + + } +} \ No newline at end of file diff --git a/shared/js/i18n/localize.ts b/shared/js/i18n/localize.ts index 2ea3fd9a..cf1a1fbc 100644 --- a/shared/js/i18n/localize.ts +++ b/shared/js/i18n/localize.ts @@ -104,7 +104,8 @@ namespace i18n { file.full_url = url; file.path = path; - //TODO validate file + + //TODO: Validate file resolve(file); } catch(error) { log.warn(LogCategory.I18N, tr("Failed to load translation file %s. Failed to parse or process json: %o"), url, error); @@ -119,10 +120,16 @@ namespace i18n { } export function load_file(url: string, path: string) : Promise { - return load_translation_file(url, path).then(result => { + return load_translation_file(url, path).then(async result => { + /* TODO: Improve this test?!*/ + try { + tr("Dummy translation test"); + } catch(error) { + throw "dummy test failed"; + } + log.info(LogCategory.I18N, tr("Successfully initialized up translation file from %s"), url); translations = result.translations; - return Promise.resolve(); }).catch(error => { log.warn(LogCategory.I18N, tr("Failed to load translation file from \"%s\". Error: %o"), url, error); return Promise.reject(error); @@ -292,6 +299,7 @@ namespace i18n { try { await load_file(cfg.current_translation_url, cfg.current_translation_path); } catch (error) { + console.error(tr("Failed to initialize selected translation: %o"), error); createErrorModal(tr("Translation System"), tr("Failed to load current selected translation file.") + "
File: " + cfg.current_translation_url + "
Error: " + error + "
" + tr("Using default fallback translations.")).open(); } } @@ -302,4 +310,7 @@ namespace i18n { // @ts-ignore const tr: typeof i18n.tr = i18n.tr; -const tra: typeof i18n.tra = i18n.tra; \ No newline at end of file +const tra: typeof i18n.tra = i18n.tra; + +(window as any).tr = i18n.tr; +(window as any).tra = i18n.tra; \ No newline at end of file diff --git a/shared/js/load.ts b/shared/js/load.ts index d5789757..fa241745 100644 --- a/shared/js/load.ts +++ b/shared/js/load.ts @@ -11,6 +11,20 @@ namespace app { export function is_web() { return type == Type.WEB_RELEASE || type == Type.WEB_DEBUG; } + + let _ui_version; + export function ui_version() { + if(typeof(_ui_version) !== "string") { + const version_node = document.getElementById("app_version"); + if(!version_node) return undefined; + + const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; + if(!version) return undefined; + + return (_ui_version = version); + } + return _ui_version; + } } namespace loader { @@ -150,19 +164,31 @@ namespace loader { console.groupCollapsed("Executing loading stage %s", Stage[current_stage]); } } + + /* cleanup */ + { + _script_promises = {}; + } console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); } - type SourcePath = string | string[]; + type DependSource = { + url: string; + depends: string[]; + } + type SourcePath = string | DependSource | string[]; - function script_name(path: string | string[]) { + function script_name(path: SourcePath) { if(Array.isArray(path)) { let buffer = ""; let _or = " or "; for(let entry of path) buffer += _or + script_name(entry); return buffer.slice(_or.length); - } else return "" + path + ""; + } else if(typeof(path) === "string") + return "" + path + ""; + else + return "" + path.url + ""; } class SyntaxError { @@ -173,6 +199,7 @@ namespace loader { } } + let _script_promises: {[key: string]: Promise} = {}; export async function load_script(path: SourcePath) : Promise { if(Array.isArray(path)) { //We have some fallback return load_script(path[0]).catch(error => { @@ -185,45 +212,64 @@ namespace loader { return Promise.reject(error); }); } else { - return new Promise((resolve, reject) => { + const source = typeof(path) === "string" ? {url: path, depends: []} : path; + if(source.url.length == 0) return Promise.resolve(); + + return _script_promises[source.url] = (async () => { + /* await depends */ + for(const depend of source.depends) { + if(!_script_promises[depend]) + throw "Missing dependency " + depend; + await _script_promises[depend]; + } + const tag: HTMLScriptElement = document.createElement("script"); - let error = false; - const error_handler = (event: ErrorEvent) => { - if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error - console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); + await new Promise((resolve, reject) => { + let error = false; + const error_handler = (event: ErrorEvent) => { + if(event.filename == tag.src && event.message.indexOf("Illegal constructor") == -1) { //Our tag throw an uncaught error + console.log("msg: %o, url: %o, line: %o, col: %o, error: %o", event.message, event.filename, event.lineno, event.colno, event.error); + window.removeEventListener('error', error_handler as any); + + reject(new SyntaxError(event.error)); + event.preventDefault(); + error = true; + } + }; + window.addEventListener('error', error_handler as any); + + const cleanup = () => { + tag.onerror = undefined; + tag.onload = undefined; + + clearTimeout(timeout_handle); window.removeEventListener('error', error_handler as any); + }; + const timeout_handle = setTimeout(() => { + cleanup(); + reject("timeout"); + }, 5000); + tag.type = "application/javascript"; + tag.async = true; + tag.defer = true; + tag.onerror = error => { + cleanup(); + tag.remove(); + reject(error); + }; + tag.onload = () => { + cleanup(); - reject(new SyntaxError(event.error)); - event.preventDefault(); - error = true; - } - }; - window.addEventListener('error', error_handler as any); + console.debug("Script %o loaded", path); + setTimeout(resolve, 100); + }; - const timeout_handle = setTimeout(() => { - reject("timeout"); - }, 5000); - tag.type = "application/javascript"; - tag.async = true; - tag.defer = true; - tag.onerror = error => { - clearTimeout(timeout_handle); - window.removeEventListener('error', error_handler as any); - tag.remove(); - reject(error); - }; - tag.onload = () => { - clearTimeout(timeout_handle); - window.removeEventListener('error', error_handler as any); - console.debug("Script %o loaded", path); - setTimeout(resolve, 100); - }; + document.getElementById("scripts").appendChild(tag); - document.getElementById("scripts").appendChild(tag); - - tag.src = path + (cache_tag || ""); - }); + tag.src = source.url + (cache_tag || ""); + }); + })(); } } @@ -257,7 +303,7 @@ namespace loader { export async function load_style(path: SourcePath) : Promise { if(Array.isArray(path)) { //We have some fallback - return load_script(path[0]).catch(error => { + return load_style(path[0]).catch(error => { if(error instanceof SyntaxError) return Promise.reject(error.source); @@ -267,6 +313,10 @@ namespace loader { return Promise.reject(error); }); } else { + if(!path) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { const tag: HTMLLinkElement = document.createElement("link"); @@ -283,21 +333,30 @@ namespace loader { }; window.addEventListener('error', error_handler as any); + tag.type = "text/css"; + tag.rel = "stylesheet"; + + const cleanup = () => { + tag.onerror = undefined; + tag.onload = undefined; + + clearTimeout(timeout_handle); + window.removeEventListener('error', error_handler as any); + }; + const timeout_handle = setTimeout(() => { + cleanup(); reject("timeout"); }, 5000); - tag.type = "text/css"; - tag.rel="stylesheet"; - tag.onerror = error => { - clearTimeout(timeout_handle); - window.removeEventListener('error', error_handler as any); + cleanup(); tag.remove(); console.error("File load error for file %s: %o", path, error); reject("failed to load file " + path); }; tag.onload = () => { + cleanup(); { const css: CSSStyleSheet = tag.sheet as CSSStyleSheet; const rules = css.cssRules; @@ -324,8 +383,6 @@ namespace loader { css.insertRule(rule, rules_remove[0]); } - clearTimeout(timeout_handle); - window.removeEventListener('error', error_handler as any); console.debug("Style sheet %o loaded", path); setTimeout(resolve, 100); }; @@ -464,6 +521,7 @@ const loader_javascript = { if(!window.require) { await loader.load_script(["vendor/jquery/jquery.min.js"]); } else { + /* loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { name: "forum sync", priority: 10, @@ -471,26 +529,36 @@ const loader_javascript = { forum.sync_main(); } }); + */ } + await loader.load_script(["vendor/DOMPurify/purify.min.js"]); /* bootstrap material design and libs */ - await loader.load_script(["vendor/popper/popper.js"]); + //await loader.load_script(["vendor/popper/popper.js"]); //depends on popper - await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]); + //await loader.load_script(["vendor/bootstrap-material/bootstrap-material-design.js"]); + /* loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { name: "materialize body", priority: 10, function: async () => { $(document).ready(function() { $('body').bootstrapMaterialDesign(); }); } }); + */ await loader.load_script("vendor/jsrender/jsrender.min.js"); await loader.load_scripts([ ["vendor/xbbcode/src/parser.js"], ["vendor/moment/moment.js"], + ["vendor/twemoji/twemoji.min.js", ""], /* empty string means not required */ + ["vendor/highlight/highlight.pack.js", ""], /* empty string means not required */ + ["vendor/remarkable/remarkable.min.js", ""], /* empty string means not required */ ["adapter/adapter-latest.js", "https://webrtc.github.io/adapter/adapter-latest.js"] ]); + await loader.load_scripts([ + ["vendor/emoji-picker/src/jquery.lsxemojipicker.js"] + ]); if(app.type == app.Type.WEB_RELEASE || app.type == app.Type.CLIENT_RELEASE) { loader.register_task(loader.Stage.JAVASCRIPT, { @@ -547,14 +615,19 @@ const loader_javascript = { //load the profiles "js/profiles/ConnectionProfile.js", "js/profiles/Identity.js", + "js/profiles/identities/teaspeak-forum.js", //Basic UI elements "js/ui/elements/context_divider.js", "js/ui/elements/context_menu.js", "js/ui/elements/modal.js", "js/ui/elements/tab.js", + "js/ui/elements/slider.js", + "js/ui/elements/tooltip.js", //Load UI + "js/ui/modal/ModalAbout.js", + "js/ui/modal/ModalAvatar.js", "js/ui/modal/ModalAvatarList.js", "js/ui/modal/ModalQuery.js", "js/ui/modal/ModalQueryManage.js", @@ -569,13 +642,16 @@ const loader_javascript = { "js/ui/modal/ModalBanClient.js", "js/ui/modal/ModalIconSelect.js", "js/ui/modal/ModalInvite.js", + "js/ui/modal/ModalIdentity.js", "js/ui/modal/ModalBanCreate.js", "js/ui/modal/ModalBanList.js", "js/ui/modal/ModalYesNo.js", "js/ui/modal/ModalPoke.js", - "js/ui/modal/ModalServerGroupDialog.js", + "js/ui/modal/ModalKeySelect.js", + "js/ui/modal/ModalGroupAssignment.js", "js/ui/modal/permission/ModalPermissionEdit.js", - "js/ui/modal/permission/PermissionEditor.js", + {url: "js/ui/modal/permission/CanvasPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]}, + {url: "js/ui/modal/permission/HTMLPermissionEditor.js", depends: ["js/ui/modal/permission/ModalPermissionEdit.js"]}, "js/ui/channel.js", "js/ui/client.js", @@ -590,6 +666,8 @@ const loader_javascript = { "js/ui/frames/chat_frame.js", "js/ui/frames/connection_handlers.js", "js/ui/frames/server_log.js", + "js/ui/frames/hostbanner.js", + "js/ui/frames/MenuBar.js", //Load permissions "js/permission/PermissionManager.js", @@ -640,12 +718,11 @@ const loader_javascript = { //Load codec "js/codec/Codec.js", "js/codec/BasicCodec.js", - "js/codec/CodecWrapperWorker.js", + {url: "js/codec/CodecWrapperWorker.js", depends: ["js/codec/BasicCodec.js"]}, ]); }, load_scripts_debug_client: async () => { await loader.load_scripts([ - ["js/teaforo.js"] ]); }, @@ -685,15 +762,18 @@ const loader_style = { await loader.load_styles([ "vendor/xbbcode/src/xbbcode.css" ]); + await loader.load_styles([ + "vendor/emoji-picker/src/jquery.lsxemojipicker.css" + ]); + await loader.load_styles([ + ["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */ + ]); if(app.type == app.Type.WEB_DEBUG || app.type == app.Type.CLIENT_DEBUG) { await loader_style.load_style_debug(); } else { await loader_style.load_style_release(); } - - /* the material design */ - await loader.load_style("css/theme/bootstrap-material-design.css"); }, load_style_debug: async () => { @@ -706,9 +786,12 @@ const loader_style = { "css/static/ts/tab.css", "css/static/ts/chat.css", "css/static/ts/icons.css", + "css/static/ts/icons_em.css", "css/static/ts/country.css", "css/static/general.css", + "css/static/modal.css", "css/static/modals.css", + "css/static/modal-about.css", "css/static/modal-avatar.css", "css/static/modal-icons.css", "css/static/modal-bookmarks.css", @@ -722,7 +805,9 @@ const loader_style = { "css/static/modal-settings.css", "css/static/modal-poke.css", "css/static/modal-server.css", + "css/static/modal-keyselect.css", "css/static/modal-permissions.css", + "css/static/modal-group-assignment.css", "css/static/music/info_plate.css", "css/static/frame/SelectInfo.css", "css/static/control_bar.css", @@ -730,7 +815,9 @@ const loader_style = { "css/static/frame-chat.css", "css/static/connection_handlers.css", "css/static/server-log.css", - "css/static/htmltags.css" + "css/static/htmltags.css", + "css/static/hostbanner.css", + "css/static/menu-bar.css" ]); }, @@ -740,7 +827,7 @@ const loader_style = { "css/static/main.css", ]); } -} +}; async function load_templates() { try { @@ -773,13 +860,9 @@ async function load_templates() { /* test if all files shall be load from cache or fetch again */ async function check_updates() { const app_version = (() => { - const version_node = document.getElementById("app_version"); - if(!version_node) return undefined; + const version = app.ui_version(); - const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; - if(!version) return undefined; - - if(version == "unknown" || version.replace(/0+/, "").length == 0) + if(!version || version == "unknown" || version.replace(/0+/, "").length == 0) return undefined; return version; @@ -960,6 +1043,11 @@ loader.register_task(loader.Stage.LOADED, { }, priority: 20 }); +loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "lsx emoji picker setup", + function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}), + priority: 10 +}); window["Module"] = window["Module"] || {}; diff --git a/shared/js/main.ts b/shared/js/main.ts index 3d32cdeb..71c88608 100644 --- a/shared/js/main.ts +++ b/shared/js/main.ts @@ -14,10 +14,15 @@ let settings: Settings; const js_render = window.jsrender || $; const native_client = window.require !== undefined; -function getUserMediaFunction() : (constraints: MediaStreamConstraints, success: (stream: MediaStream) => any, fail: (error: any) => any) => any { - if((navigator as any).mediaDevices && (navigator as any).mediaDevices.getUserMedia) - return (settings, success, fail) => { (navigator as any).mediaDevices.getUserMedia(settings).then(success).catch(fail); }; - return (navigator as any).getUserMedia || (navigator as any).webkitGetUserMedia || (navigator as any).mozGetUserMedia; +function getUserMediaFunctionPromise() : (constraints: MediaStreamConstraints) => Promise { + if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) + return constraints => navigator.mediaDevices.getUserMedia(constraints); + + const _callbacked_function = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + if(!_callbacked_function) + return undefined; + + return constraints => new Promise((resolve, reject) => _callbacked_function(constraints, resolve, reject)); } interface Window { @@ -36,18 +41,41 @@ function setup_close() { if(!native_client) { event.returnValue = "Are you really sure?
You're still connected!"; } else { + const do_exit = () => { + const dp = server_connections.server_connection_handlers().map(e => { + if(e.serverConnection.connected()) + return e.serverConnection.disconnect(tr("client closed")); + return Promise.resolve(); + }).map(e => e.catch(error => { + console.warn(tr("Failed to disconnect from server on client close: %o"), e); + })); + + const exit = () => { + const {remote} = require('electron'); + remote.getCurrentWindow().close(); + }; + + Promise.all(dp).then(exit); + /* force exit after 2500ms */ + setTimeout(exit, 2500); + }; if(window.open_connected_question) { event.preventDefault(); event.returnValue = "question"; window.open_connected_question().then(result => { if(result) { - window.onbeforeunload = undefined; + /* prevent quitting because we try to disconnect */ + window.onbeforeunload = e => e.preventDefault(); - const {remote} = require('electron'); - remote.getCurrentWindow().close(); + /* allow a force quit after 5 seconds */ + setTimeout(() => window.onbeforeunload, 5000); + do_exit(); } }); - } else { /* we're in debugging mode */ } + } else { + /* we're in debugging mode */ + do_exit(); + } } } }; @@ -102,7 +130,6 @@ async function initialize() { bipc.setup(); } - async function initialize_app() { const display_load_error = message => { if(typeof(display_critical_load) !== "undefined") @@ -112,7 +139,10 @@ async function initialize_app() { }; try { //Initialize main template - const main = $("#tmpl_main").renderTag().dividerfy(); + const main = $("#tmpl_main").renderTag({ + multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION), + app_version: app.ui_version() + }).dividerfy(); $("body").append(main); } catch(error) { @@ -126,7 +156,7 @@ async function initialize_app() { if(!audio.player.initialize()) console.warn(tr("Failed to initialize audio controller!")); if(audio.player.set_master_volume) - audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER, 1) / 100); + audio.player.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER) / 100); else console.warn("Client does not support audio.player.set_master_volume()... May client is too old?"); if(audio.recorder.device_refresh_available()) @@ -138,7 +168,7 @@ async function initialize_app() { sound.initialize().then(() => { console.log(tr("Sounds initialitzed")); }); - sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS, 1) / 100); + sound.set_master_volume(settings.global(Settings.KEY_SOUND_MASTER_SOUNDS) / 100); await profiles.load(); @@ -153,10 +183,6 @@ async function initialize_app() { setup_close(); } -function ab2str(buf) { - return String.fromCharCode.apply(null, new Uint16Array(buf)); -} - function str2ab8(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); @@ -177,68 +203,58 @@ function arrayBufferBase64(base64: string) { return buf; } -function base64ArrayBuffer(arrayBuffer) { - var base64 = '' - var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +function base64_encode_ab(source: ArrayBufferLike) { + const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let base64 = ""; - var bytes = new Uint8Array(arrayBuffer) - var byteLength = bytes.byteLength - var byteRemainder = byteLength % 3 - var mainLength = byteLength - byteRemainder + const bytes = new Uint8Array(source); + const byte_length = bytes.byteLength; + const byte_reminder = byte_length % 3; + const main_length = byte_length - byte_reminder; - var a, b, c, d - var chunk + let a, b, c, d; + let chunk; // Main loop deals with bytes in chunks of 3 - for (var i = 0; i < mainLength; i = i + 3) { + for (let i = 0; i < main_length; i = i + 3) { // Combine the three bytes into a single integer - chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; // Use bitmasks to extract 6-bit segments from the triplet - a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18 - b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12 - c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6 - d = chunk & 63 // 63 = 2^6 - 1 + a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 + b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 + c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 + d = (chunk & 63) >> 0; // 63 = (2^6 - 1) << 0 // Convert the raw binary segments to the appropriate ASCII encoding - base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; } // Deal with the remaining bytes and padding - if (byteRemainder == 1) { - chunk = bytes[mainLength] + if (byte_reminder == 1) { + chunk = bytes[main_length]; - a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2 + a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 // Set the 4 least significant bits to zero - b = (chunk & 3) << 4 // 3 = 2^2 - 1 + b = (chunk & 3) << 4; // 3 = 2^2 - 1 - base64 += encodings[a] + encodings[b] + '==' - } else if (byteRemainder == 2) { - chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] + base64 += encodings[a] + encodings[b] + '=='; + } else if (byte_reminder == 2) { + chunk = (bytes[main_length] << 8) | bytes[main_length + 1]; - a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 - b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 + a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 // Set the 2 least significant bits to zero - c = (chunk & 15) << 2 // 15 = 2^4 - 1 + c = (chunk & 15) << 2; // 15 = 2^4 - 1 - base64 += encodings[a] + encodings[b] + encodings[c] + '=' + base64 += encodings[a] + encodings[b] + encodings[c] + '='; } return base64 } -function Base64EncodeUrl(str){ - return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); -} - -function Base64DecodeUrl(str: string, pad?: boolean){ - if(typeof(pad) === 'undefined' || pad) - str = (str + '===').slice(0, str.length + (str.length % 4)); - return str.replace(/-/g, '+').replace(/_/g, '/'); -} - /* class TestProxy extends bipc.MethodProxy { constructor(params: bipc.MethodProxyConnectParameters) { @@ -282,7 +298,6 @@ interface Window { } */ - function main() { /* window.proxy_instance = new TestProxy({ @@ -299,6 +314,23 @@ function main() { */ //http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1 + /* initialize font */ + { + const font = settings.static_global(Settings.KEY_FONT_SIZE, parseInt(getComputedStyle(document.body).fontSize)); + $(document.body).css("font-size", font + "px"); + } + + /* context menu prevent */ + $(document).on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + if(!settings.static_global(Settings.KEY_DISABLE_GLOBAL_CONTEXT_MENU)) + event.preventDefault(); + }); + + top_menu.initialize(); + server_connections = new ServerConnectionManager($("#connection-handlers")); control_bar.initialise(); /* before connection handler to allow property apply */ @@ -306,7 +338,7 @@ function main() { initial_handler.acquire_recorder(default_recorder, false); control_bar.set_connection_handler(initial_handler); /** Setup the XF forum identity **/ - profiles.identities.setup_forum(); + profiles.identities.update_forum(); let _resize_timeout: NodeJS.Timer; $(window).on('resize', event => { @@ -334,11 +366,6 @@ function main() { console.log("Received user count update: %o", status); }); - /* - setTimeout(() => { - Modals.spawnAvatarList(globalClient); - }, 1000); - */ (window).test_upload = (message?: string) => { message = message || "Hello World"; @@ -366,16 +393,6 @@ function main() { }; server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]); - const convs = server_connections.active_connection_handler().chat_frame.private_conversations(); - let conv = convs.create_conversation("xxxx0", "WolverinDEV"); - conv = convs.create_conversation("xxxx1", "Darkatzu"); - conv = convs.create_conversation("xxxx2", "ZameXxX"); - conv.set_unread_flag(true); - - conv = convs.create_conversation("xxxx3", "Vagur"); - - //for(let i = 0; i < 100; i++) - // convs.create_conversation('xx' + i, "WolverinDEV #" + i); if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) { const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id); @@ -389,7 +406,7 @@ function main() { if(profile && profile.valid()) { const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler(); - connection.startConnection(address, profile, { + connection.startConnection(address, profile, true, { nickname: username, password: password.length > 0 ? { password: password, @@ -397,7 +414,7 @@ function main() { } : undefined }); } else { - Modals.spawnConnectModal({ + Modals.spawnConnectModal({},{ url: address, enforce: true }, { @@ -406,6 +423,18 @@ function main() { }); } } + + setTimeout(() => { + const connection = server_connections.active_connection_handler(); + /* + Modals.createChannelModal(connection, undefined, undefined, connection.permissions, (cb, perms) => { + + }); + */ + //Modals.createServerModal(connection.channelTree.server, properties => Promise.resolve()); + }, 1000); + //Modals.spawnSettingsModal("audio-sounds"); + //Modals.spawnKeySelect(console.log); } const task_teaweb_starter: loader.Task = { diff --git a/shared/js/permission/GroupManager.ts b/shared/js/permission/GroupManager.ts index b9131c6a..c6cec295 100644 --- a/shared/js/permission/GroupManager.ts +++ b/shared/js/permission/GroupManager.ts @@ -55,7 +55,11 @@ class Group { this.handle.handle.channelTree.clientsByGroup(this).forEach(client => { client.updateGroupIcon(this); }); - } + } else if(key == "sortid") + this.handle.handle.channelTree.clientsByGroup(this).forEach(client => { + client.update_group_icon_order(); + }); + } } @@ -73,6 +77,12 @@ class GroupManager extends connection.AbstractCommandHandler { this.handle = client; } + destroy() { + this.handle.serverConnection && this.handle.serverConnection.command_handler_boss().unregister_handler(this); + this.serverGroups = undefined; + this.channelGroups = undefined; + } + handle_command(command: connection.ServerCommand): boolean { switch (command.command) { case "notifyservergrouplist": @@ -94,6 +104,11 @@ class GroupManager extends connection.AbstractCommandHandler { static sorter() : (a: Group, b: Group) => number { return (a, b) => { + if(!a) + return b ? 1 : 0; + if(!b) + return a ? -1 : 0; + if(a.properties.sortid > b.properties.sortid) return 1; if(a.properties.sortid < b.properties.sortid) diff --git a/shared/js/permission/PermissionManager.ts b/shared/js/permission/PermissionManager.ts index cf813d24..638982ae 100644 --- a/shared/js/permission/PermissionManager.ts +++ b/shared/js/permission/PermissionManager.ts @@ -396,8 +396,6 @@ class PermissionValue { } class NeededPermissionValue extends PermissionValue { - changeListener: ((newValue: number) => void)[] = []; - constructor(type, value) { super(type, value); } @@ -424,6 +422,8 @@ class PermissionManager extends connection.AbstractCommandHandler { permissionGroups: PermissionGroup[] = []; neededPermissions: NeededPermissionValue[] = []; + needed_permission_change_listener: {[permission: string]:(() => any)[]} = {}; + requests_channel_permissions: ChannelPermissionRequest[] = []; requests_client_permissions: TeaPermissionRequest[] = []; requests_client_channel_permissions: TeaPermissionRequest[] = []; @@ -515,6 +515,24 @@ class PermissionManager extends connection.AbstractCommandHandler { this.handle = client; } + destroy() { + this.handle.serverConnection && this.handle.serverConnection.command_handler_boss().unregister_handler(this); + this.needed_permission_change_listener = {}; + + this.permissionList = undefined; + this.permissionGroups = undefined; + + this.neededPermissions = undefined; + + this.requests_channel_permissions = undefined; + this.requests_client_permissions = undefined; + this.requests_client_channel_permissions = undefined; + this.requests_playlist_permissions = undefined; + + this.initializedListener = undefined; + this._cacheNeededPermissions = undefined; + } + handle_command(command: connection.ServerCommand): boolean { switch (command.command) { case "notifyclientneededpermissions": @@ -631,8 +649,8 @@ class PermissionManager extends connection.AbstractCommandHandler { if(entry.value == parseInt(e["permvalue"])) continue; entry.value = parseInt(e["permvalue"]); - for(let listener of entry.changeListener) - listener(entry.value); + for(const listener of this.needed_permission_change_listener[entry.type.name] || []) + listener(); table_entries.push({ "permission": entry.type.name, @@ -643,15 +661,28 @@ class PermissionManager extends connection.AbstractCommandHandler { log.table("Needed client permissions", table_entries); group.end(); - //TODO tr - log.debug(LogCategory.PERMISSIONS, "Dropping " + copy.length + " needed permissions and added " + addcount + " permissions."); + log.debug(LogCategory.PERMISSIONS, tr("Dropping %o needed permissions and added %o permissions."), copy.length, addcount); for(let e of copy) { e.value = -2; - for(let listener of e.changeListener) - listener(e.value); + for(const listener of this.needed_permission_change_listener[e.type.name] || []) + listener(); } } + register_needed_permission(key: PermissionType, listener: () => any) { + const array = this.needed_permission_change_listener[key] || []; + array.push(listener); + this.needed_permission_change_listener[key] = array; + } + + unregister_needed_permission(key: PermissionType, listener: () => any) { + const array = this.needed_permission_change_listener[key]; + if(!array) return; + + array.remove(listener); + this.needed_permission_change_listener[key] = array.length > 0 ? array : undefined; + } + private onChannelPermList(json) { let channelId: number = parseInt(json[0]["cid"]); @@ -780,7 +811,7 @@ class PermissionManager extends connection.AbstractCommandHandler { return request.promise; } - neededPermission(key: number | string | PermissionType | PermissionInfo) : PermissionValue { + neededPermission(key: number | string | PermissionType | PermissionInfo) : NeededPermissionValue { for(let perm of this.neededPermissions) if(perm.type.id == key || perm.type.name == key || perm.type == key) return perm; diff --git a/shared/js/profiles/ConnectionProfile.ts b/shared/js/profiles/ConnectionProfile.ts index fd666c2d..ddd707cd 100644 --- a/shared/js/profiles/ConnectionProfile.ts +++ b/shared/js/profiles/ConnectionProfile.ts @@ -66,7 +66,7 @@ namespace profiles { const identity = this.selected_identity(); if(!identity || !identity.valid()) return false; - return this.default_username !== undefined; + return true; } } diff --git a/shared/js/profiles/identities/NameIdentity.ts b/shared/js/profiles/identities/NameIdentity.ts index 26bcd506..02af0b00 100644 --- a/shared/js/profiles/identities/NameIdentity.ts +++ b/shared/js/profiles/identities/NameIdentity.ts @@ -60,7 +60,7 @@ namespace profiles.identities { } valid(): boolean { - return this._name != undefined && this._name.length >= 3; + return this._name != undefined && this._name.length >= 5; } decode(data) : Promise { diff --git a/shared/js/profiles/identities/TeaForumIdentity.ts b/shared/js/profiles/identities/TeaForumIdentity.ts index c7088bfb..95fce4e9 100644 --- a/shared/js/profiles/identities/TeaForumIdentity.ts +++ b/shared/js/profiles/identities/TeaForumIdentity.ts @@ -17,7 +17,7 @@ namespace profiles.identities { this.connection.send_command("handshakebegin", { intention: 0, authentication_method: this.identity.type(), - data: this.identity.data_json() + data: this.identity.data().data_json() }).catch(error => { log.error(LogCategory.IDENTITIES, tr("Failed to initialize TeaForum based handshake. Error: %o"), error); @@ -30,7 +30,7 @@ namespace profiles.identities { private handle_proof(json) { this.connection.send_command("handshakeindentityproof", { - proof: this.identity.data_sign() + proof: this.identity.data().data_sign() }).catch(error => { log.error(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error); @@ -52,73 +52,50 @@ namespace profiles.identities { } export class TeaForumIdentity implements Identity { - private identity_data: string; - private identity_data_raw: string; - private identity_data_sign: string; + private readonly identity_data: forum.Data; valid() : boolean { - return this.identity_data_raw.length > 0 && this.identity_data_raw.length > 0 && this.identity_data_sign.length > 0; + return !!this.identity_data && !this.identity_data.is_expired(); } - constructor(data: string, sign: string) { - this.identity_data_raw = data; - this.identity_data_sign = sign; - try { - this.identity_data = data ? JSON.parse(this.identity_data_raw) : undefined; - } catch(error) { } + constructor(data: forum.Data) { + this.identity_data = data; } - data_json() : string { return this.identity_data_raw; } - data_sign() : string { return this.identity_data_sign; } - - name() : string { return this.identity_data["user_name"]; } - uid() : string { return "TeaForo#" + this.identity_data["user_id"]; } - type() : IdentitifyType { return IdentitifyType.TEAFORO; } - - forum_user_id() { return this.identity_data["user_id"]; } - forum_user_group() { return this.identity_data["user_group_id"]; } - is_stuff() : boolean { return this.identity_data["is_staff"]; } - is_premium() : boolean { return (this.identity_data["user_groups"]).indexOf(5) != -1; } - data_age() : Date { return new Date(this.identity_data["data_age"]); } - - /* - $user_data["user_id"] = $user->user_id; - $user_data["user_name"] = $user->username; - $user_data["user_group"] = $user->user_group_id; - $user_data["user_groups"] = $user->secondary_group_ids; - - $user_data["trophy_points"] = $user->trophy_points; - $user_data["register_date"] = $user->register_date; - $user_data["is_staff"] = $user->is_staff; - $user_data["is_admin"] = $user->is_admin; - $user_data["is_super_admin"] = $user->is_super_admin; - $user_data["is_banned"] = $user->is_banned; - - $user_data["data_age"] = milliseconds(); - */ + data() : forum.Data { + return this.identity_data; + } decode(data) : Promise { data = JSON.parse(data); if(data.version !== 1) throw "invalid version"; - this.identity_data_raw = data["identity_data"]; - this.identity_data_sign = data["identity_sign"]; - this.identity_data = JSON.parse(this.identity_data); return; } - encode?() : string { + encode() : string { return JSON.stringify({ - version: 1, - identity_data: this.identity_data_raw, - identity_sign: this.identity_data_sign + version: 1 }); } spawn_identity_handshake_handler(connection: connection.AbstractServerConnection) : connection.HandshakeIdentityHandler { return new TeaForumHandshakeHandler(connection, this); } + + name(): string { + return (this.identity_data ? this.identity_data.name() : "Another TeaSpeak user"); + } + + type(): profiles.identities.IdentitifyType { + return IdentitifyType.TEAFORO; + } + + uid(): string { + //FIXME: Real UID! + return "TeaForo#" + ((this.identity_data ? this.identity_data.name() : "Another TeaSpeak user")); + } } let static_identity: TeaForumIdentity; @@ -127,12 +104,10 @@ namespace profiles.identities { static_identity = identity; } - export function setup_forum() { - const user_data = settings.static("forum_user_data") as string; - const user_sign = settings.static("forum_user_sign") as string; - - if(user_data && user_sign) - static_identity = new TeaForumIdentity(user_data, user_sign); + export function update_forum() { + if(forum.logged_in() && (!static_identity || static_identity.data() !== forum.data())) { + static_identity = new TeaForumIdentity(forum.data()); + } } export function valid_static_forum_identity() : boolean { diff --git a/shared/js/profiles/identities/TeamSpeakIdentity.ts b/shared/js/profiles/identities/TeamSpeakIdentity.ts index 2d3559d1..bf5d620c 100644 --- a/shared/js/profiles/identities/TeamSpeakIdentity.ts +++ b/shared/js/profiles/identities/TeamSpeakIdentity.ts @@ -2,6 +2,20 @@ namespace profiles.identities { export namespace CryptoHelper { + export function base64_url_encode(str){ + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); + } + + export function base64_url_decode(str: string, pad?: boolean){ + if(typeof(pad) === 'undefined' || pad) + str = (str + '===').slice(0, str.length + (str.length % 4)); + return str.replace(/-/g, '+').replace(/_/g, '/'); + } + + export function arraybuffer_to_string(buf) { + return String.fromCharCode.apply(null, new Uint16Array(buf)); + } + export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) { /* Tomcrypt public key export: @@ -50,7 +64,7 @@ namespace profiles.identities { buffer[index++] = 0x02; /* type */ buffer[index++] = 0x20; /* length */ - const raw = atob(Base64DecodeUrl(key_data.x, false)); + const raw = atob(base64_url_decode(key_data.x, false)); if(raw.charCodeAt(0) > 0x7F) { buffer[index - 1] += 1; buffer[index++] = 0; @@ -68,7 +82,7 @@ namespace profiles.identities { buffer[index++] = 0x02; /* type */ buffer[index++] = 0x20; /* length */ - const raw = atob(Base64DecodeUrl(key_data.y, false)); + const raw = atob(base64_url_decode(key_data.y, false)); if(raw.charCodeAt(0) > 0x7F) { buffer[index - 1] += 1; buffer[index++] = 0; @@ -87,7 +101,7 @@ namespace profiles.identities { buffer[index++] = 0x02; /* type */ buffer[index++] = 0x20; /* length */ - const raw = atob(Base64DecodeUrl(key_data.d, false)); + const raw = atob(base64_url_decode(key_data.d, false)); if(raw.charCodeAt(0) > 0x7F) { buffer[index - 1] += 1; buffer[index++] = 0; @@ -104,7 +118,7 @@ namespace profiles.identities { buffer[1] = index - 2; /* set the final sequence length */ - return base64ArrayBuffer(buffer.buffer.slice(0, index)); + return base64_encode_ab(buffer.buffer.slice(0, index)); } const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e"; @@ -125,7 +139,7 @@ namespace profiles.identities { for(let i = 0; i < length; i++) buffer[i] ^= crypt_key.charCodeAt(i); - return ab2str(buffer); + return arraybuffer_to_string(buffer); } export async function encrypt_ts_identity(buffer: Uint8Array) : Promise { @@ -137,7 +151,7 @@ namespace profiles.identities { for(let i = 0; i < 20; i++) buffer[i] ^= hash[i]; - return base64ArrayBuffer(buffer); + return base64_encode_ab(buffer); } /** @@ -185,9 +199,9 @@ namespace profiles.identities { */ return { crv: "P-256", - d: Base64EncodeUrl(btoa(k)), - x: Base64EncodeUrl(btoa(x)), - y: Base64EncodeUrl(btoa(y)), + d: base64_url_encode(btoa(k)), + x: base64_url_encode(btoa(x)), + y: base64_url_encode(btoa(y)), ext: true, key_ops:["deriveKey", "sign"], @@ -587,7 +601,7 @@ namespace profiles.identities { if(carry) char_result.push(49); - return String.fromCharCode.apply(null, char_result.reverse()); + return String.fromCharCode.apply(null, char_result.slice().reverse()); } @@ -774,7 +788,7 @@ namespace profiles.identities { try { this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true); - this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key)); + this._unique_id = base64_encode_ab(await sha.sha1(this.public_key)); } catch(error) { log.error(LogCategory.IDENTITIES, error); throw "failed to calculate unique id"; @@ -840,7 +854,7 @@ namespace profiles.identities { } buffer[1] = index - 2; - return base64ArrayBuffer(buffer.subarray(0, index)); + return base64_encode_ab(buffer.subarray(0, index)); } spawn_identity_handshake_handler(connection: connection.AbstractServerConnection): connection.HandshakeIdentityHandler { diff --git a/shared/js/profiles/identities/teaspeak-forum.ts b/shared/js/profiles/identities/teaspeak-forum.ts new file mode 100644 index 00000000..1ab6cd53 --- /dev/null +++ b/shared/js/profiles/identities/teaspeak-forum.ts @@ -0,0 +1,366 @@ +interface Window { + grecaptcha: GReCaptcha; +} + +interface GReCaptcha { + render(container: string | HTMLElement, parameters: { + sitekey: string; + theme?: "dark" | "light"; + size?: "compact" | "normal"; + + tabindex?: number; + + callback?: (token: string) => any; + "expired-callback"?: () => any; + "error-callback"?: (error: any) => any; + }) : string; /* widget_id */ + + reset(widget_id?: string); +} + +namespace forum { + export namespace gcaptcha { + export async function initialize() { + if(typeof(window.grecaptcha) === "undefined") { + let script = document.createElement("script"); + script.async = true; + + let timeout; + const callback_name = "captcha_callback_" + Math.random().toString().replace(".", ""); + try { + await new Promise((resolve, reject) => { + script.onerror = reject; + window[callback_name] = resolve; + script.src = "https://www.google.com/recaptcha/api.js?onload=" + encodeURIComponent(callback_name) + "&render=explicit"; + + document.body.append(script); + timeout = setTimeout(() => reject("timeout"), 15000); + }); + } catch(error) { + script.remove(); + script = undefined; + + console.error(tr("Failed to fetch recaptcha javascript source: %o"), error); + throw tr("failed to download source"); + } finally { + if(script) + script.onerror = undefined; + delete window[callback_name]; + clearTimeout(timeout); + } + } + + if(typeof(window.grecaptcha) === "undefined") + throw tr("failed to load recaptcha"); + } + + export async function spawn(container: JQuery, key: string, callback_data: (token: string) => any) { + try { + await initialize(); + } catch(error) { + console.error(tr("Failed to initialize G-Recaptcha. Error: %o"), error); + throw tr("initialisation failed"); + } + if(container.attr("captcha-uuid")) + window.grecaptcha.reset(container.attr("captcha-uuid")); + else { + container.attr("captcha-uuid", window.grecaptcha.render(container[0], { + "sitekey": key, + callback: callback_data + })); + } + } + } + + function api_url() { + return settings.static_global(Settings.KEY_TEAFORO_URL); + } + + export class Data { + readonly auth_key: string; + readonly raw: string; + readonly sign: string; + + parsed: { + user_id: number; + user_name: string; + + data_age: number; + + user_group_id: number; + + is_staff: boolean; + user_groups: number[]; + }; + + constructor(auth: string, raw: string, sign: string) { + this.auth_key = auth; + this.raw = raw; + this.sign = sign; + + this.parsed = JSON.parse(raw); + } + + + data_json() : string { return this.raw; } + data_sign() : string { return this.sign; } + + name() : string { return this.parsed.user_name; } + + user_id() { return this.parsed.user_id; } + user_group() { return this.parsed.user_group_id; } + + is_stuff() : boolean { return this.parsed.is_staff; } + is_premium() : boolean { return this.parsed.user_groups.indexOf(5) != -1; } + + data_age() : Date { return new Date(this.parsed.data_age); } + + is_expired() : boolean { return this.parsed.data_age + 48 * 60 * 60 * 1000 < Date.now(); } + should_renew() : boolean { return this.parsed.data_age + 24 * 60 * 60 * 1000 < Date.now(); } /* renew data all 24hrs */ + } + let _data: Data | undefined; + + export function logged_in() : boolean { + return !!_data && !_data.is_expired(); + } + + export function data() : Data { return _data; } + + export interface LoginResult { + status: "success" | "captcha" | "error"; + + error_message?: string; + captcha?: { + type: "gre-captcha" | "unknown"; + data: any; /* in case of gre-captcha it would be the side key */ + }; + } + + export async function login(username: string, password: string, captcha?: any) : Promise { + let response; + try { + response = await new Promise((resolve, reject) => { + $.ajax({ + url: api_url() + "?web-api/v1/login", + type: "POST", + cache: false, + data: { + username: username, + password: password, + remember: true, + "g-recaptcha-response": captcha + }, + + crossDomain: true, + + success: resolve, + error: (xhr, status, error) => { + console.log(tr("Login request failed %o: %o"), status, error); + reject(tr("request failed")); + } + }) + }); + } catch(error) { + return { + status: "error", + error_message: tr("failed to send login request") + }; + } + + if(response["status"] !== "ok") { + console.error(tr("Response status not okey. Error happend: %o"), response); + return { + status: "error", + error_message: (response["errors"] || [])[0] || tr("Unknown error") + }; + } + + if(!response["success"]) { + console.error(tr("Login failed. Response %o"), response); + + let message = tr("failed to login"); + let captcha; + /* user/password wrong | and maybe captcha required */ + if(response["code"] == 1 || response["code"] == 3) + message = tr("Invalid username or password"); + if(response["code"] == 2 || response["code"] == 3) { + captcha = { + type: response["captcha"]["type"], + data: response["captcha"]["siteKey"] //TODO: Why so static here? + }; + if(response["code"] == 2) + message = tr("captcha required"); + } + + return { + status: typeof(captcha) !== "undefined" ? "captcha" : "error", + error_message: message, + captcha: captcha + }; + } + //document.cookie = "user_data=" + response["data"] + ";path=/"; + //document.cookie = "user_sign=" + response["sign"] + ";path=/"; + + try { + _data = new Data(response["auth-key"], response["data"], response["sign"]); + localStorage.setItem("teaspeak-forum-data", response["data"]); + localStorage.setItem("teaspeak-forum-sign", response["sign"]); + localStorage.setItem("teaspeak-forum-auth", response["auth-key"]); + profiles.identities.update_forum(); + } catch(error) { + console.error(tr("Failed to parse forum given data: %o"), error); + return { + status: "error", + error_message: tr("Failed to parse response data") + } + } + + return { + status: "success" + }; + } + + export async function renew_data() : Promise<"success" | "login-required"> { + let response; + try { + response = await new Promise((resolve, reject) => { + $.ajax({ + url: api_url() + "?web-api/v1/renew-data", + type: "GET", + cache: false, + + crossDomain: true, + + data: { + "auth-key": _data.auth_key + }, + + success: resolve, + error: (xhr, status, error) => { + console.log(tr("Renew request failed %o: %o"), status, error); + reject(tr("request failed")); + } + }) + }); + } catch(error) { + throw tr("failed to send renew request"); + } + + if(response["status"] !== "ok") { + console.error(tr("Response status not okey. Error happend: %o"), response); + throw (response["errors"] || [])[0] || tr("Unknown error"); + } + + if(!response["success"]) { + if(response["code"] == 1) { + return "login-required"; + } + throw "invalid error code (" + response["code"] + ")"; + } + if(!response["data"] || !response["sign"]) + throw tr("response missing data"); + + console.debug(tr("Renew succeeded. Parsing data.")); + + try { + _data = new Data(_data.auth_key, response["data"], response["sign"]); + localStorage.setItem("teaspeak-forum-data", response["data"]); + localStorage.setItem("teaspeak-forum-sign", response["sign"]); + profiles.identities.update_forum(); + } catch(error) { + console.error(tr("Failed to parse forum given data: %o"), error); + throw tr("failed to parse data"); + } + + return "success"; + } + + export async function logout() : Promise { + if(!logged_in()) + return; + + let response; + try { + response = await new Promise((resolve, reject) => { + $.ajax({ + url: api_url() + "?web-api/v1/logout", + type: "GET", + cache: false, + + crossDomain: true, + + data: { + "auth-key": _data.auth_key + }, + + success: resolve, + error: (xhr, status, error) => { + console.log(tr("Logout request failed %o: %o"), status, error); + reject(tr("request failed")); + } + }) + }); + } catch(error) { + throw tr("failed to send logout request"); + } + + if(response["status"] !== "ok") { + console.error(tr("Response status not okey. Error happend: %o"), response); + throw (response["errors"] || [])[0] || tr("Unknown error"); + } + + if(!response["success"]) { + /* code 1 means not logged in, its an success */ + if(response["code"] != 1) { + throw "invalid error code (" + response["code"] + ")"; + } + } + + _data = undefined; + localStorage.removeItem("teaspeak-forum-data"); + localStorage.removeItem("teaspeak-forum-sign"); + localStorage.removeItem("teaspeak-forum-auth"); + profiles.identities.update_forum(); + } + + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "TeaForo initialize", + priority: 10, + function: async () => { + const raw_data = localStorage.getItem("teaspeak-forum-data"); + const raw_sign = localStorage.getItem("teaspeak-forum-sign"); + const forum_auth = localStorage.getItem("teaspeak-forum-auth"); + if(!raw_data || !raw_sign || !forum_auth) { + console.log(tr("No TeaForo authentification found. TeaForo connection status: unconnected")); + return; + } + + try { + _data = new Data(forum_auth, raw_data, raw_sign); + } catch(error) { + console.error(tr("Failed to initialize TeaForo connection from local data. Error: %o"), error); + return; + } + if(_data.should_renew()) { + console.info(tr("TeaForo data should be renewed. Executing renew.")); + renew_data().then(status => { + if(status === "success") { + console.info(tr("TeaForo data has been successfully renewed.")); + } else { + console.warn(tr("Failed to renew TeaForo data. New login required.")); + localStorage.removeItem("teaspeak-forum-data"); + localStorage.removeItem("teaspeak-forum-sign"); + localStorage.removeItem("teaspeak-forum-auth"); + } + }).catch(error => { + console.warn(tr("Failed to renew TeaForo data. An error occurred: %o"), error); + }); + return; + } + + if(_data && _data.is_expired()) { + console.error(tr("TeaForo data is expired. TeaForo connection isn't available!")); + } + } + }) +} \ No newline at end of file diff --git a/shared/js/proto.ts b/shared/js/proto.ts index 82044349..348d767f 100644 --- a/shared/js/proto.ts +++ b/shared/js/proto.ts @@ -24,6 +24,9 @@ interface JQuery { alert() : JQuery; modal(properties: any) : this; bootstrapMaterialDesign() : this; + + /* first element which matches the selector, could be the element itself or a parent */ + firstParent(selector: string) : JQuery; } interface JQueryStatic { @@ -184,6 +187,12 @@ if(typeof ($) !== "undefined") { this.attr("style", original_style || ""); return result; } + if(!$.fn.firstParent) + $.fn.firstParent = function (this: JQuery, selector: string) { + if(this.is(selector)) + return this; + return this.parent(selector); + } } if (!String.prototype.format) { @@ -247,8 +256,39 @@ function calculate_width(text: string) : number { return size; } +interface Twemoji { + parse(message: string) : string; +} +declare let twemoji: Twemoji; + +interface HighlightJS { + listLanguages() : string[]; + getLanguage(name: string) : any | undefined; + + highlight(language: string, text: string, ignore_illegals?: boolean) : HighlightJSResult; + highlightAuto(text: string) : HighlightJSResult; +} + +interface HighlightJSResult { + language: string; + relevance: number; + + value: string; + second_best?: any; +} + +interface DOMPurify { + sanitize(html: string, config?: { + ADD_ATTR?: string[] + }) : string; +} +declare let DOMPurify: DOMPurify; + +declare let remarkable: typeof window.remarkable; + declare class webkitAudioContext extends AudioContext {} declare class webkitOfflineAudioContext extends OfflineAudioContext {} + interface Window { readonly webkitAudioContext: typeof webkitAudioContext; readonly AudioContext: typeof webkitAudioContext; @@ -258,6 +298,10 @@ interface Window { readonly Pointer_stringify: any; readonly jsrender: any; + twemoji: Twemoji; + hljs: HighlightJS; + remarkable: any; + require(id: string): any; } diff --git a/shared/js/settings.ts b/shared/js/settings.ts index 2ab78c36..694dd226 100644 --- a/shared/js/settings.ts +++ b/shared/js/settings.ts @@ -18,6 +18,8 @@ interface SettingsKey { fallback_imports?: {[key: string]:(value: string) => T}; description?: string; default_value?: T; + + require_restart?: boolean; } class SettingsBase { @@ -144,6 +146,13 @@ class Settings extends StaticSettings { key: 'disableContextMenu', description: 'Disable the context menu for the channel tree which allows to debug the DOM easier' }; + + static readonly KEY_DISABLE_GLOBAL_CONTEXT_MENU: SettingsKey = { + key: 'disableGlobalContextMenu', + description: 'Disable the general context menu prevention', + default_value: false + }; + static readonly KEY_DISABLE_UNLOAD_DIALOG: SettingsKey = { key: 'disableUnloadDialog', description: 'Disables the unload popup on side closing' @@ -154,6 +163,8 @@ class Settings extends StaticSettings { }; static readonly KEY_DISABLE_MULTI_SESSION: SettingsKey = { key: 'disableMultiSession', + default_value: false, + require_restart: true }; static readonly KEY_LOAD_DUMMY_ERROR: SettingsKey = { @@ -194,6 +205,9 @@ class Settings extends StaticSettings { static readonly KEY_FLAG_CONNECT_PASSWORD: SettingsKey = { key: 'connect_password_hashed' }; + static readonly KEY_CONNECT_HISTORY: SettingsKey = { + key: 'connect_history' + }; static readonly KEY_CERTIFICATE_CALLBACK: SettingsKey = { key: 'certificate_callback' @@ -201,11 +215,82 @@ class Settings extends StaticSettings { /* sounds */ static readonly KEY_SOUND_MASTER: SettingsKey = { - key: 'audio_master_volume' + key: 'audio_master_volume', + default_value: 100 }; static readonly KEY_SOUND_MASTER_SOUNDS: SettingsKey = { - key: 'audio_master_volume_sounds' + key: 'audio_master_volume_sounds', + default_value: 100 + }; + + static readonly KEY_CHAT_FIXED_TIMESTAMPS: SettingsKey = { + key: 'chat_fixed_timestamps', + default_value: false, + description: 'Enables fixed timestamps for chat messages and disabled the updating once (2 seconds ago... etc)' + }; + + static readonly KEY_CHAT_COLLOQUIAL_TIMESTAMPS: SettingsKey = { + key: 'chat_colloquial_timestamps', + default_value: true, + description: 'Enabled colloquial timestamp formatting like "Yesterday at ..." or "Today at ..."' + }; + + static readonly KEY_CHAT_COLORED_EMOJIES: SettingsKey = { + key: 'chat_colored_emojies', + default_value: true, + description: 'Enables colored emojies powered by Twemoji' + }; + + static readonly KEY_CHAT_TAG_URLS: SettingsKey = { + key: 'chat_tag_urls', + default_value: true, + description: 'Automatically link urls with [url]' + }; + + static readonly KEY_CHAT_ENABLE_MARKDOWN: SettingsKey = { + key: 'chat_enable_markdown', + default_value: true, + description: 'Enabled markdown chat support.' + }; + + static readonly KEY_CHAT_ENABLE_BBCODE: SettingsKey = { + key: 'chat_enable_bbcode', + default_value: true, + description: 'Enabled bbcode support in chat.' + }; + + static readonly KEY_SWITCH_INSTANT_CHAT: SettingsKey = { + key: 'switch_instant_chat', + default_value: true, + description: 'Directly switch to channel chat on channel select' + }; + + static readonly KEY_SWITCH_INSTANT_CLIENT: SettingsKey = { + key: 'switch_instant_client', + default_value: true, + description: 'Directly switch to client info on client select' + }; + + static readonly KEY_HOSTBANNER_BACKGROUND: SettingsKey = { + key: 'hostbanner_background', + default_value: false, + description: 'Enables a default background begind the hostbanner' + }; + + static readonly KEY_CHANNEL_EDIT_ADVANCED: SettingsKey = { + key: 'channel_edit_advanced', + default_value: false, + description: 'Edit channels in advanced mode with a lot more settings' + }; + + static readonly KEY_TEAFORO_URL: SettingsKey = { + key: "teaforo_url", + default_value: "https://forum.teaspeak.de/" + }; + + static readonly KEY_FONT_SIZE: SettingsKey = { + key: "font_size" }; static readonly FN_SERVER_CHANNEL_SUBSCRIBE_MODE: (channel: ChannelEntry) => SettingsKey = channel => { @@ -250,14 +335,17 @@ class Settings extends StaticSettings { } static_global?(key: string | SettingsKey, _default?: T) : T { + const actual_default = typeof(_default) === "undefined" && typeof(key) === "object" && 'default_value' in key ? key.default_value : _default; + const default_object = { seed: Math.random() } as any; let _static = this.static(key, default_object, typeof _default); - if(_static !== default_object) return StaticSettings.transformStO(_static, _default); - return this.global(key, _default); + if(_static !== default_object) return StaticSettings.transformStO(_static, actual_default); + return this.global(key, actual_default); } global?(key: string | SettingsKey, _default?: T) : T { - return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheGlobal[key]); + const actual_default = typeof(_default) === "undefined" && typeof(key) === "object" && 'default_value' in key ? key.default_value : _default; + return StaticSettings.resolveKey(Settings.keyify(key), actual_default, key => this.cacheGlobal[key]); } changeGlobal(key: string | SettingsKey, value?: T){ @@ -287,6 +375,7 @@ class ServerSettings extends SettingsBase { private currentServer: ServerEntry; private _server_save_worker: NodeJS.Timer; private _server_settings_updated: boolean = false; + private _destroyed = false; constructor() { super(); @@ -296,11 +385,23 @@ class ServerSettings extends SettingsBase { }, 5 * 1000); } + destroy() { + this._destroyed = true; + + this.currentServer = undefined; + this.cacheServer = undefined; + + clearInterval(this._server_save_worker); + this._server_save_worker = undefined; + } + server?(key: string | SettingsKey, _default?: T) : T { + if(this._destroyed) throw "destroyed"; return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]); } changeServer(key: string | SettingsKey, value?: T) { + if(this._destroyed) throw "destroyed"; key = Settings.keyify(key); if(this.cacheServer[key.key] == value) return; @@ -313,6 +414,7 @@ class ServerSettings extends SettingsBase { } setServer(server: ServerEntry) { + if(this._destroyed) throw "destroyed"; if(this.currentServer) { this.save(); this.cacheServer = {}; @@ -329,6 +431,7 @@ class ServerSettings extends SettingsBase { } save() { + if(this._destroyed) throw "destroyed"; this._server_settings_updated = false; if(this.currentServer) { diff --git a/shared/js/sound/Sounds.ts b/shared/js/sound/Sounds.ts index f6ccbf5f..bb55d1bf 100644 --- a/shared/js/sound/Sounds.ts +++ b/shared/js/sound/Sounds.ts @@ -5,6 +5,12 @@ enum Sound { AWAY_ACTIVATED = "away_activated", AWAY_DEACTIVATED = "away_deactivated", + MICROPHONE_MUTED = "microphone.muted", + MICROPHONE_ACTIVATED = "microphone.activated", + + SOUND_MUTED = "sound.muted", + SOUND_ACTIVATED = "sound.activated", + CONNECTION_CONNECTED = "connection.connected", CONNECTION_DISCONNECTED = "connection.disconnected", CONNECTION_BANNED = "connection.banned", @@ -155,23 +161,22 @@ namespace sound { const data: any = {}; data.version = 1; - for(const sound in Sound) { - if(typeof(speech_volume[sound]) !== "undefined") - data[sound] = speech_volume[sound]; + for(const key in Sound) { + if(typeof(speech_volume[Sound[key]]) !== "undefined") + data[Sound[key]] = speech_volume[Sound[key]]; } data.master = master_volume; data.overlap = overlap_sounds; data.ignore_muted = ignore_muted; settings.changeGlobal("sound_volume", JSON.stringify(data)); - console.error(data); } } export function initialize() : Promise { $.ajaxSetup({ beforeSend: function(jqXHR,settings){ - if (settings.dataType === 'binary'){ + if (settings.dataType === 'binary') { settings.xhr().responseType = 'arraybuffer'; settings.processData = false; } @@ -181,12 +186,11 @@ namespace sound { /* volumes */ { const data = JSON.parse(settings.static_global("sound_volume", "{}")); - for(const sound in Sound) { - if(typeof(data[sound]) !== "undefined") - speech_volume[sound] = data[sound]; + for(const sound_key in Sound) { + if(typeof(data[Sound[sound_key]]) !== "undefined") + speech_volume[Sound[sound_key]] = data[Sound[sound_key]]; } - console.error(data); master_volume = data.master || 1; overlap_sounds = data.overlap || true; ignore_muted = data.ignore_muted || true; @@ -223,6 +227,8 @@ namespace sound { ignore_overlap?: boolean; default_volume?: number; + + callback?: (flag: boolean) => any; } export async function resolve_sound(sound: Sound) : Promise { @@ -358,6 +364,8 @@ namespace sound { handle.replaying = true; player.onended = event => { + if(options.callback) + options.callback(true); delete this._playing_sounds[_sound]; }; @@ -375,11 +383,24 @@ namespace sound { } } else if(handle.node) { handle.node.currentTime = 0; - handle.node.play(); + handle.node.play().then(() => { + if(options.callback) + options.callback(true); + }).catch(error => { + console.warn(tr("Sound playback for sound %o resulted in an error: %o"), sound, error); + if(options.callback) + options.callback(false); + }); } else { - console.warn(tr("Failed to replay sound because of missing handles."), sound); + console.warn(tr("Failed to replay sound %o because of missing handles."), sound); + if(options.callback) + options.callback(false); return; } + }).catch(error => { + console.warn(tr("Failed to replay sound %o because it could not be resolved: %o"), sound, error); + if(options.callback) + options.callback(false); }); } } diff --git a/shared/js/ui/channel.ts b/shared/js/ui/channel.ts index 34295d8f..d495b220 100644 --- a/shared/js/ui/channel.ts +++ b/shared/js/ui/channel.ts @@ -50,6 +50,8 @@ class ChannelProperties { //Only after request channel_description: string = ""; + + channel_flag_conversation_private: boolean = false; } class ChannelEntry { @@ -70,6 +72,7 @@ class ChannelEntry { private _tag_siblings: JQuery; /* container for all sub channels */ private _tag_clients: JQuery; /* container for all clients */ private _tag_channel: JQuery; /* container for the channel info itself */ + private _destroyed = false; private _cachedPassword: string; private _cached_channel_description: string = undefined; @@ -91,6 +94,26 @@ class ChannelEntry { this.__updateChannelName(); } + destroy() { + this._destroyed = true; + if(this._tag_root) { + this._tag_root.remove(); /* removes also all other tags */ + this._tag_root = undefined; + } + this._tag_siblings = undefined; + this._tag_channel = undefined; + this._tag_clients = undefined; + + this._cached_channel_description_promise = undefined; + this._cached_channel_description_promise_resolve = undefined; + this._cached_channel_description_promise_reject = undefined; + + this.channel_previous = undefined; + this.parent = undefined; + this.channel_next = undefined; + this.channelTree = undefined; + } + channelName(){ return this.properties.channel_name; } @@ -186,7 +209,7 @@ class ChannelEntry { if(current_index == new_index && !enforce) return; this._tag_channel.css("z-index", this._family_index); - this._tag_channel.css("padding-left", (this._family_index + 1) * 16 + "px"); + this._tag_channel.css("padding-left", ((this._family_index + 1) * 16 + 10) + "px"); } calculate_family_index(enforce_recalculate: boolean = false) : number { @@ -213,6 +236,15 @@ class ChannelEntry { container_entry.attr("channel-id", this.channelId); container_entry.addClass(this._channel_name_alignment); + /* unread marker */ + { + container_entry.append( + $.spawn("div") + .addClass("marker-text-unread hidden") + .attr("conversation", this.channelId) + ); + } + /* channel icon (type) */ { container_entry.append( @@ -317,7 +349,7 @@ class ChannelEntry { /* setInterval(() => { let color = (Math.random() * 10000000).toString(16).substr(0, 6); - bg.css("background", "#" + color); + tag_channel.css("background", "#" + color); }, 150); */ @@ -455,23 +487,31 @@ class ChannelEntry { const bold = text => contextmenu.get_provider().html_format_enabled() ? "" + text + "" : text; contextmenu.spawn_context_menu(x, y, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Show channel info"), - callback: () => { - trigger_close = false; - this.channelTree.client.select_info.open_popover() - }, - icon_class: "client-about", - visible: this.channelTree.client.select_info.is_popover() - }, { - type: contextmenu.MenuEntryType.HR, - visible: this.channelTree.client.select_info.is_popover(), - name: '' - }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_switch", name: bold(tr("Switch to channel")), callback: () => this.joinChannel() + }, { + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-channel_switch", + name: bold(tr("Join text channel")), + callback: () => { + this.channelTree.client.side_bar.channel_conversations().set_current_channel(this.getChannelId()); + this.channelTree.client.side_bar.show_channel_conversations(); + }, + visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) + }, { + type: contextmenu.MenuEntryType.HR, + name: '' + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Show channel info"), + callback: () => { + trigger_close = false; + + alert('TODO!'); + }, + icon_class: "client-about" }, ...(() => { const local_client = this.channelTree.client.getClient(); @@ -734,13 +774,19 @@ class ChannelEntry { this.updateChannelTypeIcon(); info_update = true; } + if(key == "channel_flag_conversation_private") { + const conversations = this.channelTree.client.side_bar.channel_conversations(); + const conversation = conversations.conversation(this.channelId, false); + if(conversation) + conversation.set_flag_private(this.properties.channel_flag_conversation_private); + } } group.end(); if(info_update) { const _client = this.channelTree.client.getClient(); if(_client.currentChannel() === this) - this.channelTree.client.chat_frame.info_frame().update_channel_talk(); + this.channelTree.client.side_bar.info_frame().update_channel_talk(); //TODO chat channel! } } @@ -855,6 +901,7 @@ class ChannelEntry { get flag_subscribed() : boolean { return this._flag_subscribed; } + set flag_subscribed(flag: boolean) { if(this._flag_subscribed == flag) return; @@ -875,6 +922,10 @@ class ChannelEntry { this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this), mode); } + set flag_text_unread(flag: boolean) { + this._tag_channel.find(".marker-text-unread").toggleClass("hidden", !flag); + } + log_data() : log.server.base.Channel { return { channel_name: this.channelName(), diff --git a/shared/js/ui/client.ts b/shared/js/ui/client.ts index 19099f9d..491bab8f 100644 --- a/shared/js/ui/client.ts +++ b/shared/js/ui/client.ts @@ -1,10 +1,7 @@ /// /// -/// /// -import KeyEvent = ppt.KeyEvent; - enum ClientType { CLIENT_VOICE, CLIENT_QUERY, @@ -35,6 +32,7 @@ class ClientProperties { client_away_message: string = ""; client_away: boolean = false; + client_country: string = ""; client_input_hardware: boolean = false; client_output_hardware: boolean = false; @@ -42,8 +40,9 @@ class ClientProperties { client_output_muted: boolean = false; client_is_channel_commander: boolean = false; - client_teaforum_id: number = 0; - client_teaforum_name: string = ""; + client_teaforo_id: number = 0; + client_teaforo_name: string = ""; + client_teaforo_flags: number = 0; /* 0x01 := Banned | 0x02 := Stuff | 0x04 := Premium */ client_talk_power: number = 0; } @@ -55,9 +54,12 @@ class ClientEntry { protected _properties: ClientProperties; protected lastVariableUpdate: number = 0; - protected _speaking: boolean = false; + protected _speaking: boolean; protected _listener_initialized: boolean; + protected _audio_handle: connection.voice.VoiceClient; + protected _audio_volume: number; + protected _audio_muted: boolean; channelTree: ChannelTree; @@ -69,10 +71,42 @@ class ClientEntry { this._channel = null; } + destroy() { + if(this._tag) { + this._tag.remove(); + this._tag = undefined; + } + if(this._audio_handle) { + console.warn(tr("Destroying client with an active audio handle. This could cause memory leaks!")); + this._audio_handle.abort_replay(); + this._audio_handle.callback_playback = undefined; + this._audio_handle.callback_stopped = undefined; + this._audio_handle = undefined; + } + + this._channel = undefined; + } + + tree_unregistered() { + this.channelTree = undefined; + if(this._audio_handle) { + this._audio_handle.abort_replay(); + this._audio_handle.callback_playback = undefined; + this._audio_handle.callback_stopped = undefined; + this._audio_handle = undefined; + } + + this._channel = undefined; + } + set_audio_handle(handle: connection.voice.VoiceClient) { if(this._audio_handle === handle) return; + if(this._audio_handle) { + this._audio_handle.callback_playback = undefined; + this._audio_handle.callback_stopped = undefined; + } //TODO may ensure that the id is the same? this._audio_handle = handle; if(!handle) { @@ -97,6 +131,41 @@ class ClientEntry { clientUid(){ return this.properties.client_unique_identifier; } clientId(){ return this._clientId; } + is_muted() { return !!this._audio_muted; } + set_muted(flag: boolean, update_icon: boolean, force?: boolean) { + if(this._audio_muted === flag && !force) + return; + + if(flag) { + this.channelTree.client.serverConnection.send_command('clientmute', { + clid: this.clientId() + }); + } else if(this._audio_muted) { + this.channelTree.client.serverConnection.send_command('clientunmute', { + clid: this.clientId() + }); + } + this._audio_muted = flag; + + this.channelTree.client.settings.changeServer("mute_client_" + this.clientUid(), flag); + if(this._audio_handle) { + if(flag) { + this._audio_handle.set_volume(0); + } else { + this._audio_handle.set_volume(this._audio_volume); + } + } + + if(update_icon) + this.updateClientSpeakIcon(); + + for(const client of this.channelTree.clients) { + if(client === this || client.properties.client_unique_identifier != this.properties.client_unique_identifier) + continue; + client.set_muted(flag, true); + } + } + protected initializeListener(){ if(this._listener_initialized) return; this._listener_initialized = true; @@ -116,7 +185,7 @@ class ClientEntry { if($.isArray(this.channelTree.currently_selected)) { //Multiselect return; } - this.chat(true).focus(); + this.open_text_chat(); }); if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { @@ -169,6 +238,25 @@ class ClientEntry { }); } + protected contextmenu_info() : contextmenu.MenuEntry[] { + return [ + { + type: contextmenu.MenuEntryType.ENTRY, + name: this.properties.client_type_exact === ClientType.CLIENT_MUSIC ? tr("Show bot info") : tr("Show client info"), + callback: () => { + this.channelTree.client.side_bar.show_client_info(this); + }, + icon_class: "client-about", + visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT) + }, { + callback: () => {}, + type: contextmenu.MenuEntryType.HR, + name: "", + visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT) + } + ] + } + protected assignment_context() : contextmenu.MenuEntry[] { let server_groups: contextmenu.MenuEntry[] = []; for(let group of this.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { @@ -229,7 +317,7 @@ class ClientEntry { sub_menu: [ { type: contextmenu.MenuEntryType.ENTRY, - icon: "client-permission_server_groups", + icon_class: "client-permission_server_groups", name: "Server groups dialog", callback: () => { Modals.createServerGroupAssignmentModal(this, (group, flag) => { @@ -260,36 +348,50 @@ class ClientEntry { type: contextmenu.MenuEntryType.SUB_MENU, icon_class: "client-permission_client", name: tr("Permissions"), - disabled: true, - sub_menu: [ ] + sub_menu: [ + { + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-permission_client", + name: tr("Client permissions"), + callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clp", {unique_id: this.clientUid()}).open() + }, + { + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-permission_client", + name: tr("Client channel permissions"), + callback: () => Modals.spawnPermissionEdit(this.channelTree.client, "clchp", {unique_id: this.clientUid(), channel_id: this._channel ? this._channel.channelId : undefined }).open() + } + ] }]; } + open_text_chat() { + const chat = this.channelTree.client.side_bar; + const conversation = chat.private_conversations().find_conversation({ + name: this.clientNickName(), + client_id: this.clientId(), + unique_id: this.clientUid() + }, { + attach: true, + create: true + }); + chat.private_conversations().set_selected_conversation(conversation); + /* TODO: Check if auto switch to private conversations is enabled */ + chat.show_private_conversations(); + chat.private_conversations().try_input_focus(); + } + showContextMenu(x: number, y: number, on_close: () => void = undefined) { let trigger_close = true; contextmenu.spawn_context_menu(x, y, - { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Show client info"), - callback: () => { - trigger_close = false; - this.channelTree.client.select_info.open_popover() - }, - icon_class: "client-about", - visible: this.channelTree.client.select_info.is_popover() - }, { - type: contextmenu.MenuEntryType.HR, - visible: this.channelTree.client.select_info.is_popover(), - name: '' - }, { + ...this.contextmenu_info(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-change_nickname", name: (contextmenu.get_provider().html_format_enabled() ? "" : "") + tr("Open text chat") + (contextmenu.get_provider().html_format_enabled() ? "" : ""), callback: () => { - this.channelTree.client.chat.activeChat = this.chat(true); - this.channelTree.client.chat.focus(); + this.open_text_chat(); } }, { type: contextmenu.MenuEntryType.ENTRY, @@ -417,15 +519,29 @@ class ClientEntry { icon_class: "client-volume", name: tr("Change Volume"), callback: () => { - Modals.spawnChangeVolume(this._audio_handle.get_volume(), volume => { + Modals.spawnChangeVolume(this._audio_volume, volume => { + this._audio_volume = volume; this.channelTree.client.settings.changeServer("volume_client_" + this.clientUid(), volume); - this._audio_handle.set_volume(volume); + if(this._audio_handle) + this._audio_handle.set_volume(volume); if(this.channelTree.client.select_info.currentSelected == this) this.channelTree.client.select_info.update(); }); } + }, { + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-input_muted_local", + name: tr("Mute client"), + visible: !this._audio_muted, + callback: () => this.set_muted(true, true) + }, { + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-input_muted_local", + name: tr("Unmute client"), + visible: this._audio_muted, + callback: () => this.set_muted(false, true) }, - contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})()) + contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))()) ); } @@ -510,7 +626,7 @@ class ClientEntry { } set speaking(flag) { - if(flag == this._speaking) return; + if(flag === this._speaking) return; this._speaking = flag; this.updateClientSpeakIcon(); } @@ -531,8 +647,10 @@ class ClientEntry { icon = "client-server_query"; console.log("Server query!"); } else { - if(this.properties.client_away) { + if (this.properties.client_away) { icon = "client-away"; + } else if (this._audio_muted && !(this instanceof LocalClientEntry)) { + icon = "client-input_muted_local"; } else if(!this.properties.client_output_hardware) { icon = "client-hardware_output_muted"; } else if(this.properties.client_output_muted) { @@ -582,6 +700,7 @@ class ClientEntry { let update_icon_speech = false; let update_away = false; let reorder_channel = false; + let update_avatar = false; { const entries = []; @@ -595,13 +714,34 @@ class ClientEntry { } for(const variable of variables) { + const old_value = this._properties[variable.key]; JSON.map_field_to(this._properties, variable.value, variable.key); if(variable.key == "client_nickname") { - this.tag.find(".client-name").text(variable.value); - let chat = this.chat(false); - if(chat) chat.name = variable.value; + if(variable.value !== old_value && typeof(old_value) === "string") { + if(!(this instanceof LocalClientEntry)) { /* own changes will be logged somewhere else */ + this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, { + own_client: false, + client: this.log_data(), + new_name: variable.value, + old_name: old_value + }); + } + } + this.tag.find(".client-name").text(variable.value); + + const chat = this.channelTree.client.side_bar; + const conversation = chat.private_conversations().find_conversation({ + name: this.clientNickName(), + client_id: this.clientId(), + unique_id: this.clientUid() + }, { + attach: false, + create: false + }); + if(conversation) + conversation.set_client_name(variable.value); reorder_channel = true; } if( @@ -617,13 +757,15 @@ class ClientEntry { update_away = true; } if(variable.key == "client_unique_identifier") { - if(this._audio_handle) { - const volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1")); - this._audio_handle.set_volume(volume); - log.debug(LogCategory.CLIENT, tr("Loaded client volume %d for client %s from config."), volume, this.clientUid()); - } else { - log.warn(LogCategory.CLIENT, tr("Visible client got unique id assigned, but hasn't yet an audio handle. Ignoring volume assignment.")); - } + this._audio_volume = parseFloat(this.channelTree.client.settings.server("volume_client_" + this.clientUid(), "1")); + const mute_status = this.channelTree.client.settings.server("mute_client_" + this.clientUid(), false); + this.set_muted(mute_status, false, mute_status); /* force only needed when we want to mute the client */ + + if(this._audio_handle) + this._audio_handle.set_volume(this._audio_muted ? 0 : this._audio_volume); + + update_icon_speech = true; + log.debug(LogCategory.CLIENT, tr("Loaded client (%s) server specific properties. Volume: %o Muted: %o."), this.clientUid(), this._audio_volume, this._audio_muted); } if(variable.key == "client_talk_power") { reorder_channel = true; @@ -639,6 +781,8 @@ class ClientEntry { } if(variable.key =="client_channel_group_id" || variable.key == "client_servergroups") this.update_displayed_client_groups(); + else if(variable.key == "client_flag_avatar") + update_avatar = true; } /* process updates after variables have been set */ @@ -651,15 +795,30 @@ class ClientEntry { if(update_away) this.updateAwayMessage(); + const side_bar = this.channelTree.client.side_bar; + { + const client_info = side_bar.client_info(); + if(client_info.current_client() === this) + client_info.set_current_client(this, true); /* force an update */ + } + if(update_avatar) { + this.channelTree.client.fileManager.avatars.update_cache(this.avatarId(), this.properties.client_flag_avatar); + + const conversations = side_bar.private_conversations(); + const conversation = conversations.find_conversation({name: this.clientNickName(), unique_id: this.clientUid(), client_id: this.clientId()}, {create: false, attach: false}); + if(conversation) + conversation.update_avatar(); + } + group.end(); } update_displayed_client_groups() { - this.tag.find(".container-icons-group").children().detach(); + this.tag.find(".container-icons-group").children().remove(); for(let id of this.assignedServerGroupIds()) this.updateGroupIcon(this.channelTree.client.groups.serverGroup(id)); - + this.update_group_icon_order(); this.updateGroupIcon(this.channelTree.client.groups.channelGroup(this.properties.client_channel_group_id)); let prefix_groups: string[] = []; @@ -696,44 +855,8 @@ class ClientEntry { } } - private chat_name() { - return "client_" + this.clientUid() + ":" + this.clientId(); - } - - chat(create: boolean = false) : ChatEntry { - let chatName = "client_" + this.clientUid() + ":" + this.clientId(); - let chat = this.channelTree.client.chat.findChat(chatName); - if(!chat && create) { - chat = this.channelTree.client.chat.createChat(chatName); - chat.flag_closeable = true; - chat.name = this.clientNickName(); - chat.owner_unique_id = this.properties.client_unique_identifier; - } - - this.initialize_chat(chat); - return chat; - } - - initialize_chat(handle?: ChatEntry) { - handle = handle || this.channelTree.client.chat.findChat(this.chat_name()); - if(!handle) - return; - - handle.onMessageSend = text => { - this.channelTree.client.serverConnection.command_helper.sendMessage(text, ChatType.CLIENT, this); - }; - - handle.onClose = () => { - if(!handle.flag_offline) - this.channelTree.client.serverConnection.send_command("clientchatclosed", {"clid": this.clientId()}, {process_result: false}).catch(error => { - log.warn(LogCategory.GENERAL, tr("Failed to notify chat participant (%o) that the chat has been closed. Error: %o"), this, error); - }); - return true; - }; - } - updateClientIcon() { - this.tag.find(".container-icon-client").children().detach(); + this.tag.find(".container-icon-client").children().remove(); if(this.properties.client_icon_id > 0) { this.channelTree.client.fileManager.icons.generateTag(this.properties.client_icon_id).attr("title", "Client icon") .appendTo(this.tag.find(".container-icon-client")); @@ -742,18 +865,25 @@ class ClientEntry { updateGroupIcon(group: Group) { if(!group) return; - //TODO group icon order - this.tag.find(".container-icons-group .icon_group_" + group.id).detach(); + + const container = this.tag.find(".container-icons-group"); + container.find(".icon_group_" + group.id).remove(); if (group.properties.iconid > 0) { - this.tag.find(".container-icons-group").append( - $.spawn("div") + container.append( + $.spawn("div").attr('group-power', group.properties.sortid) .addClass("container-group-icon icon_group_" + group.id) .append(this.channelTree.client.fileManager.icons.generateTag(group.properties.iconid)).attr("title", group.name) ); } } + update_group_icon_order() { + const container = this.tag.find(".container-icons-group"); + + container.append(...[...container.children()].sort((a, b) => parseInt(a.getAttribute("group-power")) - parseInt(b.getAttribute("group-power")))); + } + assignedServerGroupIds() : number[] { let result = []; for(let id of this.properties.client_servergroups.split(",")){ @@ -843,7 +973,7 @@ class LocalClientEntry extends ClientEntry { const _self = this; contextmenu.spawn_context_menu(x, y, - { + ...this.contextmenu_info(), { name: (contextmenu.get_provider().html_format_enabled() ? "" : "") + tr("Change name") + @@ -875,6 +1005,7 @@ class LocalClientEntry extends ClientEntry { } initializeListener(): void { + this._listener_initialized = false; /* could there be a better system */ super.initializeListener(); this.tag.find(".client-name").addClass("client-name-own"); @@ -918,11 +1049,14 @@ class LocalClientEntry extends ClientEntry { if(_self.clientNickName() == text) return; elm.text(_self.clientNickName()); + const old_name = _self.clientNickName(); _self.handle.serverConnection.command_helper.updateClient("client_nickname", text).then((e) => { settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, text); this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGED, { client: this.log_data(), - own_action: true + old_name: old_name, + new_name: text, + own_client: true }); }).catch((e: CommandResult) => { this.channelTree.client.log.log(log.server.Type.CLIENT_NICKNAME_CHANGE_FAILED, { @@ -973,6 +1107,13 @@ class MusicClientEntry extends ClientEntry { super(clientId, clientName, new MusicClientProperties()); } + destroy() { + super.destroy(); + this._info_promise = undefined; + this._info_promise_reject = undefined; + this._info_promise_resolve = undefined; + } + get properties() : MusicClientProperties { return this._properties as MusicClientProperties; } @@ -980,20 +1121,7 @@ class MusicClientEntry extends ClientEntry { showContextMenu(x: number, y: number, on_close: () => void = undefined): void { let trigger_close = true; contextmenu.spawn_context_menu(x, y, - { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Show bot info"), - callback: () => { - trigger_close = false; - this.channelTree.client.select_info.open_popover() - }, - icon_class: "client-about", - visible: this.channelTree.client.select_info.is_popover() - }, { - type: contextmenu.MenuEntryType.HR, - visible: this.channelTree.client.select_info.is_popover(), - name: '' - }, { + ...this.contextmenu_info(), { name: tr("Change bot name"), icon_class: "client-change_nickname", disabled: false, @@ -1160,7 +1288,7 @@ class MusicClientEntry extends ClientEntry { }, type: contextmenu.MenuEntryType.ENTRY }, - contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : () => {})()) + contextmenu.Entry.CLOSE(() => (trigger_close ? on_close : (() => {}))()) ); } diff --git a/shared/js/ui/elements/context_menu.ts b/shared/js/ui/elements/context_menu.ts index 24d82add..1f4562e5 100644 --- a/shared/js/ui/elements/context_menu.ts +++ b/shared/js/ui/elements/context_menu.ts @@ -88,8 +88,13 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { return; menu.animate({opacity: 0}, 100, () => menu.css("display", "none")); - for(const callback of this._close_callbacks) + for(const callback of this._close_callbacks) { + if(typeof(callback) !== "function") { + console.error(tr("Given close callback is not a function!. Callback: %o"), callback); + continue; + } callback(); + } this._close_callbacks = []; } @@ -135,7 +140,7 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { } return tag; } else if(entry.type == contextmenu.MenuEntryType.CHECKBOX) { - let checkbox = $.spawn("label").addClass("checkbox"); + let checkbox = $.spawn("label").addClass("ccheckbox"); $.spawn("input").attr("type", "checkbox").prop("checked", !!entry.checkbox_checked).appendTo(checkbox); $.spawn("span").addClass("checkmark").appendTo(checkbox); @@ -191,7 +196,8 @@ class HTMLContextMenuProvider implements contextmenu.ContextMenuProvider { continue; if(entry.type == contextmenu.MenuEntryType.CLOSE) { - this._close_callbacks.push(entry.callback); + if(entry.callback) + this._close_callbacks.push(entry.callback); } else menu_container.append(this.generate_tag(entry)); } diff --git a/shared/js/ui/elements/modal.ts b/shared/js/ui/elements/modal.ts index 2027fe99..a8fd63e3 100644 --- a/shared/js/ui/elements/modal.ts +++ b/shared/js/ui/elements/modal.ts @@ -1,3 +1,5 @@ +import ClickEvent = JQuery.ClickEvent; + enum ElementType { HEADER, BODY, @@ -22,7 +24,7 @@ const ModalFunctions = { switch (typeof val){ case "string": if(type == ElementType.HEADER) - return $.spawn("h5").addClass("modal-title").text(val); + return $.spawn("div").addClass("modal-title").text(val); return $("
" + val + "
"); case "object": return val as JQuery; case "undefined": @@ -61,6 +63,7 @@ class ModalProperties { return this; } width: number | string = "60%"; + min_width?: number | string; height: number | string = "auto"; closeable: boolean = true; @@ -78,8 +81,33 @@ class ModalProperties { full_size?: boolean = false; } -class Modal { +$(document).on('mousedown', (event: MouseEvent) => { + /* pageX or pageY are undefined if this is an event executed via .trigger('click'); */ + if(_global_modal_count == 0 || typeof(event.pageX) === "undefined" || typeof(event.pageY) === "undefined") + return; + + let element = event.target as HTMLElement; + do { + if(element.classList.contains('modal-content')) + break; + + if(!element.classList.contains('modal')) + continue; + + if(element == _global_modal_last && _global_modal_last_time + 100 > Date.now()) + break; + + $(element).find("> .modal-dialog > .modal-content > .modal-header .button-modal-close").trigger('click'); + break; + } while((element = element.parentElement)); +}); + +let _global_modal_count = 0; +let _global_modal_last: HTMLElement; +let _global_modal_last_time: number; + +class Modal { private _htmlTag: JQuery; properties: ModalProperties; shown: boolean; @@ -119,32 +147,57 @@ class Modal { Object.assign(properties, this.properties.template_properties); const tag = template.renderTag(properties); + if(typeof(this.properties.width) !== "undefined") + tag.find(".modal-content").css("min-width", this.properties.width); + if(typeof(this.properties.min_width) !== "undefined") + tag.find(".modal-content").css("min-width", this.properties.min_width); + this.close_elements = tag.find(".button-modal-close"); - this.close_elements.toggle(this.properties.closeable); + this.close_elements.toggle(this.properties.closeable).on('click', event => { + if(this.properties.closeable) + this.close(); + }); this._htmlTag = tag; - this._htmlTag.on('shown.bs.modal', event => { for(const listener of this.open_listener) listener(); }); + + this._htmlTag.find("input").on('change', event => { + $(event.target).parents(".form-group").toggleClass('is-filled', !!(event.target as HTMLInputElement).value); + }); + + //TODO: After the animation! this._htmlTag.on('hide.bs.modal', event => !this.properties.closeable || this.close()); - this._htmlTag.on('hidden.bs.modal', event => this._htmlTag.detach()); + this._htmlTag.on('hidden.bs.modal', event => this._htmlTag.remove()); } open() { + if(this.shown) + return; + + _global_modal_last_time = Date.now(); + _global_modal_last = this.htmlTag[0]; + this.shown = true; this.htmlTag.appendTo($("body")); - this.htmlTag.bootstrapMaterialDesign().modal(this.properties.closeable ? 'show' : { - backdrop: 'static', - keyboard: false, - }); + _global_modal_count++; + this.htmlTag.show(); + setTimeout(() => this.htmlTag.addClass('shown'), 0); - if(this.properties.trigger_tab) - this.htmlTag.one('shown.bs.modal', () => this.htmlTag.find(".tab").trigger('tab.resize')); + setTimeout(() => { + for(const listener of this.open_listener) listener(); + this.htmlTag.find(".tab").trigger('tab.resize'); + }, 300); } close() { if(!this.shown) return; + _global_modal_count--; this.shown = false; - this.htmlTag.modal('hide'); + this.htmlTag.removeClass('shown'); + setTimeout(() => { + this.htmlTag.remove(); + this._htmlTag = undefined; + }, 300); this.properties.triggerClose(); for(const listener of this.close_listener) listener(); diff --git a/shared/js/ui/elements/slider.ts b/shared/js/ui/elements/slider.ts new file mode 100644 index 00000000..0e956585 --- /dev/null +++ b/shared/js/ui/elements/slider.ts @@ -0,0 +1,105 @@ +interface SliderOptions { + min_value?: number; + max_value?: number; + initial_value?: number; + step?: number; + + unit?: string; + value_field?: JQuery | JQuery[]; +} + +interface Slider { + value(value?: number) : number; +} + +function sliderfy(slider: JQuery, options?: SliderOptions) : Slider { + options = Object.assign( { + initial_value: 0, + min_value: 0, + max_value: 100, + step: 1, + unit: '%', + value_field: [] + }, options); + + if(!Array.isArray(options.value_field)) + options.value_field = [options.value_field]; + if(options.min_value >= options.max_value) + throw "invalid range"; + if(options.step > (options.max_value - options.min_value)) + throw "invalid step size"; + + + const tool = tooltip(slider); /* add the tooltip functionality */ + const filler = slider.find(".filler"); + const thumb = slider.find(".thumb"); + const tooltip_text = slider.find(".tooltip a"); + + let _current_value; + const update_value = (value: number, trigger_change: boolean) => { + _current_value = value; + + const offset = Math.min(100, Math.max(0, ((value - options.min_value) * 100) / (options.max_value - options.min_value))); + filler.css('width', offset + '%'); + thumb.css('left', offset + '%'); + + + tooltip_text.text(value.toFixed(0) + options.unit); + slider.attr("value", value); + if(trigger_change) + slider.trigger('change'); + for(const field of options.value_field) + (field as JQuery).text(value + options.unit); + + tool.update(); + }; + + const mouse_up_listener = () => { + document.removeEventListener('mousemove', mouse_listener); + document.removeEventListener('touchmove', mouse_listener); + + document.removeEventListener('mouseup', mouse_up_listener); + document.removeEventListener('touchend', mouse_up_listener); + document.removeEventListener('touchcancel', mouse_up_listener); + + tool.hide(); + slider.removeClass("active"); + console.log("Events removed"); + }; + + const mouse_listener = (event: MouseEvent | TouchEvent) => { + const parent_offset = slider.offset(); + const min = parent_offset.left; + const max = parent_offset.left + slider.width(); + const current = event instanceof MouseEvent ? event.pageX : event.touches[event.touches.length - 1].clientX; + + const range = options.max_value - options.min_value; + const offset = Math.round(((current - min) * (range / options.step)) / (max - min)) * options.step; + let value = Math.min(options.max_value, Math.max(options.min_value, options.min_value + offset)); + //console.log("Min: %o | Max: %o | %o (%o)", min, max, current, offset); + + update_value(value, true); + }; + + slider.on('mousedown', event => { + document.addEventListener('mousemove', mouse_listener); + document.addEventListener('touchmove', mouse_listener); + + document.addEventListener('mouseup', mouse_up_listener); + document.addEventListener('touchend', mouse_up_listener); + document.addEventListener('touchcancel', mouse_up_listener); + + tool.show(); + slider.addClass("active"); + }); + + update_value(options.initial_value, false); + + return { + value(value?: number) { + if(typeof(value) !== "undefined" && value !== _current_value) + update_value(value, true); + return _current_value; + } + } +} \ No newline at end of file diff --git a/shared/js/ui/elements/tab.ts b/shared/js/ui/elements/tab.ts index b1c0c939..fb34577c 100644 --- a/shared/js/ui/elements/tab.ts +++ b/shared/js/ui/elements/tab.ts @@ -75,6 +75,9 @@ var TabFunctions = { if(header_tag.attr("x-entry-class")) tag_header.addClass(header_tag.attr("x-entry-class")); + if(header_tag.attr("x-entry-id")) + tag_header.attr("x-id", header_tag.attr("x-entry-id")); + tag_header.append(header_data); /* listener if the tab might got removed */ diff --git a/shared/js/ui/elements/tooltip.ts b/shared/js/ui/elements/tooltip.ts new file mode 100644 index 00000000..3ae14084 --- /dev/null +++ b/shared/js/ui/elements/tooltip.ts @@ -0,0 +1,78 @@ +function tooltip(entry: JQuery) { + return tooltip.initialize(entry); +} + +namespace tooltip { + let _global_tooltip: JQuery; + export type Handle = { + show(); + is_shown(); + hide(); + update(); + } + export function initialize(entry: JQuery) : Handle { + let _show; + let _hide; + let _shown; + let _update; + + entry.find(".container-tooltip").each((index, _node) => { + const node = $(_node) as JQuery; + const node_content = node.find(".tooltip"); + + let _force_show = false, _flag_shown = false; + + const mouseenter = (event?) => { + const bounds = node[0].getBoundingClientRect(); + + if(!_global_tooltip) { + _global_tooltip = $("#global-tooltip"); + } + + _global_tooltip[0].style.left = (bounds.left + bounds.width / 2) + "px"; + _global_tooltip[0].style.top = bounds.top + "px"; + _global_tooltip[0].classList.add("shown"); + + _global_tooltip[0].innerHTML = node_content[0].innerHTML; + _flag_shown = _flag_shown || !!event; /* if event is undefined then it has been triggered by hand */ + }; + + const mouseexit = () => { + if(_global_tooltip) { + if(!_force_show) { + _global_tooltip[0].classList.remove("shown"); + } + _flag_shown = false; + } + }; + + _node.addEventListener("mouseenter", mouseenter); + + _node.addEventListener("mouseleave", mouseexit); + + _show = () => { + _force_show = true; + mouseenter(); + }; + + _hide = () => { + _force_show = false; + if(!_flag_shown) + mouseexit(); + }; + + _update = () => { + if(_flag_shown || _force_show) + mouseenter(); + }; + + _shown = () => _flag_shown || _force_show; + }); + return { + hide: _hide || (() => {}), + show: _show || (() => {}), + is_shown: _shown || (() => false), + update: _update || (() => {}) + }; + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/ControlBar.ts b/shared/js/ui/frames/ControlBar.ts index 86205f43..25ee9b78 100644 --- a/shared/js/ui/frames/ControlBar.ts +++ b/shared/js/ui/frames/ControlBar.ts @@ -27,6 +27,8 @@ class ControlBar { private connection_handler: ConnectionHandler | undefined; + private _button_hostbanner: JQuery; + htmlTag: JQuery; constructor(htmlTag: JQuery) { this.htmlTag = htmlTag; @@ -47,6 +49,7 @@ class ControlBar { this.connection_handler = handler; this.apply_server_state(); + this.update_connection_state(); } apply_server_state() { @@ -63,15 +66,30 @@ class ControlBar { this.button_query_visible = this.connection_handler.client_status.queries_visible; this.button_subscribe_all = this.connection_handler.client_status.channel_subscribe_all; + this.apply_server_hostbutton(); this.apply_server_voice_state(); } + apply_server_hostbutton() { + const server = this.connection_handler.channelTree.server; + if(server && server.properties.virtualserver_hostbutton_gfx_url) { + this._button_hostbanner + .attr("title", server.properties.virtualserver_hostbutton_tooltip || server.properties.virtualserver_hostbutton_gfx_url) + .attr("href", server.properties.virtualserver_hostbutton_url); + this._button_hostbanner.find("img").attr("src", server.properties.virtualserver_hostbutton_gfx_url); + this._button_hostbanner.show(); + } else { + this._button_hostbanner.hide(); + } + } + apply_server_voice_state() { if(!this.connection_handler) return; this.button_microphone = !this.connection_handler.client_status.input_hardware ? "disabled" : this.connection_handler.client_status.input_muted ? "muted" : "enabled"; this.button_speaker = this.connection_handler.client_status.output_muted ? "muted" : "enabled"; + top_menu.update_state(); //TODO: Only run "small" update? } current_connection_handler() { @@ -95,6 +113,7 @@ class ControlBar { }; this.htmlTag.find(".btn_connect").on('click', this.on_open_connect.bind(this)); + this.htmlTag.find(".btn_connect_new_tab").on('click', this.on_open_connect_new_tab.bind(this)); this.htmlTag.find(".btn_disconnect").on('click', this.on_execute_disconnect.bind(this)); this.htmlTag.find(".btn_mute_input").on('click', this.on_toggle_microphone.bind(this)); @@ -110,6 +129,15 @@ class ControlBar { this.htmlTag.find(".btn_token_use").on('click', this.on_token_use.bind(this)); this.htmlTag.find(".btn_token_list").on('click', this.on_token_list.bind(this)); + (this._button_hostbanner = this.htmlTag.find(".button-hostbutton")).hide().on('click', () => { + if(!this.connection_handler) return; + + const server = this.connection_handler.channelTree.server; + if(!server || !server.properties.virtualserver_hostbutton_url) return; + + window.open(server.properties.virtualserver_hostbutton_url, '_blank'); + }); + { this.htmlTag.find(".btn_away_disable").on('click', this.on_away_disable.bind(this)); @@ -124,6 +152,7 @@ class ControlBar { this.htmlTag.find(".btn_away_toggle").on('click', this.on_away_toggle.bind(this)); } + dropdownify(this.htmlTag.find(".container-connect")); dropdownify(this.htmlTag.find(".container-disconnect")); dropdownify(this.htmlTag.find(".btn_token")); dropdownify(this.htmlTag.find(".btn_away")); @@ -202,13 +231,20 @@ class ControlBar { this._button_microphone = state; let tag = this.htmlTag.find(".btn_mute_input"); - const tag_icon = tag.find(".icon_x32, .icon"); + const tag_icon = tag.find(".icon_em, .icon"); tag.toggleClass('activated', state === "muted"); + /* tag_icon .toggleClass('client-input_muted', state === "muted") .toggleClass('client-capture', state === "enabled") .toggleClass('client-activate_microphone', state === "disabled"); + */ + + tag_icon + .toggleClass('client-input_muted', state !== "disabled") + .toggleClass('client-capture', false) + .toggleClass('client-activate_microphone', state === "disabled"); if(state === "disabled") tag_icon.attr('title', tr("Enable your microphone on this server")); @@ -224,12 +260,17 @@ class ControlBar { this._button_speakers = state; let tag = this.htmlTag.find(".btn_mute_output"); - const tag_icon = tag.find(".icon_x32, .icon"); + const tag_icon = tag.find(".icon_em, .icon"); tag.toggleClass('activated', state === "muted"); + /* tag_icon .toggleClass('client-output_muted', state !== "enabled") .toggleClass('client-volume', state === "enabled"); + */ + tag_icon + .toggleClass('client-output_muted', true) + .toggleClass('client-volume', false); if(state === "enabled") tag_icon.attr('title', tr("Mute sound")); @@ -245,7 +286,7 @@ class ControlBar { this.htmlTag .find(".button-subscribe-mode") .toggleClass('activated', this._button_subscribe_all) - .find('.icon_x32') + .find('.icon_em') .toggleClass('client-unsubscribe_from_all_channels', !this._button_subscribe_all) .toggleClass('client-subscribe_to_all_channels', this._button_subscribe_all); } @@ -320,10 +361,13 @@ class ControlBar { private on_toggle_microphone() { - if(this._button_microphone === "disabled" || this._button_microphone === "muted") + if(this._button_microphone === "disabled" || this._button_microphone === "muted") { this.button_microphone = "enabled"; - else + sound.manager.play(Sound.MICROPHONE_ACTIVATED); + } else { this.button_microphone = "muted"; + sound.manager.play(Sound.MICROPHONE_MUTED); + } if(this.connection_handler) { this.connection_handler.client_status.input_muted = this._button_microphone !== "enabled"; @@ -338,10 +382,13 @@ class ControlBar { } private on_toggle_sound() { - if(this._button_speakers === "muted") + if(this._button_speakers === "muted") { this.button_speaker = "enabled"; - else + sound.manager.play(Sound.SOUND_ACTIVATED); + } else { this.button_speaker = "muted"; + sound.manager.play(Sound.SOUND_MUTED); + } if(this.connection_handler) { this.connection_handler.client_status.output_muted = this._button_speakers !== "enabled"; @@ -379,8 +426,17 @@ class ControlBar { private on_open_connect() { if(this.connection_handler) - this.connection_handler.cancel_reconnect(); + this.connection_handler.cancel_reconnect(true); + Modals.spawnConnectModal({}, { + url: "ts.TeaSpeak.de", + enforce: false + }); + } + + private on_open_connect_new_tab() { Modals.spawnConnectModal({ + default_connect_new_tab: true + }, { url: "ts.TeaSpeak.de", enforce: false }); @@ -410,7 +466,7 @@ class ControlBar { } private on_execute_disconnect() { - this.connection_handler.cancel_reconnect(); + this.connection_handler.cancel_reconnect(true); this.connection_handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message? this.update_connection_state(); this.connection_handler.sound.play(Sound.CONNECTION_DISCONNECTED); @@ -426,7 +482,7 @@ class ControlBar { createInfoModal(tr("Use token"), tr("Toke successfully used!")).open(); }).catch(error => { //TODO tr - createErrorModal(tr("Use token"), "Failed to use token: " + (error instanceof CommandResult ? error.message : error)).open(); + createErrorModal(tr("Use token"), MessageHelper.formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open(); }); }).open(); } @@ -459,23 +515,7 @@ class ControlBar { } private on_bookmark_server_add() { - if(this.connection_handler && this.connection_handler.connected) { - createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:
"), text => true, result => { - if(result) { - const bookmark = bookmarks.create_bookmark(result as string, bookmarks.bookmarks(), { - server_port: this.connection_handler.serverConnection.remote_address().port, - server_address: this.connection_handler.serverConnection.remote_address().host, - - server_password: "", - server_password_hash: "" - }, this.connection_handler.getClient().clientNickName()); - bookmarks.save_bookmark(bookmark); - this.update_bookmarks() - } - }).open(); - } else { - createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open(); - } + bookmarks.add_current_server(); } update_bookmark_status() { @@ -486,8 +526,8 @@ class ControlBar { update_bookmarks() { // - let tag_bookmark = this.htmlTag.find(".btn_bookmark .dropdown"); - tag_bookmark.find(".bookmark, .directory").detach(); + let tag_bookmark = this.htmlTag.find(".btn_bookmark > .dropdown"); + tag_bookmark.find(".bookmark, .directory").remove(); const build_entry = (bookmark: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => { if(bookmark.type == bookmarks.BookmarkType.ENTRY) { @@ -495,37 +535,14 @@ class ControlBar { const bookmark_connect = (new_tab: boolean) => { this.htmlTag.find(".btn_bookmark").find(".dropdown").removeClass("displayed"); //FIXME Not working - - const profile = profiles.find_profile(mark.connect_profile) || profiles.default_profile(); - if(profile.valid()) { - const connection = this.connection_handler && !new_tab ? this.connection_handler : server_connections.spawn_server_connection_handler(); - server_connections.set_active_connection_handler(connection); - connection.startConnection( - mark.server_properties.server_address + ":" + mark.server_properties.server_port, - profile, - { - nickname: mark.nickname, - password: { - password: mark.server_properties.server_password_hash, - hashed: true - } - } - ); - } else { - Modals.spawnConnectModal({ - url: mark.server_properties.server_address + ":" + mark.server_properties.server_port, - enforce: true - }, { - profile: profile, - enforce: true - }) - } + bookmarks.boorkmak_connect(mark, new_tab); }; return $.spawn("div") .addClass("bookmark") .append( - $.spawn("div").addClass("icon client-server") + //$.spawn("div").addClass("icon client-server") + IconManager.generate_tag(IconManager.load_cached_icon(mark.last_icon_id || 0), {animate: false}) /* must be false */ ) .append( $.spawn("div") @@ -550,7 +567,8 @@ class ControlBar { type: contextmenu.MenuEntryType.ENTRY, name: tr("Connect in a new tab"), icon_class: 'client-connect', - callback: () => bookmark_connect(true) + callback: () => bookmark_connect(true), + visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION) }, contextmenu.Entry.CLOSE(() => { setTimeout(() => { this.htmlTag.find(".btn_bookmark.dropdown-arrow").removeClass("force-show") @@ -564,10 +582,7 @@ class ControlBar { const mark = bookmark; const container = $.spawn("div").addClass("sub-menu dropdown"); - for(const member of mark.content) - container.append(build_entry(member)); - - return $.spawn("div") + const result = $.spawn("div") .addClass("directory") .append( $.spawn("div").addClass("icon client-folder") @@ -583,7 +598,13 @@ class ControlBar { .append( $.spawn("div").addClass("sub-container") .append(container) - ) + ); + + /* we've to keep it this order because we're then keeping the reference of the loading icons... */ + for(const member of mark.content) + container.append(build_entry(member)); + + return result; } }; diff --git a/shared/js/ui/frames/MenuBar.ts b/shared/js/ui/frames/MenuBar.ts new file mode 100644 index 00000000..ae5c1280 --- /dev/null +++ b/shared/js/ui/frames/MenuBar.ts @@ -0,0 +1,517 @@ +namespace top_menu { + export interface HRItem { } + + export interface MenuItem { + append_item(label: string): MenuItem; + append_hr(): HRItem; + delete_item(item: MenuItem | HRItem); + items() : (MenuItem | HRItem)[]; + + icon(klass?: string | Promise | Icon) : string; + label(value?: string) : string; + visible(value?: boolean) : boolean; + disabled(value?: boolean) : boolean; + click(callback: () => any) : this; + } + + export interface MenuBarDriver { + initialize(); + + append_item(label: string) : MenuItem; + delete_item(item: MenuItem); + items() : MenuItem[]; + + flush_changes(); + } + + let _driver: MenuBarDriver; + export function driver() : MenuBarDriver { + return _driver; + } + + export function set_driver(driver: MenuBarDriver) { + _driver = driver; + } + + export interface NativeActions { + open_dev_tools(); + reload_page(); + + check_native_update(); + open_change_log(); + + quit(); + } + export let native_actions: NativeActions; + + namespace html { + class HTMLHrItem implements top_menu.HRItem { + readonly html_tag: JQuery; + + constructor() { + this.html_tag = $.spawn("hr"); + } + } + + class HTMLMenuItem implements top_menu.MenuItem { + readonly html_tag: JQuery; + readonly _label_tag: JQuery; + readonly _label_icon_tag: JQuery; + readonly _label_text_tag: JQuery; + readonly _submenu_tag: JQuery; + + private _items: (MenuItem | HRItem)[] = []; + private _label: string; + private _callback_click: () => any; + + + constructor(label: string, mode: "side" | "down") { + this._label = label; + + this.html_tag = $.spawn("div").addClass("container-menu-item type-" + mode); + + this._label_tag = $.spawn("div").addClass("menu-item"); + this._label_icon_tag = $.spawn("div").addClass("container-icon").appendTo(this._label_tag); + $.spawn("div").addClass("container-label").append( + this._label_text_tag = $.spawn("a").text(label) + ).appendTo(this._label_tag); + this._label_tag.on('click', event => { + if(event.isDefaultPrevented()) + return; + + const disabled = this.html_tag.hasClass("disabled"); + if(this._callback_click && !disabled) { + this._callback_click(); + } + event.preventDefault(); + if(disabled) event.stopPropagation(); + }); + + this._submenu_tag = $.spawn("div").addClass("sub-menu"); + + this.html_tag.append(this._label_tag); + this.html_tag.append(this._submenu_tag); + } + + append_item(label: string): top_menu.MenuItem { + const item = new HTMLMenuItem(label, "side"); + this._items.push(item); + this._submenu_tag.append(item.html_tag); + this.html_tag.addClass('sub-entries'); + return item; + } + + append_hr(): HRItem { + const item = new HTMLHrItem(); + this._items.push(item); + this._submenu_tag.append(item.html_tag); + return item; + } + + delete_item(item: top_menu.MenuItem | top_menu.HRItem) { + this._items.remove(item); + (item as any).html_tag.detach(); + this.html_tag.toggleClass('sub-entries', this._items.length > 0); + } + + disabled(value?: boolean): boolean { + if(typeof(value) === "undefined") + return this.html_tag.hasClass("disabled"); + + this.html_tag.toggleClass("disabled", value); + return value; + } + + items(): (top_menu.MenuItem | top_menu.HRItem)[] { + return this._items; + } + + label(value?: string): string { + if(typeof(value) === "undefined" || this._label === value) + return this._label; + + return this._label; + } + + visible(value?: boolean): boolean { + if(typeof(value) === "undefined") + return this.html_tag.is(':visible'); //FIXME! + + this.html_tag.toggle(!!value); + return value; + } + + click(callback: () => any): this { + this._callback_click = callback; + return this; + } + + icon(klass?: string | Promise | Icon): string { + this._label_icon_tag.children().remove(); + if(typeof(klass) === "string") + $.spawn("div").addClass("icon_em " + klass).appendTo(this._label_icon_tag); + else + IconManager.generate_tag(klass).appendTo(this._label_icon_tag); + return ""; + } + + } + + export class HTMLMenuBarDriver implements MenuBarDriver { + private static _instance: HTMLMenuBarDriver; + public static instance() : HTMLMenuBarDriver { + if(!this._instance) + this._instance = new HTMLMenuBarDriver(); + return this._instance; + } + + readonly html_tag: JQuery; + + private _items: MenuItem[] = []; + constructor() { + this.html_tag = $.spawn("div").addClass("top-menu-bar"); + } + + append_item(label: string): top_menu.MenuItem { + const item = new HTMLMenuItem(label, "down"); + this._items.push(item); + + this.html_tag.append(item.html_tag); + item._label_tag.on('click', event => { + event.preventDefault(); + + this.html_tag.find(".active").removeClass("active"); + item.html_tag.addClass("active"); + + setTimeout(() => { + $(document).one('click focusout', event => item.html_tag.removeClass("active")); + }, 0); + }); + return item; + } + + delete_item(item: MenuItem) { + return undefined; + } + + items(): top_menu.MenuItem[] { + return this._items; + } + + flush_changes() { /* unused, all changed were made instantly */ } + + initialize() { + $("#top-menu-bar").replaceWith(this.html_tag); + } + } + } + + let _items_bookmark: { + root: MenuItem, + manage: MenuItem, + add_current: MenuItem + }; + + export function rebuild_bookmarks() { + if(!_items_bookmark) { + _items_bookmark = { + root: driver().append_item(tr("Favorites")), + + add_current: undefined, + manage: undefined + }; + _items_bookmark.manage = _items_bookmark.root.append_item(tr("Manage bookmarks")); + _items_bookmark.manage.icon("client-bookmark_manager"); + _items_bookmark.manage.click(() => Modals.spawnBookmarkModal()); + + _items_bookmark.add_current = _items_bookmark.root.append_item(tr("Add current server to bookmarks")); + _items_bookmark.add_current.icon('client-bookmark_add'); + _items_bookmark.add_current.click(() => bookmarks.add_current_server()); + _state_updater["bookmarks.ac"] = { item: _items_bookmark.add_current, conditions: [condition_connected]}; + } + + _items_bookmark.root.items().filter(e => e !== _items_bookmark.add_current && e !== _items_bookmark.manage).forEach(e => { + _items_bookmark.root.delete_item(e); + }); + _items_bookmark.root.append_hr(); + + const build_bookmark = (root: MenuItem, entry: bookmarks.DirectoryBookmark | bookmarks.Bookmark) => { + if(entry.type == bookmarks.BookmarkType.DIRECTORY) { + const directory = entry as bookmarks.DirectoryBookmark; + const item = root.append_item(directory.display_name); + item.icon('client-folder'); + for(const entry of directory.content) + build_bookmark(item, entry); + if(directory.content.length == 0) + item.disabled(true); + } else { + const bookmark = entry as bookmarks.Bookmark; + const item = root.append_item(bookmark.display_name); + item.icon(IconManager.load_cached_icon(bookmark.last_icon_id || 0)); + item.click(() => bookmarks.boorkmak_connect(bookmark)); + } + }; + + for(const entry of bookmarks.bookmarks().content) + build_bookmark(_items_bookmark.root, entry); + driver().flush_changes(); + } + + /* will be called on connection handler change or on client connect state or mic state change etc... */ + let _state_updater: {[key: string]:{ item: MenuItem; conditions: (() => boolean)[], update_handler?: (item: MenuItem) => any }} = {}; + export function update_state() { + for(const _key of Object.keys(_state_updater)) { + const item = _state_updater[_key]; + if(item.update_handler) { + if(item.update_handler(item.item)) + continue; + } + let enabled = true; + for(const condition of item.conditions) + if(!condition()) { + enabled = false; + break; + } + item.item.disabled(!enabled); + } + driver().flush_changes(); + } + + const condition_connected = () => { + const scon = server_connections ? server_connections.active_connection_handler() : undefined; + return scon && scon.connected; + }; + + declare namespace native { + export function initialize(); + } + + export function initialize() { + const driver = top_menu.driver(); + driver.initialize(); + + /* build connection */ + let item: MenuItem; + { + const menu = driver.append_item(tr("Connection")); + item = menu.append_item("Connect to a server"); + item.icon('client-connect'); + item.click(() => Modals.spawnConnectModal({})); + + const do_disconnect = (handlers: ConnectionHandler[]) => { + for(const handler of handlers) { + handler.cancel_reconnect(true); + handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message? + server_connections.active_connection_handler().serverConnection.disconnect(); + handler.sound.play(Sound.CONNECTION_DISCONNECTED); + } + control_bar.update_connection_state(); + update_state(); + }; + item = menu.append_item("Disconnect from current server"); + item.icon('client-disconnect'); + item.disabled(true); + item.click(() => { + const handler = server_connections.active_connection_handler(); + do_disconnect([handler]); + }); + _state_updater["connection.dc"] = { item: item, conditions: [() => condition_connected()]}; + + item = menu.append_item("Disconnect from all servers"); + item.icon('client-disconnect'); + item.click(() => { + do_disconnect(server_connections.server_connection_handlers()); + }); + _state_updater["connection.dca"] = { item: item, conditions: [], update_handler: (item) => { + item.visible(server_connections && server_connections.server_connection_handlers().length > 1); + return true; + }}; + + if(!app.is_web()) { + menu.append_hr(); + + item = menu.append_item(tr("Quit")); + item.icon('client-close_button'); + item.click(() => native_actions.quit()); + } + } + { + rebuild_bookmarks(); + } + + if(false) { + const menu = driver.append_item("Self"); + /* Microphone | Sound | Away */ + } + + { + const menu = driver.append_item("Rights"); + + item = menu.append_item(tr("Server Groups")); + item.icon("client-permission_server_groups"); + item.click(() => { + Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "sg").open(); + }); + _state_updater["permission.sg"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Client Permissions")); + item.icon("client-permission_client"); + item.click(() => { + Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "clp").open(); + }); + _state_updater["permission.clp"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Channel Client Permissions")); + item.icon("client-permission_client"); + item.click(() => { + Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "clchp").open(); + }); + _state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Channel Groups")); + item.icon("client-permission_channel"); + item.click(() => { + Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "cg").open(); + }); + _state_updater["permission.cg"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Channel Permissions")); + item.icon("client-permission_channel"); + item.click(() => { + Modals.spawnPermissionEdit(server_connections.active_connection_handler(), "chp").open(); + }); + _state_updater["permission.cp"] = { item: item, conditions: [condition_connected]}; + + menu.append_hr(); + item = menu.append_item(tr("List Privilege Keys")); + item.icon("client-token"); + item.click(() => { + createErrorModal(tr("Not implemented"), tr("Privilege key list is not implemented yet!")).open(); + }); + _state_updater["permission.pk"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Use Privilege Key")); + item.icon("client-token_use"); + item.click(() => { + //TODO: Fixeme use one method for the control bar and here! + createInputModal(tr("Use token"), tr("Please enter your token/priviledge key"), message => message.length > 0, result => { + if(!result) return; + const scon = server_connections.active_connection_handler(); + + if(scon.serverConnection.connected) + scon.serverConnection.send_command("tokenuse", { + token: result + }).then(() => { + createInfoModal(tr("Use token"), tr("Toke successfully used!")).open(); + }).catch(error => { + //TODO tr + createErrorModal(tr("Use token"), MessageHelper.formatMessage(tr("Failed to use token: {}"), error instanceof CommandResult ? error.message : error)).open(); + }); + }).open(); + }); + _state_updater["permission.upk"] = { item: item, conditions: [condition_connected]}; + } + + { + const menu = driver.append_item("Tools"); + + item = menu.append_item(tr("Manage Playlists")); + item.icon('client-music'); + item.click(() => { + const scon = server_connections.active_connection_handler(); + if(scon && scon.connected) { + Modals.spawnPlaylistManage(scon); + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); + } + }); + _state_updater["tools.pl"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Ban List")); + item.icon('client-ban_list'); + item.click(() => { + const scon = server_connections.active_connection_handler(); + if(scon && scon.connected) { + if(scon.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) { + Modals.openBanList(scon); + } else { + createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the ban list")).open(); + scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); + } + Modals.spawnPlaylistManage(scon); + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); + } + }); + _state_updater["tools.bl"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Query List")); + item.icon('client-server_query'); + item.click(() => { + const scon = server_connections.active_connection_handler(); + if(scon && scon.connected) { + if(scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST_OWN).granted(1)) { + Modals.spawnQueryManage(scon); + } else { + createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to view the server query list")).open(); + scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); + } + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); + } + }); + _state_updater["tools.ql"] = { item: item, conditions: [condition_connected]}; + + item = menu.append_item(tr("Query Create")); + item.icon('client-server_query'); + item.click(() => { + const scon = server_connections.active_connection_handler(); + if(scon && scon.connected) { + if(scon.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1)) { + Modals.spawnQueryManage(scon); + } else { + createErrorModal(tr("You dont have the permission"), tr("You dont have the permission to create a server query login")).open(); + scon.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); + } + } else { + createErrorModal(tr("You have to be connected"), tr("You have to be connected to use this function!")).open(); + } + }); + _state_updater["tools.qc"] = { item: item, conditions: [condition_connected]}; + menu.append_hr(); + + item = menu.append_item(tr("Settings")); + item.icon("client-settings"); + item.click(() => Modals.spawnSettingsModal()); + } + + { + const menu = driver.append_item("Help"); + + if(!app.is_web()) { + item = menu.append_item(tr("Check for updates")); + item.click(() => native_actions.check_native_update()); + + item = menu.append_item(tr("Open changelog")); + item.click(() => native_actions.open_change_log()); + } + + item = menu.append_item(tr("Visit TeaSpeak.de")); + //TODO: Client direct browser? + item.click(() => window.open('https://teaspeak.de/', '_blank')); + + item = menu.append_item(tr("Visit TeaSpeak forum")); + //TODO: Client direct browser? + item.click(() => window.open('https://forum.teaspeak.de/', '_blank')); + + menu.append_hr(); + item = menu.append_item(app.is_web() ? tr("About TeaWeb") : tr("About TeaClient")); + item.click(() => Modals.spawnAbout()) + } + + update_state(); + } + + /* default is HTML, the client will override this */ + set_driver(html.HTMLMenuBarDriver.instance()); +} \ No newline at end of file diff --git a/shared/js/ui/frames/SelectedItemInfo.ts b/shared/js/ui/frames/SelectedItemInfo.ts index 1c9d1bca..0d05f7ee 100644 --- a/shared/js/ui/frames/SelectedItemInfo.ts +++ b/shared/js/ui/frames/SelectedItemInfo.ts @@ -60,7 +60,6 @@ class InfoBar this.close_popover()); } @@ -83,6 +80,16 @@ class InfoBar).updateFrame(this.current_selected, this._tag_info); } - update_banner() { - this.banner_manager.update(); - } - current_manager() { return this._current_manager; } is_popover() : boolean { @@ -138,7 +139,6 @@ class InfoBar; - readonly client: ConnectionHandler; - - private updater: NodeJS.Timer; - private _hostbanner_url: string; - - constructor(client: ConnectionHandler, htmlTag: JQuery) { - this.client = client; - this.html_tag = htmlTag; - } - - update() { - if(this.updater) { - clearTimeout(this.updater); - this.updater = undefined; - } - - const tag = this.generate_tag(); - - if(tag) { - tag.then(element => { - const children = this.html_tag.children(); - this.html_tag.append(element).removeClass("disabled"); - - /* allow the new image be loaded from cache URL */ - { - children - .css('z-index', '2') - .css('position', 'absolute') - .css('height', '100%') - .css('width', '100%'); - setTimeout(() => { - children.detach(); - }, 250); - } - }).catch(error => { - console.warn(tr("Failed to load hostbanner: %o"), error); - this.html_tag.empty().addClass("disabled"); - }) - } else { - this.html_tag.empty().addClass("disabled"); - } - } - - handle_resize() { - this.html_tag.find("[x-divider-require-resize]").trigger('resize'); - } - - private generate_tag?() : Promise> { - if(!this.client.connected) return undefined; - - const server = this.client.channelTree.server; - if(!server) return undefined; - if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined; - - let properties: any = {}; - for(let key in server.properties) - properties["property_" + key] = server.properties[key]; - - properties["hostbanner_gfx_url"] = server.properties.virtualserver_hostbanner_gfx_url; - if(server.properties.virtualserver_hostbanner_gfx_interval > 0) { - const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60); - const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString(); - try { - const url = new URL(server.properties.virtualserver_hostbanner_gfx_url); - if(url.search.length == 0) - properties["hostbanner_gfx_url"] += "?_ts=" + update_timestamp; - else - properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp; - } catch(error) { - console.warn(tr("Failed to parse banner URL: %o"), error); - properties["hostbanner_gfx_url"] += "&_ts=" + update_timestamp; - } - - this.updater = setTimeout(() => this.update(), update_interval * 1000); - } - - const rendered = $("#tmpl_selected_hostbanner").renderTag(properties); - - /* ration watcher */ - if(server.properties.virtualserver_hostbanner_mode == 2) { - const jimage = rendered.find(".meta-image"); - if(jimage.length == 0) { - log.warn(LogCategory.SERVER, tr("Missing hostbanner meta image tag")); - } else { - const image = jimage[0]; - image.onload = event => { - const image: HTMLImageElement = jimage[0] as any; - rendered.on('resize', event => { - const container = rendered.parent(); - container.css('height', null); - container.css('flex-grow', '1'); - - const max_height = rendered.visible_height(); - const max_width = rendered.visible_width(); - container.css('flex-grow', '0'); - - - const original_height = image.naturalHeight; - const original_width = image.naturalWidth; - - const ratio_height = max_height / original_height; - const ratio_width = max_width / original_width; - - const ratio = Math.min(ratio_height, ratio_width); - - if(ratio == 0) - return; - const hostbanner_height = ratio * original_height; - container.css('height', Math.ceil(hostbanner_height) + "px"); - /* the width is ignorable*/ - }); - setTimeout(() => rendered.trigger('resize'), 100); - }; - } - } - - if(window.fetch) { - return (async () => { - const start = Date.now(); - - const tag_image = rendered.find(".hostbanner-image"); - - _fetch: - try { - const result = await fetch(properties["hostbanner_gfx_url"]); - - if(!result.ok) { - if(result.type === 'opaque' || result.type === 'opaqueredirect') { - log.warn(LogCategory.SERVER, tr("Could not load hostbanner because 'Access-Control-Allow-Origin' isnt valid!")); - break _fetch; - } - } - - if(this._hostbanner_url) { - log.debug(LogCategory.SERVER, tr("Revoked old hostbanner url %s"), this._hostbanner_url); - URL.revokeObjectURL(this._hostbanner_url); - } - const url = (this._hostbanner_url = URL.createObjectURL(await result.blob())); - tag_image.css('background-image', 'url(' + url + ')'); - tag_image.attr('src', url); - log.debug(LogCategory.SERVER, tr("Fetsched hostbanner successfully (%o, type: %o, url: %o)"), Date.now() - start, result.type, url); - } catch(error) { - log.warn(LogCategory.SERVER, tr("Failed to fetch hostbanner image: %o"), error); - } - return rendered; - })(); - } else { - console.debug(tr("Hostbanner has been loaded")); - return Promise.resolve(rendered); - } - } -} - class ClientInfoManager extends InfoManager { available(object: V): boolean { return typeof object == "object" && object instanceof ClientEntry; diff --git a/shared/js/ui/frames/chat.ts b/shared/js/ui/frames/chat.ts index c54694db..4613fec6 100644 --- a/shared/js/ui/frames/chat.ts +++ b/shared/js/ui/frames/chat.ts @@ -93,12 +93,19 @@ namespace MessageHelper { const result: xbbcode.Result = xbbcode.parse(message, { /* TODO make this configurable and allow IMG */ tag_whitelist: [ - "b", - "i", - "u", + "b", "big", + "i", "italic", + "u", "underlined", "color", - "url" - ] + "url", + "code", + "icode", + "i-code", + + "ul", "ol", "list", + "li", + /* "img" */ + ] //[img]https://i.ytimg.com/vi/kgeSTkZssPg/maxresdefault.jpg[/img] }); /* if(result.error) { @@ -106,470 +113,58 @@ namespace MessageHelper { return formatElement(message); } */ - return [$.spawn("div").html(result.build_html()).contents() as any]; + + let html = result.build_html(); + + if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)) + html = twemoji.parse(html); + + const container = $.spawn("div"); + container[0].innerHTML = DOMPurify.sanitize(html, { + ADD_ATTR: [ + "x-highlight-type", + "x-code-type" + ] + }); + + container.find("a").attr('target', "_blank"); + + return [container.contents() as JQuery]; //return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry)); } -} -class ChatMessage { - date: Date; - message: JQuery[]; - private _html_tag: JQuery; + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: "XBBCode code tag init", + function: async () => { + /* override default parser */ + xbbcode.register.register_parser( { + tag: ["code", "icode", "i-code"], + content_tags_whitelist: [], - constructor(message: JQuery[]) { - this.date = new Date(); - this.message = message; - } + build_html(layer: xbbcode.TagLayer) : string { + const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code"; + const language = (layer.options || "").replace("\"", "'").toLowerCase(); - private num(num: number) : string { - let str = num.toString(); - while(str.length < 2) str = '0' + str; - return str; - } + /* remove heading empty lines */ + let text = layer.content.map(e => e.build_text()) + .reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "") + .replace(/^([ \n\r\t]*)(?=\n)+/g, ""); + if(text.startsWith("\r") || text.startsWith("\n")) + text = text.substr(1); - get html_tag() { - if(this._html_tag) return this._html_tag; + let result: HighlightJSResult; + if(window.hljs.getLanguage(language)) + result = window.hljs.highlight(language, text, true); + else + result = window.hljs.highlightAuto(text); - let tag = $.spawn("div"); - tag.addClass("message"); - - let dateTag = $.spawn("div"); - dateTag.text("<" + this.num(this.date.getUTCHours()) + ":" + this.num(this.date.getUTCMinutes()) + ":" + this.num(this.date.getUTCSeconds()) + "> "); - dateTag.css("margin-right", "4px"); - dateTag.css("color", "dodgerblue"); - - this._html_tag = tag; - tag.append(dateTag); - this.message.forEach(e => e.appendTo(tag)); - return tag; - } -} - -class ChatEntry { - readonly handle: ChatBox; - type: ChatType; - key: string; - history: ChatMessage[] = []; - send_history: string[] = []; - - owner_unique_id?: string; - - private _name: string; - private _html_tag: any; - - private _flag_closeable: boolean = true; - private _flag_unread : boolean = false; - private _flag_offline: boolean = false; - - onMessageSend: (text: string) => void; - onClose: () => boolean = () => true; - - constructor(handle, type : ChatType, key) { - this.handle = handle; - this.type = type; - this.key = key; - this._name = key; - } - - appendError(message: string, ...args) { - let entries = MessageHelper.formatMessage(message, ...args); - entries.forEach(e => e.css("color", "red")); - this.pushChatMessage(new ChatMessage(entries)); - } - - appendMessage(message : string, fmt: boolean = true, ...args) { - this.pushChatMessage(new ChatMessage(MessageHelper.formatMessage(message, ...args))); - } - - private pushChatMessage(entry: ChatMessage) { - this.history.push(entry); - while(this.history.length > 100) { - let elm = this.history.pop_front(); - elm.html_tag.animate({opacity: 0}, 200, function () { - $(this).detach(); - }); - } - if(this.handle.activeChat === this) { - let box = $(this.handle.htmlTag).find(".messages"); - let mbox = $(this.handle.htmlTag).find(".message_box"); - let bottom : boolean = box.scrollTop() + box.height() + 1 >= mbox.height(); - mbox.append(entry.html_tag); - entry.html_tag.css("opacity", "0").animate({opacity: 1}, 100); - if(bottom) box.scrollTop(mbox.height()); - } else { - this.flag_unread = true; - } - } - - displayHistory() { - this.flag_unread = false; - let box = this.handle.htmlTag.find(".messages"); - let mbox = box.find(".message_box").detach(); /* detach the message box to improve performance */ - mbox.empty(); - - for(let e of this.history) { - mbox.append(e.html_tag); - /* TODO Is this really totally useless? - Because its at least a performance bottleneck because is(...) recalculates the page style - if(e.htmlTag.is(":hidden")) - e.htmlTag.show(); - */ - } - - mbox.appendTo(box); - box.scrollTop(mbox.height()); - } - - get html_tag() { - if(this._html_tag) - return this._html_tag; - - let tag = $.spawn("div"); - tag.addClass("chat"); - if(this._flag_unread) - tag.addClass('unread'); - if(this._flag_offline) - tag.addClass('offline'); - if(this._flag_closeable) - tag.addClass('closeable'); - - tag.append($.spawn("div").addClass("chat-type icon " + this.chat_icon())); - tag.append($.spawn("a").addClass("name").text(this._name)); - - let tag_close = $.spawn("div"); - tag_close.addClass("btn_close icon client-tab_close_button"); - if(!this._flag_closeable) tag_close.hide(); - tag.append(tag_close); - - tag.click(() => { this.handle.activeChat = this; }); - tag.on("contextmenu", (e) => { - e.preventDefault(); - - let actions: contextmenu.MenuEntry[] = []; - actions.push({ - type: contextmenu.MenuEntryType.ENTRY, - icon_class: "", - name: tr("Clear"), - callback: () => { - this.history = []; - this.displayHistory(); + let html = '
';
+                    html += '';
+                    html += result.value;
+                    return html + "
"; } }); - if(this.flag_closeable) { - actions.push({ - type: contextmenu.MenuEntryType.ENTRY, - icon_class: "client-tab_close_button", - name: tr("Close"), - callback: () => this.handle.deleteChat(this) - }); - } - - actions.push({ - type: contextmenu.MenuEntryType.ENTRY, - icon_class: "client-tab_close_button", - name: tr("Close all private tabs"), - callback: () => { - //TODO Implement this? - }, - visible: false - }); - contextmenu.spawn_context_menu(e.pageX, e.pageY, ...actions); - }); - - tag_close.click(() => { - if($.isFunction(this.onClose) && !this.onClose()) - return; - - this.handle.deleteChat(this); - }); - - return this._html_tag = tag; - } - - focus() { - this.handle.activeChat = this; - this.handle.htmlTag.find(".input_box").focus(); - } - - set name(newName : string) { - this._name = newName; - this.html_tag.find(".name").text(this._name); - } - - set flag_closeable(flag : boolean) { - if(this._flag_closeable == flag) return; - - this._flag_closeable = flag; - - this.html_tag.toggleClass('closeable', flag); - } - - set flag_unread(flag : boolean) { - if(this._flag_unread == flag) return; - this._flag_unread = flag; - this.html_tag.find(".chat-type").attr("class", "chat-type icon " + this.chat_icon()); - this.html_tag.toggleClass('unread', flag); - } - - get flag_offline() { return this._flag_offline; } - - set flag_offline(flag: boolean) { - if(flag == this._flag_offline) - return; - - this._flag_offline = flag; - this.html_tag.toggleClass('offline', flag); - } - - private chat_icon() : string { - if(this._flag_unread) { - switch (this.type) { - case ChatType.CLIENT: - return "client-new_chat"; - } - } - switch (this.type) { - case ChatType.SERVER: - return "client-server_log"; - case ChatType.CHANNEL: - return "client-channel_chat"; - case ChatType.CLIENT: - return "client-player_chat"; - case ChatType.GENERAL: - return "client-channel_chat"; - } - return ""; - } -} - - -class ChatBox { - //https://regex101.com/r/YQbfcX/2 - //static readonly URL_REGEX = /^(?([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?(?:[^\s?]+)?)(?:\?(?\S+))?)?$/gm; - static readonly URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm; - - readonly connection_handler: ConnectionHandler; - htmlTag: JQuery; - chats: ChatEntry[]; - private _activeChat: ChatEntry; - private _history_index: number = 0; - - private _button_send: JQuery; - private _input_message: JQuery; - - constructor(connection_handler: ConnectionHandler) { - this.connection_handler = connection_handler; - } - - initialize() { - this.htmlTag = $("#tmpl_frame_chat").renderTag(); - this._button_send = this.htmlTag.find(".button-send"); - this._input_message = this.htmlTag.find(".input-message"); - - this._button_send.click(this.onSend.bind(this)); - this._input_message.on('keypress',event => { - if(!event.shiftKey) { - console.log(event.keyCode); - if(event.keyCode == KeyCode.KEY_RETURN) { - this.onSend(); - return false; - } else if(event.keyCode == KeyCode.KEY_UP || event.keyCode == KeyCode.KEY_DOWN) { - if(this._activeChat) { - const message = (this._input_message.val() || "").toString(); - const history = this._activeChat.send_history; - - if(history.length == 0 || this._history_index > history.length) - return; - - if(message.replace(/[ \n\r\t]/, "").length == 0 || this._history_index == 0 || (this._history_index > 0 && message == this._activeChat.send_history[this._history_index - 1])) { - if(event.keyCode == KeyCode.KEY_UP) - this._history_index = Math.min(history.length, this._history_index + 1); - else - this._history_index = Math.max(0, this._history_index - 1); - - if(this._history_index > 0) - this._input_message.val(this._activeChat.send_history[this._history_index - 1]); - else - this._input_message.val(""); - } - } - } - } - }).on('input', (event) => { - let text = $(event.target).val().toString(); - if(this.testMessage(text)) - this._button_send.removeAttr("disabled"); - else - this._button_send.attr("disabled", "true"); - }).trigger("input"); - - this.chats = []; - this._activeChat = undefined; - - this.createChat("chat_server", ChatType.SERVER).onMessageSend = (text: string) => { - if(!this.connection_handler.serverConnection) { - this.serverChat().appendError(tr("Could not send chat message (Not connected)")); - return; - } - - this.connection_handler.serverConnection.command_helper.sendMessage(text, ChatType.SERVER).catch(error => { - if(error instanceof CommandResult) - return; - - this.serverChat().appendMessage(tr("Failed to send text message.")); - log.error(LogCategory.GENERAL, tr("Failed to send server text message: %o"), error); - }); - }; - this.serverChat().name = tr("Server chat"); - this.serverChat().flag_closeable = false; - - this.createChat("chat_channel", ChatType.CHANNEL).onMessageSend = (text: string) => { - if(!this.connection_handler.serverConnection) { - this.channelChat().appendError(tr("Could not send chant message (Not connected)")); - return; - } - - this.connection_handler.serverConnection.command_helper.sendMessage(text, ChatType.CHANNEL, this.connection_handler.getClient().currentChannel()).catch(error => { - this.channelChat().appendMessage(tr("Failed to send text message.")); - log.error(LogCategory.GENERAL, tr("Failed to send channel text message: %o"), error); - }); - }; - this.channelChat().name = tr("Channel chat"); - this.channelChat().flag_closeable = false; - - this.connection_handler.permissions.initializedListener.push(flag => { - if(flag) this.activeChat0(this._activeChat); - }); - } - - createChat(key, type : ChatType = ChatType.CLIENT) : ChatEntry { - let chat = new ChatEntry(this, type, key); - this.chats.push(chat); - this.htmlTag.find(".chats").append(chat.html_tag); - if(!this._activeChat) this.activeChat = chat; - return chat; - } - - open_chats() : ChatEntry[] { - return this.chats; - } - - findChat(key : string) : ChatEntry { - for(let e of this.chats) - if(e.key == key) return e; - return undefined; - } - - deleteChat(chat : ChatEntry) { - this.chats.remove(chat); - chat.html_tag.detach(); - if(this._activeChat === chat) { - if(this.chats.length > 0) - this.activeChat = this.chats.last(); - else - this.activeChat = undefined; - } - } - - - onSend() { - let text = this._input_message.val().toString(); - if(!this.testMessage(text)) return; - this._input_message.val(""); - this._input_message.trigger("input"); - - /* preprocessing text */ - const words = text.split(/[ \n]/); - for(let index = 0; index < words.length; index++) { - const flag_escaped = words[index].startsWith('!'); - const unescaped = flag_escaped ? words[index].substr(1) : words[index]; - - _try: - try { - const url = new URL(unescaped); - log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url); - if(url.protocol !== 'http:' && url.protocol !== 'https:') - break _try; - if(flag_escaped) - words[index] = unescaped; - else { - text = undefined; - words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]"; - } - } catch(e) { /* word isn't an url */ } - - if(unescaped.match(ChatBox.URL_REGEX)) { - if(flag_escaped) - words[index] = unescaped; - else { - text = undefined; - words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]"; - } - } - } - - text = text || words.join(" "); - if(this._activeChat.send_history.length == 0 || this._activeChat.send_history[0] != text) - this._activeChat.send_history.unshift(text); - while(this._activeChat.send_history.length > 100) - this._activeChat.send_history.pop(); - this._history_index = 0; - if(this._activeChat && $.isFunction(this._activeChat.onMessageSend)) - this._activeChat.onMessageSend(text); - } - - set activeChat(chat : ChatEntry) { - if(this.chats.indexOf(chat) === -1) return; - if(this._activeChat == chat) return; - this.activeChat0(chat); - } - - private activeChat0(chat: ChatEntry) { - this._activeChat = chat; - for(let e of this.chats) - e.html_tag.removeClass("active"); - - let disable_input = !chat; - if(this._activeChat) { - this._activeChat.html_tag.addClass("active"); - this._activeChat.displayHistory(); - - if(!disable_input && this.connection_handler && this.connection_handler.permissions && this.connection_handler.permissions.initialized()) - switch (this._activeChat.type) { - case ChatType.CLIENT: - disable_input = false; - break; - case ChatType.SERVER: - disable_input = !this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND).granted(1); - break; - case ChatType.CHANNEL: - disable_input = !this.connection_handler.permissions.neededPermission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1); - break; - } - } - this._input_message.prop("disabled", disable_input); - } - - get activeChat() : ChatEntry { return this._activeChat; } - - channelChat() : ChatEntry { - return this.findChat("chat_channel"); - } - - serverChat() { - return this.findChat("chat_server"); - } - - focus(){ - this._input_message.focus(); - } - - private testMessage(message: string) : boolean { - message = message - .replace(/ /gi, "") - .replace(/
/gi, "") - .replace(/\n/gi, "") - .replace(//gi, ""); - return message.length > 0; - } + }, + priority: 10 + }) } \ No newline at end of file diff --git a/shared/js/ui/frames/chat_frame.ts b/shared/js/ui/frames/chat_frame.ts index 22e20284..d30107f0 100644 --- a/shared/js/ui/frames/chat_frame.ts +++ b/shared/js/ui/frames/chat_frame.ts @@ -1,29 +1,95 @@ /* the bar on the right with the chats (Channel & Client) */ namespace chat { + /* Fix some declare issues... */ + import instantiate = WebAssembly.instantiate; + + declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; + declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number; + + export enum InfoFrameMode { + NONE = "none", + CHANNEL_CHAT = "channel_chat", + PRIVATE_CHAT = "private_chat", + CLIENT_INFO = "client_info" + } export class InfoFrame { private readonly handle: Frame; private _html_tag: JQuery; + private _mode: InfoFrameMode; + + private _value_ping: JQuery; + private _ping_updater: number; constructor(handle: Frame) { this.handle = handle; this._build_html_tag(); this.update_channel_talk(); + this.update_channel_text(); + this.set_mode(InfoFrameMode.CHANNEL_CHAT); + this._ping_updater = setInterval(() => this.update_ping(), 2000); + this.update_ping(); } html_tag() : JQuery { return this._html_tag; } + destroy() { + clearInterval(this._ping_updater); + + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._value_ping = undefined; + } private _build_html_tag() { this._html_tag = $("#tmpl_frame_chat_info").renderTag(); + this._html_tag.find(".button-switch-chat-channel").on('click', () => this.handle.show_channel_conversations()); + this._value_ping = this._html_tag.find(".value-ping"); + this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations()); + } + + update_ping() { + this._value_ping.removeClass("very-good good medium poor very-poor"); + const connection = this.handle.handle.serverConnection; + if(!this.handle.handle.connected || !connection) { + this._value_ping.text("Not connected"); + return; + } + + const ping = connection.ping(); + if(!ping || typeof(ping.native) !== "number") { + this._value_ping.text("Not available"); + return; + } + + let value; + if(typeof(ping.javascript) !== "undefined") { + value = ping.javascript; + this._value_ping.text(ping.javascript.toFixed(0) + "ms").attr('title', 'Native: ' + ping.native.toFixed(3) + "ms \nJavascript: " + ping.javascript.toFixed(3) + "ms"); + } else { + value = ping.native; + this._value_ping.text(ping.native.toFixed(0) + "ms").attr('title', "Ping: " + ping.native.toFixed(3) + "ms"); + } + + if(value <= 10) + this._value_ping.addClass("very-good"); + else if(value <= 30) + this._value_ping.addClass("good"); + else if(value <= 60) + this._value_ping.addClass("medium"); + else if(value <= 150) + this._value_ping.addClass("poor"); + else + this._value_ping.addClass("very-poor"); } update_channel_talk() { - const client = this.handle.handle.connected ? this.handle.handle.getClient() : undefined; + const client = this.handle.handle.getClient(); const channel = client ? client.currentChannel() : undefined; const html_tag = this._html_tag.find(".value-voice-channel"); const html_limit_tag = this._html_tag.find(".value-voice-limit"); + html_limit_tag.text(""); - html_tag.children().detach(); + html_tag.children().remove(); if(channel) { if(channel.properties.channel_icon_id != 0) @@ -44,30 +110,98 @@ namespace chat { } } - update_chat_counter() { - const count = this.handle.private_conversations().conversations().filter(e => e.is_unread()).length; - const count_container = this._html_tag.find(".container-indicator"); - const count_tag = count_container.find(".chat-counter"); - count_container.toggle(count > 0); - count_tag.text(count); - } - } + update_channel_text() { + const channel_tree = this.handle.handle.connected ? this.handle.handle.channelTree : undefined; + const current_channel_id = channel_tree ? this.handle.channel_conversations().current_channel() : 0; + const channel = channel_tree ? channel_tree.findChannel(current_channel_id) : undefined; - export enum ConversationState { - ACTIVE, - CLOSED, - OFFLINE + const tag_container = this._html_tag.find(".mode-channel_chat"); + const html_tag_title = tag_container.find(".title"); + const html_tag = tag_container.find(".value-text-channel"); + const html_limit_tag = tag_container.find(".value-text-limit"); + + /* reset */ + html_tag_title.text(tr("You're chatting in Channel")); + html_limit_tag.text(""); + html_tag.children().detach(); + + /* initialize */ + if(channel) { + if(channel.properties.channel_icon_id != 0) + this.handle.handle.fileManager.icons.generateTag(channel.properties.channel_icon_id).appendTo(html_tag); + $.spawn("div").text(channel.channelName()).appendTo(html_tag); + + let channel_limit = tr("Unlimited"); + if(!channel.properties.channel_flag_maxclients_unlimited) + channel_limit = "" + channel.properties.channel_maxclients; + else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) { + if(channel.properties.channel_maxfamilyclients >= 0) + channel_limit = "" + channel.properties.channel_maxfamilyclients; + } + html_limit_tag.text(channel.clients(false).length + " / " + channel_limit); + } else if(channel_tree && current_channel_id > 0) { + html_tag.append(MessageHelper.formatMessage(tr("Unknown channel id {}"), current_channel_id)); + } else if(channel_tree && current_channel_id == 0) { + const server = this.handle.handle.channelTree.server; + if(server.properties.virtualserver_icon_id != 0) + this.handle.handle.fileManager.icons.generateTag(server.properties.virtualserver_icon_id).appendTo(html_tag); + $.spawn("div").text(server.properties.virtualserver_name).appendTo(html_tag); + html_tag_title.text(tr("You're chatting in Server")); + } else if(this.handle.handle.connected) { + $.spawn("div").text("No channel selected").appendTo(html_tag); + } else { + $.spawn("div").text("Not connected").appendTo(html_tag); + } + } + + update_chat_counter() { + const conversations = this.handle.private_conversations().conversations(); + { + const count = conversations.filter(e => e.is_unread()).length; + const count_container = this._html_tag.find(".container-indicator"); + const count_tag = count_container.find(".chat-unread-counter"); + count_container.toggle(count > 0); + count_tag.text(count); + } + { + const count_tag = this._html_tag.find(".chat-counter"); + if(conversations.length == 0) + count_tag.text(tr("No conversations")); + else if(conversations.length == 1) + count_tag.text(tr("One conversation")); + else + count_tag.text(conversations.length + " " + tr("conversations")); + } + } + + current_mode() : InfoFrameMode { + return this._mode; + } + + set_mode(mode: InfoFrameMode) { + if(this._mode === mode) + return; + this._mode = mode; + this._html_tag.find(".mode-based").hide(); + this._html_tag.find(".mode-" + mode).show(); + } } class ChatBox { private _html_tag: JQuery; - private _html_input: JQuery; + private _html_input: JQuery; + private _enabled: boolean; private __callback_text_changed; private __callback_key_down; + private __callback_paste; + + callback_text: (text: string) => any; constructor() { + this._enabled = true; this.__callback_key_down = this._callback_key_down.bind(this); this.__callback_text_changed = this._callback_text_changed.bind(this); + this.__callback_paste = event => this._callback_paste(event); this._build_html_tag(); this._initialize_listener(); @@ -77,17 +211,39 @@ namespace chat { return this._html_tag; } + destroy() { + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._html_input = undefined; + + this.__callback_text_changed = undefined; + this.__callback_key_down = undefined; + this.__callback_paste = undefined; + + this.callback_text = undefined; + } + private _initialize_listener() { - this._html_input.on("cut paste drop keydown", (event) => setTimeout(() => this.__callback_text_changed(event), 0)); + this._html_input.on("cut paste drop key keyup", (event) => this.__callback_text_changed(event)); this._html_input.on("change", this.__callback_text_changed); this._html_input.on("keydown", this.__callback_key_down); + this._html_input.on("paste", this.__callback_paste); } private _build_html_tag() { this._html_tag = $("#tmpl_frame_chat_chatbox").renderTag({ - emojy_support: true + emojy_support: settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES) + }); + this._html_input = this._html_tag.find(".textarea") as any; + + const tag: JQuery & { lsxEmojiPicker(args: any); } = this._html_tag.find('.button-emoji') as any; + tag.lsxEmojiPicker({ + width: 300, + height: 400, + twemoji: typeof(window.twemoji) !== "undefined", + onSelect: emoji => this._html_input.html(this._html_input.html() + emoji.value), + closeOnSelect: false }); - this._html_input = this._html_tag.find("textarea") as any; } private _callback_text_changed(event) { @@ -100,6 +256,65 @@ namespace chat { text.style.height = text.scrollHeight + 'px'; } + private _text(element: HTMLElement) { + if(typeof(element) !== "object") + return element; + + if(element instanceof HTMLImageElement) + return element.alt || element.title; + if(element instanceof HTMLBRElement) { + return '\n'; + } + + if(element.childNodes.length > 0) + return [...element.childNodes].map(e => this._text(e as HTMLElement)).join(""); + + if(element.nodeType == Node.TEXT_NODE) + return element.textContent; + return element.innerText + "-"; + } + + private htmlEscape(message: string) : string { + const div = document.createElement('div'); + div.innerText = message; + message = div.innerHTML; + return message.replace(/ /g, ' '); + } + private _callback_paste(event: ClipboardEvent) { + + const _event = (event).originalEvent as ClipboardEvent || event; + const clipboard = _event.clipboardData || (window).clipboardData; + if(!clipboard) return; + + + const raw_text = clipboard.getData('text/plain'); + const selection = window.getSelection(); + if (!selection.rangeCount) + return false; + + let html_xml = clipboard.getData('text/html'); + if(!html_xml) + html_xml = $.spawn("div").text(raw_text).html(); + + const parser = new DOMParser(); + const nodes = parser.parseFromString(html_xml, "text/html"); + + const data = this._text(nodes.body); + event.preventDefault(); + + selection.deleteFromDocument(); + document.execCommand('insertHTML', false, this.htmlEscape(data)); + } + + private test_message(message: string) : boolean { + message = message + .replace(/ /gi, "") + .replace(/
/gi, "") + .replace(/\n/gi, "") + .replace(//gi, ""); + return message.length > 0; + } + private _callback_key_down(event: KeyboardEvent) { if(event.shiftKey) return; @@ -107,57 +322,930 @@ namespace chat { if(event.key.toLowerCase() === "enter") { event.preventDefault(); - //TODO Notify text! - console.log("Sending text: %s", this._html_input.val()); - this._html_input.val(undefined); + /* deactivate chatbox when no callback? */ + let text = this._html_input[0].innerText as string; + if(!this.test_message(text)) + return; + + if(this.callback_text) { + this.callback_text(helpers.preprocess_chat_message(text)); + } + + this._html_input.text(""); setTimeout(() => this.__callback_text_changed()); } } + + set_enabled(flag: boolean) { + if(this._enabled === flag) + return; + + this._enabled = flag; + this._html_input.prop("contenteditable", flag); + this._html_tag.find('.button-emoji').toggleClass("disabled", !flag); + } + + is_enabled() { + return this._enabled; + } + + focus_input() { + this._html_input.focus(); + } + } + + export namespace helpers { + //https://regex101.com/r/YQbfcX/2 + //static readonly URL_REGEX = /^(?([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?(?:[^\s?]+)?)(?:\?(?\S+))?)?$/gm; + const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm; + function process_urls(message: string) : string { + const words = message.split(/[ \n]/); + for(let index = 0; index < words.length; index++) { + const flag_escaped = words[index].startsWith('!'); + const unescaped = flag_escaped ? words[index].substr(1) : words[index]; + + _try: + try { + const url = new URL(unescaped); + log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url); + if(url.protocol !== 'http:' && url.protocol !== 'https:') + break _try; + if(flag_escaped) + words[index] = unescaped; + else { + message = undefined; + words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]"; + } + } catch(e) { /* word isn't an url */ } + + if(unescaped.match(URL_REGEX)) { + if(flag_escaped) + words[index] = unescaped; + else { + message = undefined; + words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]"; + } + } + } + + return message || words.join(" "); + } + + namespace md2bbc { + type RemarkToken = { + type: string; + tight: boolean; + lines: number[]; + level: number; + + /* img */ + alt?: string; + src?: string; + + /* link */ + href?: string; + + /* table */ + align?: string; + + /* code */ + params?: string; + + content?: string; + hLevel?: number; + children?: RemarkToken[]; + } + + export class Renderer { + private static renderers = { + "text": (renderer: Renderer, token: RemarkToken) => renderer.options().process_url ? process_urls(renderer.maybe_escape_bb(token.content)) : renderer.maybe_escape_bb(token.content), + "softbreak": () => "\n", + + "paragraph_open": () => "", + "paragraph_close": () => "\n", + + "strong_open": (renderer: Renderer, token: RemarkToken) => "[b]", + "strong_close": (renderer: Renderer, token: RemarkToken) => "[/b]", + + "em_open": (renderer: Renderer, token: RemarkToken) => "[i]", + "em_close": (renderer: Renderer, token: RemarkToken) => "[/i]", + + "del_open": () => "[s]", + "del_close": () => "[/s]", + + "sup": (renderer: Renderer, token: RemarkToken) => "[sup]" + renderer.maybe_escape_bb(token.content) + "[/sup]", + "sub": (renderer: Renderer, token: RemarkToken) => "[sub]" + renderer.maybe_escape_bb(token.content) + "[/sub]", + + "bullet_list_open": () => "[ul]", + "bullet_list_close": () => "[/ul]", + + "ordered_list_open": () => "[ol]", + "ordered_list_close": () => "[/ol]", + + "list_item_open": () => "[li]", + "list_item_close": () => "[/li]", + + "table_open": () => "[table]", + "table_close": () => "[/table]", + + "thead_open": () => "", + "thead_close": () => "", + + "tbody_open": () => "", + "tbody_close": () => "", + + "tr_open": () => "[tr]", + "tr_close": () => "[/tr]", + + "th_open": (renderer: Renderer, token: RemarkToken) => "[th" + (token.align ? ("=" + token.align) : "") + "]", + "th_close": () => "[/th]", + + "td_open": () => "[td]", + "td_close": () => "[/td]", + + "link_open": (renderer: Renderer, token: RemarkToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]", + "link_close": () => "[/url]", + + "image": (renderer: Renderer, token: RemarkToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]", + + //footnote_ref + + //"content": "==Marked text==", + //mark_open + //mark_close + + //++Inserted text++ + "ins_open": () => "[u]", + "ins_close": () => "[/u]", + + /* +``` +test +[/code] +test +``` + */ + + "code": (renderer: Renderer, token: RemarkToken) => "[i-code]" + xbbcode.escape(token.content) + "[/i-code]", + "fence": (renderer: Renderer, token: RemarkToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + xbbcode.escape(token.content) + "[/code]", + + "heading_open": (renderer: Renderer, token: RemarkToken) => "[h" + Math.min(3, token.hLevel) + "]", + "heading_close": (renderer: Renderer, token: RemarkToken) => "[/h" + Math.min(3, token.hLevel) + "]", + + "hr": () => "[hr]", + + //> Experience real-time editing with Remarkable! + //blockquote_open, + //blockquote_close + }; + + private _options; + + render(tokens: RemarkToken[], options: any, env: any) { + this._options = options; + let result = ''; + + //TODO: Escape BB-Codes + for(let index = 0; index < tokens.length; index++) { + if (tokens[index].type === 'inline') { + result += this.render_inline(tokens[index].children, index); + } else { + result += this.render_token(tokens[index], index); + } + } + + this._options = undefined; + return result; + } + + private render_token(token: RemarkToken, index: number) { + console.log("Render token: %o", token); + const renderer = Renderer.renderers[token.type]; + if(typeof(renderer) === "undefined") { + console.warn(tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token); + return token.content || ""; + } + + return renderer(this, token, index); + } + + private render_inline(tokens: RemarkToken[], index: number) { + let result = ''; + + for(let index = 0; index < tokens.length; index++) { + result += this.render_token(tokens[index], index); + } + + return result; + } + + options() : any { + return this._options; + } + + maybe_escape_bb(text: string) { + if(this._options.escape_bb) + return xbbcode.escape(text); + return text; + } + } + } + + let _renderer: any; + function process_markdown(message: string, options: { + process_url?: boolean, + escape_bb?: boolean + }) : string { + if(typeof(window.remarkable) === "undefined") + return (options.process_url ? process_urls(message) : message); + + if(!_renderer) { + _renderer = new window.remarkable.Remarkable('full'); + _renderer.set({ + typographer: true + }); + _renderer.renderer = new md2bbc.Renderer(); + } + _renderer.set({ + process_url: !!options.process_url, + escape_bb: !!options.escape_bb + }); + let result: string = _renderer.render(message); + if(result.endsWith("\n")) + result = result.substr(0, result.length - 1); + return result; + } + + export function preprocess_chat_message(message: string) : string { + const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS); + const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN); + const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE); + + if(parse_markdown) + return process_markdown(message, { + process_url: process_url, + escape_bb: escape_bb + }); + + if(escape_bb) + message = xbbcode.escape(message); + return process_url ? process_urls(message) : message; + } + + export namespace history { + let _local_cache: Cache; + + async function get_cache() { + if(_local_cache) + return _local_cache; + + if(!('caches' in window)) + throw "missing cache extension!"; + + return (_local_cache = await caches.open('chat_history')); + } + + export async function load_history(key: string) : Promise { + const cache = await get_cache(); + const request = new Request("https://_local_cache/cache_request_" + key); + const cached_response = await cache.match(request); + if(!cached_response) + return undefined; + + return await cached_response.json(); + } + + export async function save_history(key: string, value: any) { + const cache = await get_cache(); + const request = new Request("https://_local_cache/cache_request_" + key); + const data = JSON.stringify(value); + + const new_headers = new Headers(); + new_headers.set("Content-type", "application/json"); + new_headers.set("Content-length", data.length.toString()); + + + await cache.put(request, new Response(data, { + headers: new_headers + })); + } + } + + export namespace date { + export function same_day(a: number | Date, b: number | Date) { + a = a instanceof Date ? a : new Date(a); + b = b instanceof Date ? b : new Date(b); + + if(a.getDate() !== b.getDate()) + return false; + if(a.getMonth() !== b.getMonth()) + return false; + return a.getFullYear() === b.getFullYear(); + } + } + } + + export namespace format { + export namespace date { + export enum ColloquialFormat { + YESTERDAY, + TODAY, + GENERAL + } + + export function date_format(date: Date, now: Date, ignore_settings?: boolean) : ColloquialFormat { + if(!ignore_settings && !settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS)) + return ColloquialFormat.GENERAL; + + let delta_day = now.getDate() - date.getDate(); + if(delta_day < 1) /* month change? */ + delta_day = date.getDate() - now.getDate(); + if(delta_day == 0) + return ColloquialFormat.TODAY; + else if(delta_day == 1) + return ColloquialFormat.YESTERDAY; + return ColloquialFormat.GENERAL; + } + + export function format_date_general(date: Date, hours?: boolean) : string { + return ('00' + date.getDate()).substr(-2) + "." + + ('00' + date.getMonth()).substr(-2) + "." + + date.getFullYear() + + (typeof(hours) === "undefined" || hours ? " at " + + ('00' + date.getHours()).substr(-2) + ":" + + ('00' + date.getMinutes()).substr(-2) + : ""); + } + + export function format_date_colloquial(date: Date, current_timestamp: Date) : { result: string; format: ColloquialFormat } { + const format = date_format(date, current_timestamp); + if(format == ColloquialFormat.GENERAL) { + return { + result: format_date_general(date), + format: format + }; + } else { + let hrs = date.getHours(); + let time = "AM"; + if(hrs > 12) { + hrs -= 12; + time = "PM"; + } + return { + result: (format == ColloquialFormat.YESTERDAY ? tr("Yesterday at") : tr("Today at")) + " " + hrs + ":" + date.getMinutes() + " " + time, + format: format + }; + } + } + + export function format_chat_time(date: Date) : { + result: string, + next_update: number /* in MS */ + } { + const timestamp = date.getTime(); + const current_timestamp = new Date(); + + const result = { + result: "", + next_update: 0 + }; + + if(settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)) { + const format = format_date_colloquial(date, current_timestamp); + result.result = format.result; + result.next_update = 0; /* TODO: Update on day change? */ + } else { + const delta = current_timestamp.getTime() - timestamp; + if(delta < 2000) { + result.result = "now"; + result.next_update = 2500 - delta; /* update after two seconds */ + } else if(delta < 30000) { /* 30 seconds */ + result.result = Math.floor(delta / 1000) + " " + tr("seconds ago"); + result.next_update = 1000; /* update every second */ + } else if(delta < 30 * 60 * 1000) { /* 30 minutes */ + if(delta < 120 * 1000) + result.result = tr("one minute ago"); + else + result.result = Math.floor(delta / (1000 * 60)) + " " + tr("minutes ago"); + result.next_update = 60000; /* updater after a minute */ + } else { + result.result = format_date_colloquial(date, current_timestamp).result; + result.next_update = 0; /* TODO: Update on day change? */ + } + } + + return result; + } + } + export namespace time { + export function format_online_time(secs: number) : string { + let years = Math.floor(secs / (60 * 60 * 24 * 365)); + let days = Math.floor(secs / (60 * 60 * 24)) % 365; + let hours = Math.floor(secs / (60 * 60)) % 24; + let minutes = Math.floor(secs / 60) % 60; + let seconds = Math.floor(secs % 60); + + let result = ""; + if(years > 0) + result += years + " " + tr("years") + " "; + if(years > 0 || days > 0) + result += days + " " + tr("days") + " "; + if(years > 0 || days > 0 || hours > 0) + result += hours + " " + tr("hours") + " "; + if(years > 0 || days > 0 || hours > 0 || minutes > 0) + result += minutes + " " + tr("minutes") + " "; + if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0) + result += seconds + " " + tr("seconds") + " "; + else + result = tr("now") + " "; + + return result.substr(0, result.length - 1); + } + } + } + + type PrivateConversationViewEntry = { + html_tag: JQuery; + } + + type PrivateConversationMessageData = { + message_id: string; + message: string; + sender: "self" | "partner"; + + sender_name: string; + sender_unique_id: string; + sender_client_id: number; + + timestamp: number; + }; + + type PrivateConversationViewMessage = PrivateConversationMessageData & PrivateConversationViewEntry & { + time_update_id: number; + }; + type PrivateConversationViewSpacer = PrivateConversationViewEntry; + + export enum PrivateConversationState { + OPEN, + CLOSED, + DISCONNECTED, + } + + type DisplayedMessage = { + timestamp: number; + + message: PrivateConversationViewMessage | PrivateConversationViewEntry; + message_type: "spacer" | "message"; + + /* structure as following + 1. time pointer + 2. unread + 3. message + */ + tag_message: JQuery; + tag_unread: PrivateConversationViewSpacer | undefined; + tag_timepointer: PrivateConversationViewSpacer | undefined; } export class PrivateConveration { readonly handle: PrivateConverations; - private _flag_unread: boolean; private _html_entry_tag: JQuery; - private _html_messages_tag: JQuery; /* TODO: Consider to create them every time on the fly? */ - private _message_history: { - message: string; - sender: "self" | "partner"; - }[] = []; + private _message_history: PrivateConversationMessageData[] = []; + + private _callback_message: (text: string) => any; + + private _state: PrivateConversationState; + + private _last_message_updater_id: number; + + _scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */ + _html_message_container: JQuery; /* only set when this chat is selected! */ client_unique_id: string; - client_id: string; + client_id: number; client_name: string; - state: ConversationState = ConversationState.ACTIVE; + private _displayed_messages: DisplayedMessage[] = []; + private _displayed_messages_length: number = 500; + private _spacer_unread_message: DisplayedMessage; - - constructor(handle: PrivateConverations, client_unique_id: string, client_name: string) { + constructor(handle: PrivateConverations, client_unique_id: string, client_name: string, client_id: number) { this.handle = handle; this.client_name = client_name; this.client_unique_id = client_unique_id; + this.client_id = client_id; + this._state = PrivateConversationState.OPEN; - this._flag_unread = false; this._build_entry_tag(); this.set_unread_flag(false); + + this.load_history(); + } + + private history_key() { return this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier + "_" + this.client_unique_id; } + private load_history() { + helpers.history.load_history(this.history_key()).then((data: PrivateConversationMessageData[]) => { + if(!data) return; + + const flag_unread = !!this._spacer_unread_message; + for(const message of data.slice(data.length > this._displayed_messages_length ? data.length - this._displayed_messages_length : 0)) { + this.append_message(message.message, { + type: message.sender, + name: message.sender_name, + unique_id: message.sender_unique_id, + client_id: message.sender_client_id + }, new Date(message.timestamp), false); + } + + if(!flag_unread) + this.set_unread_flag(false); + + this.fix_scroll(false); + this.save_history(); + }).catch(error => { + console.warn(tr("Failed to load private conversation history for user %s on server %s: %o"), + this.client_unique_id, this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier, error); + }) + } + + private save_history() { + helpers.history.save_history(this.history_key(), this._message_history).catch(error => { + console.warn(tr("Failed to save private conversation history for user %s on server %s: %o"), + this.client_unique_id, this.handle.handle.handle.channelTree.server.properties.virtualserver_unique_identifier, error); + }); } entry_tag() : JQuery { return this._html_entry_tag; } - append_message(message: string, sender: "self" | "partner") { - this._message_history.push({ + destroy() { + this._html_message_container = undefined; /* we do not own this container */ + + this.clear_messages(false); + + this._html_entry_tag && this._html_entry_tag.remove(); + this._html_entry_tag = undefined; + + this._message_history = undefined; + } + + private _2d_flat(array: T[][]) : T[] { + const result = []; + for(const a of array) + result.push(...a.filter(e => typeof(e) !== "undefined")); + return result; + } + + messages_tags() : JQuery[] { + return this._2d_flat(this._displayed_messages.slice().reverse().map(e => [ + e.tag_timepointer ? e.tag_timepointer.html_tag : undefined, + e.tag_unread ? e.tag_unread.html_tag : undefined, + e.tag_message + ])); + } + + append_message(message: string, sender: { + type: "self" | "partner"; + name: string; + unique_id: string; + client_id: number; + }, timestamp?: Date, save_history?: boolean) { + const message_date = timestamp || new Date(); + const message_timestamp = message_date.getTime(); + + const packed_message = { message: message, - sender: sender + sender: sender.type, + sender_name: sender.name, + sender_client_id: sender.client_id, + sender_unique_id: sender.unique_id, + timestamp: message_date.getTime(), + message_id: 'undefined' + }; + + /* first of all register message in message history */ + { + let index = 0; + for(;index < this._message_history.length; index++) { + if(this._message_history[index].timestamp > message_timestamp) + continue; + this._message_history.splice(index, 0, packed_message); + break; + } + + if(index > 100) + return; /* message is too old to be displayed */ + + if(index >= this._message_history.length) + this._message_history.push(packed_message); + + while(this._message_history.length > 100) + this._message_history.pop(); + } + this._update_message_timestamp(); + + if(typeof(save_history) !== "boolean" || save_history) + this.save_history(); + + /* insert in view */ + { + const basic_view_entry = this._build_message(packed_message); + + this._register_displayed_message({ + timestamp: basic_view_entry.timestamp, + message: basic_view_entry, + message_type: "message", + tag_message: basic_view_entry.html_tag, + tag_timepointer: undefined, + tag_unread: undefined + }); + } + } + + private _displayed_message_first_tag(message: DisplayedMessage) { + const tp = message.tag_timepointer ? message.tag_timepointer.html_tag : undefined; + const tu = message.tag_unread ? message.tag_unread.html_tag : undefined; + return tp || tu || message.tag_message; + } + + private _destroy_displayed_message(message: DisplayedMessage, update_pointers: boolean) { + if(update_pointers) { + const index = this._displayed_messages.indexOf(message); + if(index != -1 && index > 0) { + const next = this._displayed_messages[index - 1]; + if(!next.tag_timepointer && message.tag_timepointer) { + next.tag_timepointer = message.tag_timepointer; + message.tag_timepointer = undefined; + } + if(!next.tag_unread && message.tag_unread) { + this._spacer_unread_message = next; + next.tag_unread = message.tag_unread; + message.tag_unread = undefined; + } + } + + if(message == this._spacer_unread_message) + this._spacer_unread_message = undefined; + } + + this._displayed_messages.remove(message); + if(message.tag_timepointer) + this._destroy_view_entry(message.tag_timepointer); + + if(message.tag_unread) + this._destroy_view_entry(message.tag_unread); + + this._destroy_view_entry(message.message); + } + + clear_messages(save?: boolean) { + this._message_history = []; + while(this._displayed_messages.length > 0) { + this._destroy_displayed_message(this._displayed_messages[0], false); + } + + this._spacer_unread_message = undefined; + + this._update_message_timestamp(); + if(save) + this.save_history(); + } + + fix_scroll(animate: boolean) { + if(!this._html_message_container) + return; + + let offset; + if(this._spacer_unread_message) { + offset = this._displayed_message_first_tag(this._spacer_unread_message)[0].offsetTop; + console.log("Scroll by unread: %o", offset); + } else if(typeof(this._scroll_position) !== "undefined") { + offset = this._scroll_position; + console.log("Scroll by scroll: %o", offset); + } else { + offset = this._html_message_container[0].scrollHeight; + console.log("Height: %o", offset); + } + if(animate) { + this._html_message_container.stop(true).animate({ + scrollTop: offset + }, 'slow'); + } else { + this._html_message_container.stop(true).scrollTop(offset); + } + } + + private _update_message_timestamp() { + if(this._last_message_updater_id) + clearTimeout(this._last_message_updater_id); + const last_message = this._message_history[0]; + if(!last_message) { + this._html_entry_tag.find(".last-message").text(tr("no history")); + return; + } + + const timestamp = new Date(last_message.timestamp); + let time = format.date.format_chat_time(timestamp); + this._html_entry_tag.find(".last-message").text(time.result); + + if(time.next_update > 0) { + this._last_message_updater_id = setTimeout(() => this._update_message_timestamp(), time.next_update); + } else { + this._last_message_updater_id = 0; + } + } + + private _destroy_message(message: PrivateConversationViewMessage) { + if(message.time_update_id) + clearTimeout(message.time_update_id); + } + + private _build_message(message: PrivateConversationMessageData) : PrivateConversationViewMessage { + const result = message as PrivateConversationViewMessage; + if(result.html_tag) + return result; + + const timestamp = new Date(message.timestamp); + let time = format.date.format_chat_time(timestamp); + result.html_tag = $("#tmpl_frame_chat_private_message").renderTag({ + timestamp: time.result, + message_id: message.message_id, + client_name: htmltags.generate_client_object({ + add_braces: false, + client_name: message.sender_name, + client_unique_id: message.sender_unique_id, + client_id: message.sender_client_id + }), + message: MessageHelper.bbcode_chat(message.message), + avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: message.sender_client_id}, message.sender_unique_id) }); + if(time.next_update > 0) { + const _updater = () => { + time = format.date.format_chat_time(timestamp); + result.html_tag.find(".info .timestamp").text(time.result); + if(time.next_update > 0) + result.time_update_id = setTimeout(_updater, time.next_update); + else + result.time_update_id = 0; + }; + result.time_update_id = setTimeout(_updater, time.next_update); + } else { + result.time_update_id = 0; + } + + return result; + } + + private _build_spacer(message: string, type: "date" | "new" | "disconnect" | "reconnect" | "closed" | "error") : PrivateConversationViewSpacer { + const tag = $("#tmpl_frame_chat_private_spacer").renderTag({ + message: message + }).addClass("type-" + type); + return { + html_tag: tag + } + } + + private _register_displayed_message(message: DisplayedMessage) { + const message_date = new Date(message.timestamp); + + /* before := older message; after := newer message */ + let entry_before: DisplayedMessage, entry_after: DisplayedMessage; + let index = 0; + for(;index < this._displayed_messages.length; index++) { + if(this._displayed_messages[index].timestamp > message.timestamp) + continue; + + entry_after = index > 0 ? this._displayed_messages[index - 1] : undefined; + entry_before = this._displayed_messages[index]; + this._displayed_messages.splice(index, 0, message); + break; + } + if(index >= this._displayed_messages_length) { + return; /* message is out of view region */ + } + + if(index >= this._displayed_messages.length) { + entry_before = undefined; + entry_after = this._displayed_messages.last(); + this._displayed_messages.push(message); + } + + while(this._displayed_messages.length > this._displayed_messages_length) + this._destroy_displayed_message(this._displayed_messages.last(), true); + + const flag_new_message = index == 0 && (message.message_type === "spacer" || (message.message).sender === "partner"); + + /* Timeline for before - now */ + { + let append_pointer = false; + + if(entry_before) { + if(!helpers.date.same_day(message.timestamp, entry_before.timestamp)) { + append_pointer = true; + } + } else { + append_pointer = true; + } + if(append_pointer) { + const diff = format.date.date_format(message_date, new Date()); + if(diff == format.date.ColloquialFormat.YESTERDAY) + message.tag_timepointer = this._build_spacer(tr("Yesterday"), "date"); + else if(diff == format.date.ColloquialFormat.TODAY) + message.tag_timepointer = this._build_spacer(tr("Today"), "date"); + else if(diff == format.date.ColloquialFormat.GENERAL) + message.tag_timepointer = this._build_spacer(format.date.format_date_general(message_date, false), "date"); + } + } + + /* Timeline not and after */ + { + if(entry_after) { + if(helpers.date.same_day(message_date, entry_after.timestamp)) { + if(entry_after.tag_timepointer) { + this._destroy_view_entry(entry_after.tag_timepointer); + entry_after.tag_timepointer = undefined; + } + } else if(!entry_after.tag_timepointer) { + const diff = format.date.date_format(new Date(entry_after.timestamp), new Date()); + if(diff == format.date.ColloquialFormat.YESTERDAY) + entry_after.tag_timepointer = this._build_spacer(tr("Yesterday"), "date"); + else if(diff == format.date.ColloquialFormat.TODAY) + entry_after.tag_timepointer = this._build_spacer(tr("Today"), "date"); + else if(diff == format.date.ColloquialFormat.GENERAL) + entry_after.tag_timepointer = this._build_spacer(format.date.format_date_general(message_date, false), "date"); + + entry_after.tag_timepointer.html_tag.insertBefore(entry_after.tag_message); + } + } + } + + /* new message flag */ + if(flag_new_message) { + if(!this._spacer_unread_message) { + this._spacer_unread_message = message; + message.tag_unread = this._build_spacer(tr("Unread messages"), "new"); + + this.set_unread_flag(true); + } + } + + if(this._html_message_container) { + if(entry_before) { + message.tag_message.insertAfter(entry_before.tag_message); + } else if(entry_after) { + message.tag_message.insertBefore(this._displayed_message_first_tag(entry_after)); + } else { + this._html_message_container.append(message.tag_message); + } + + /* first time pointer */ + if(message.tag_timepointer) + message.tag_timepointer.html_tag.insertBefore(message.tag_message); + + /* the unread */ + if(message.tag_unread) + message.tag_unread.html_tag.insertBefore(message.tag_message); + } + + this.fix_scroll(true); + } + + private _destroy_view_entry(entry: PrivateConversationViewEntry) { + if(!entry.html_tag) + return; + entry.html_tag.remove(); + if('sender' in entry) + this._destroy_message(entry); } private _build_entry_tag() { this._html_entry_tag = $("#tmpl_frame_chat_private_entry").renderTag({ - client_name: this.client_name + client_name: this.client_name, + last_time: tr("error no timestamp"), + avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: this.client_id}, this.client_unique_id) }); - this._html_entry_tag.on('click', event => this.handle.set_selected_conversation(this)); + this._html_entry_tag.on('click', event => { + if(event.isDefaultPrevented()) + return; + + this.handle.set_selected_conversation(this); + }); + this._html_entry_tag.find('.button-close').on('click', event => { + event.preventDefault(); + this.close_conversation(); + }); + this._update_message_timestamp(); + } + + update_avatar() { + const container = this._html_entry_tag.find(".container-avatar"); + container.find(".avatar").remove(); + container.append(this.handle.handle.handle.fileManager.avatars.generate_chat_tag({id: this.client_id}, this.client_unique_id)); + } + + close_conversation() { + this.handle.delete_conversation(this, true); } set_client_name(name: string) { @@ -168,15 +1256,105 @@ namespace chat { } set_unread_flag(flag: boolean, update_chat_counter?: boolean) { - if(flag === this._flag_unread) - return; - this._flag_unread = flag; + /* unread message pointer */ + if(flag != (typeof(this._spacer_unread_message) !== "undefined")) { + if(flag) { + if(this._displayed_messages.length > 0) /* without messages we cant be unread */ + return; + + if(!this._spacer_unread_message) { + this._spacer_unread_message = this._displayed_messages[0]; + this._spacer_unread_message.tag_unread = this._build_spacer(tr("Unread messages"), "new"); + this._spacer_unread_message.tag_unread.html_tag.insertBefore(this._spacer_unread_message.tag_message); + } + } else { + if(this._spacer_unread_message) { + this._destroy_view_entry(this._spacer_unread_message.tag_unread); + this._spacer_unread_message.tag_unread = undefined; + this._spacer_unread_message = undefined; + } + } + } + + /* general notify */ this._html_entry_tag.toggleClass("unread", flag); if(typeof(update_chat_counter) !== "boolean" || update_chat_counter) this.handle.handle.info_frame().update_chat_counter(); } - is_unread() : boolean { return this._flag_unread; } + is_unread() : boolean { return !!this._spacer_unread_message; } + + private _append_state_change(state: "disconnect" | "reconnect" | "closed") { + let message; + if(state == "closed") + message = tr("Your chat partner has closed the conversation"); + else if(state == "reconnect") + message = "Your chat partner has reconnected"; + else + message = "Your chat partner has disconnected"; + + const spacer = this._build_spacer(message, state); + this._register_displayed_message({ + timestamp: Date.now(), + message: spacer, + message_type: "spacer", + tag_message: spacer.html_tag, + tag_timepointer: undefined, + tag_unread: undefined + }); + } + + state() : PrivateConversationState { + return this._state; + } + + set_state(state: PrivateConversationState) { + if(this._state == state) + return; + + if(state == PrivateConversationState.DISCONNECTED) + this._append_state_change("disconnect"); + else if(state == PrivateConversationState.OPEN && this._state != PrivateConversationState.CLOSED) + this._append_state_change("reconnect"); + else if(state == PrivateConversationState.CLOSED) + this._append_state_change("closed"); + + this._state = state; + } + + set_text_callback(callback: (text: string) => any, update_enabled_state?: boolean) { + this._callback_message = callback; + if(typeof (update_enabled_state) !== "boolean" || update_enabled_state) + this.handle.update_chatbox_state(); + } + + chat_enabled() { + return typeof(this._callback_message) !== "undefined" && (this._state == PrivateConversationState.OPEN || this._state == PrivateConversationState.CLOSED); + } + + append_error(message: string, date?: number) { + const spacer = this._build_spacer(message, "error"); + this._register_displayed_message({ + timestamp: date || Date.now(), + message: spacer, + message_type: "spacer", + tag_message: spacer.html_tag, + tag_timepointer: undefined, + tag_unread: undefined + }); + } + + call_message(message: string) { + if(this._callback_message) + this._callback_message(message); + else { + console.warn(tr("Dropping conversation message for client %o because of no message callback."), { + client_name: this.client_name, + client_id: this.client_id, + client_unique_id: this.client_unique_id + }); + } + } } export class PrivateConverations { @@ -192,36 +1370,119 @@ namespace chat { private _conversations: PrivateConveration[] = []; private _current_conversation: PrivateConveration = undefined; + private _select_read_timer: number; constructor(handle: Frame) { this.handle = handle; this._chat_box = new ChatBox(); this._build_html_tag(); + + this.update_chatbox_state(); + this._chat_box.callback_text = message => { + if(!this._current_conversation) { + console.warn(tr("Dropping conversation message because of no active conversation.")); + return; + } + this._current_conversation.call_message(message); + } + } + + clear_client_ids() { + this._conversations.forEach(e => e.client_id = 0); } html_tag() : JQuery { return this._html_tag; } + destroy() { + this._chat_box && this._chat_box.destroy(); + this._chat_box = undefined; + + for(const conversation of this._conversations) + conversation.destroy(); + this._conversations = []; + this._current_conversation = undefined; + + clearTimeout(this._select_read_timer); + + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + + } conversations() : PrivateConveration[] { return this._conversations; } - create_conversation(client_uid: string, client_name: string) : PrivateConveration { - const conv = new PrivateConveration(this, client_uid, client_name); + create_conversation(client_uid: string, client_name: string, client_id: number) : PrivateConveration { + const conv = new PrivateConveration(this, client_uid, client_name, client_id); this._conversations.push(conv); this._html_no_chats.hide(); this._container_conversation_list.append(conv.entry_tag()); + this.handle.info_frame().update_chat_counter(); return conv; } - delete_conversation(conv: PrivateConveration) { + delete_conversation(conv: PrivateConveration, update_chat_couner?: boolean) { if(!this._conversations.remove(conv)) return; //TODO: May animate? - conv.entry_tag().detach(); + conv.destroy(); + conv.clear_messages(false); this._html_no_chats.toggle(this._conversations.length == 0); if(conv === this._current_conversation) this.set_selected_conversation(undefined); + if(update_chat_couner || typeof(update_chat_couner) !== "boolean") + this.handle.info_frame().update_chat_counter(); } + find_conversation(partner: { name: string; unique_id: string; client_id: number }, mode: { create: boolean, attach: boolean }) : PrivateConveration | undefined { + for(const conversation of this.conversations()) + if(conversation.client_id == partner.client_id && (!partner.unique_id || conversation.client_unique_id == partner.unique_id)) { + if(conversation.state() != PrivateConversationState.OPEN) + conversation.set_state(PrivateConversationState.OPEN); + return conversation; + } + + let conv: PrivateConveration; + if(mode.attach) { + for(const conversation of this.conversations()) + if(conversation.client_unique_id == partner.unique_id && conversation.state() != PrivateConversationState.OPEN) { + conversation.set_state(PrivateConversationState.OPEN); + conversation.client_id = partner.client_id; + conversation.set_client_name(partner.name); + + conv = conversation; + break; + } + } + + if(mode.create && !conv) { + conv = this.create_conversation(partner.unique_id, partner.name, partner.client_id); + conv.client_id = partner.client_id; + conv.set_client_name(partner.name); + } + + if(conv) { + conv.set_text_callback(message => { + console.log(tr("Sending text message %s to %o"), message, partner); + this.handle.handle.serverConnection.send_command("sendtextmessage", {"targetmode": 1, "target": partner.client_id, "msg": message}).catch(error => { + if(error instanceof CommandResult) { + if(error.id == ErrorID.CLIENT_INVALID_ID) { + conv.set_state(PrivateConversationState.DISCONNECTED); + conv.set_text_callback(undefined); + } else if(error.id == ErrorID.PERMISSION_ERROR) { + /* may notify for no permissions? */ + } else { + conv.append_error(tr("Failed to send message: ") + (error.extra_message || error.message)); + } + } else { + conv.append_error(tr("Failed to send message. Lookup the console for more details")); + console.error(tr("Failed to send conversation message: %o", error)); + } + }); + }); + } + return conv; + } + clear_conversations() { while(this._conversations.length > 0) - this.delete_conversation(this._conversations[0]); + this.delete_conversation(this._conversations[0], false); this.handle.info_frame().update_chat_counter(); } @@ -229,24 +1490,884 @@ namespace chat { if(conv === this._current_conversation) return; + if(this._select_read_timer) + clearTimeout(this._select_read_timer); + + if(this._current_conversation) + this._current_conversation._html_message_container = undefined; + this._container_conversation_list.find(".selected").removeClass("selected"); this._container_conversation_messages.children().detach(); this._current_conversation = conv; - if(!this._current_conversation) + if(!this._current_conversation) { + this.update_chatbox_state(); return; + } + this._current_conversation._html_message_container = this._container_conversation_messages; + const messages = this._current_conversation.messages_tags(); + /* TODO: Check if the messages are empty and display "No messages" */ + this._container_conversation_messages.append(...messages); + + if(this._current_conversation.is_unread() && false) { + this._select_read_timer = setTimeout(() => { + this._current_conversation.set_unread_flag(false, true); + }, 20 * 1000); /* Lets guess you've read the new messages within 5 seconds */ + } + this._current_conversation.fix_scroll(false); this._current_conversation.entry_tag().addClass("selected"); + this.update_chatbox_state(); + } + + update_chatbox_state() { + this._chat_box.set_enabled(!!this._current_conversation && this._current_conversation.chat_enabled()); } private _build_html_tag() { this._html_tag = $("#tmpl_frame_chat_private").renderTag().dividerfy(); this._container_conversation = this._html_tag.find(".conversation"); + this._container_conversation.on('click', event => { /* lets think if a user clicks within that field that he has read the messages */ + if(this._current_conversation) + this._current_conversation.set_unread_flag(false, true); /* only updates everything if the state changes */ + }); + this._container_conversation_messages = this._container_conversation.find(".messages"); + this._container_conversation_messages.on('scroll', event => { + if(!this._current_conversation) + return; + + const current_view = this._container_conversation_messages[0].scrollTop + this._container_conversation_messages[0].clientHeight + this._container_conversation_messages[0].clientHeight * .125; + if(current_view > this._container_conversation_messages[0].scrollHeight) + this._current_conversation._scroll_position = undefined; + else + this._current_conversation._scroll_position = this._container_conversation_messages[0].scrollTop; + }); this._container_conversation.find(".chatbox").append(this._chat_box.html_tag()); this._container_conversation_list = this._html_tag.find(".conversation-list"); this._html_no_chats = this._container_conversation_list.find(".no-chats"); } + + try_input_focus() { + this._chat_box.focus_input(); + } + + on_show() { + if(this._current_conversation) + this._current_conversation.fix_scroll(false); + } + } + + export namespace channel { + export type ViewEntry = { + html_element: JQuery; + update_timer?: number; + } + export type MessageData = { + timestamp: number; + + message: string; + + sender_name: string; + sender_unique_id: string; + sender_database_id: number; + } + export type Message = MessageData & ViewEntry; + + export class Conversation { + readonly handle: ConversationManager; + readonly channel_id: number; + + private _flag_private: boolean; + + private _html_tag: JQuery; + private _container_messages: JQuery; + private _container_new_message: JQuery; + private _container_no_permissions: JQuery; + private _container_is_private: JQuery; + + private _view_max_messages = 40; /* reset to 40 again as soon we tab out :) */ + private _view_older_messages: ViewEntry; + private _has_older_messages: boolean; /* undefined := not known | else flag */ + + private _view_entries: ViewEntry[] = []; + + private _last_messages: MessageData[] = []; + private _last_messages_timestamp: number = 0; + private _first_unread_message: Message; + private _first_unread_message_pointer: ViewEntry; + + private _scroll_position: number | undefined; /* undefined to follow bottom | position for special stuff */ + + constructor(handle: ConversationManager, channel_id: number) { + this.handle = handle; + this.channel_id = channel_id; + + this._build_html_tag(); + } + + html_tag() : JQuery { return this._html_tag; } + destroy() { + this._first_unread_message_pointer.html_element.detach(); + this._first_unread_message_pointer = undefined; + + this._view_older_messages.html_element.detach(); + this._view_older_messages = undefined; + + for(const view_entry of this._view_entries) { + view_entry.html_element.detach(); + clearTimeout(view_entry.update_timer); + } + this._view_entries = []; + } + + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_channel_messages").renderTag(); + + this._container_new_message = this._html_tag.find(".new-message"); + this._container_no_permissions = this._html_tag.find(".no-permissions").hide(); + this._container_is_private = this._html_tag.find(".private-conversation").hide(); + + this._container_messages = this._html_tag.find(".container-messages"); + this._container_messages.on('scroll', event => { + const exact_position = this._container_messages[0].scrollTop + this._container_messages[0].clientHeight; + const current_view = exact_position + this._container_messages[0].clientHeight * .125; + if(current_view > this._container_messages[0].scrollHeight) { + this._scroll_position = undefined; + } else { + this._scroll_position = this._container_messages[0].scrollTop; + } + this._container_new_message.toggleClass("shown",!!this._first_unread_message && this._first_unread_message_pointer.html_element[0].offsetTop > exact_position); + }); + + this._view_older_messages = this._generate_view_spacer(tr("Load older messages"), "old"); + this._first_unread_message_pointer = this._generate_view_spacer(tr("Unread messages"), "new"); + this._view_older_messages.html_element.appendTo(this._container_messages).on('click', event => { + this.fetch_older_messages(); + }); + + this._container_new_message.on('click', event => { + if(!this._first_unread_message) + return; + this._scroll_position = this._first_unread_message_pointer.html_element[0].offsetTop; + this.fix_scroll(true); + }); + this._container_messages.on('click', event => { + if(this._container_new_message.hasClass('shown')) + return; /* we have clicked, but no chance to see the unread message pointer */ + this._mark_read(); + }); + this.set_flag_private(false); + } + + is_unread() { return !!this._first_unread_message; } + + mark_read() { this._mark_read(); } + private _mark_read() { + if(this._first_unread_message) { + this._first_unread_message = undefined; + + const ctree = this.handle.handle.handle.channelTree; + if(ctree && ctree.tag_tree()) + ctree.tag_tree().find(".marker-text-unread[conversation='" + this.channel_id + "']").addClass("hidden"); + } + this._first_unread_message_pointer.html_element.detach(); + } + + private _generate_view_message(data: MessageData) : Message { + const response = data as Message; + if(response.html_element) + return response; + + const timestamp = new Date(data.timestamp); + let time = format.date.format_chat_time(timestamp); + response.html_element = $("#tmpl_frame_chat_channel_message").renderTag({ + timestamp: time.result, + client_name: htmltags.generate_client_object({ + add_braces: false, + client_name: data.sender_name, + client_unique_id: data.sender_unique_id, + client_id: 0 + }), + message: MessageHelper.bbcode_chat(data.message), + avatar: this.handle.handle.handle.fileManager.avatars.generate_chat_tag({database_id: data.sender_database_id}, data.sender_unique_id) + }); + + response.html_element.find(".button-delete").on('click', () => this.delete_message(data)); + + if(time.next_update > 0) { + const _updater = () => { + time = format.date.format_chat_time(timestamp); + response.html_element.find(".info .timestamp").text(time.result); + if(time.next_update > 0) + response.update_timer = setTimeout(_updater, time.next_update); + else + response.update_timer = 0; + }; + response.update_timer = setTimeout(_updater, time.next_update); + } else { + response.update_timer = 0; + } + + return response; + } + + private _generate_view_spacer(message: string, type: "date" | "new" | "old" | "error") : ViewEntry { + const tag = $("#tmpl_frame_chat_private_spacer").renderTag({ + message: message + }).addClass("type-" + type); + return { + html_element: tag, + update_timer: 0 + } + } + + last_messages_timestamp() : number { + return this._last_messages_timestamp; + } + + fetch_last_messages() { + const fetch_count = this._view_max_messages - this._last_messages.length; + const fetch_timestamp_end = this._last_messages_timestamp + 1; /* we want newer messages then the last message we have */ + + //conversationhistory cid=1 [cpw=xxx] [timestamp_begin] [timestamp_end (0 := no end)] [message_count (default 25| max 100)] [-merge] + this.handle.handle.handle.serverConnection.send_command("conversationhistory", { + cid: this.channel_id, + timestamp_end: fetch_timestamp_end, + message_count: fetch_count + }, {flagset: ["merge"], process_result: false }).catch(error => { + this._view_older_messages.html_element.toggleClass('shown', false); + if(error instanceof CommandResult) { + if(error.id == ErrorID.CONVERSATION_MORE_DATA) { + if(typeof(this._has_older_messages) === "undefined") + this._has_older_messages = true; + this._view_older_messages.html_element.toggleClass('shown', true); + return; + } else if(error.id == ErrorID.PERMISSION_ERROR) { + this._container_no_permissions.show(); + } else if(error.id == ErrorID.CONVERSATION_IS_PRIVATE) { + this.set_flag_private(true); + } + } + //TODO log and handle! + console.error(tr("Failed to fetch conversation history. %o"), error); + }).then(() => { + this.handle.update_chat_box(); + }); + } + + fetch_older_messages() { + this._view_older_messages.html_element.toggleClass('shown', false); + + const entry = this._view_entries.slice().reverse().find(e => 'timestamp' in e) as any as {timestamp: number}; + console.log("Last messages: %o", entry); + //conversationhistory cid=1 [cpw=xxx] [timestamp_begin] [timestamp_end (0 := no end)] [message_count (default 25| max 100)] [-merge] + this.handle.handle.handle.serverConnection.send_command("conversationhistory", { + cid: this.channel_id, + timestamp_begin: entry.timestamp - 1, + message_count: this._view_max_messages + }, {flagset: ["merge"]}).catch(error => { + this._view_older_messages.html_element.toggleClass('shown', false); + if(error instanceof CommandResult) { + if(error.id == ErrorID.CONVERSATION_MORE_DATA) { + this._view_older_messages.html_element.toggleClass('shown', true); + this.handle.update_chat_box(); + return; + } + } + //TODO log and handle! + console.error(tr("Failed to fetch conversation history. %o"), error); + }); + } + + register_new_message(message: MessageData, update_view?: boolean) { + /* lets insert the message at the right index */ + let _new_message = false; + { + let spliced = false; + for(let index = 0; index < this._last_messages.length; index++) { + if(this._last_messages[index].timestamp < message.timestamp) { + this._last_messages.splice(index, 0, message); + spliced = true; + _new_message = index == 0; /* only set flag if this has been inserted at the front */ + break; + } else if(this._last_messages[index].timestamp == message.timestamp && this._last_messages[index].sender_database_id == message.sender_database_id) { + return; /* we already have that message */ + } + } + if(!spliced && this._last_messages.length < this._view_max_messages) { + this._last_messages.push(message); + } + this._last_messages_timestamp = this._last_messages[0].timestamp; + + while(this._last_messages.length > this._view_max_messages) { + if(this._last_messages[this._last_messages.length - 1] == this._first_unread_message) + break; + this._last_messages.pop(); + } + } + + /* message is within view */ + { + const entry = this._generate_view_message(message); + + let previous: ViewEntry; + for(let index = 0; index < this._view_entries.length; index++) { + const current_entry = this._view_entries[index]; + if(!('timestamp' in current_entry)) + continue; + + if((current_entry as Message).timestamp < message.timestamp) { + this._view_entries.splice(index, 0, entry); + previous = current_entry; + break; + } + } + if(!previous) + this._view_entries.push(entry); + + if(previous) + entry.html_element.insertAfter(previous.html_element); + else + entry.html_element.insertAfter(this._view_older_messages.html_element); /* last element is already the current element */ + + if(_new_message && (typeof(this._scroll_position) === "number" || this.handle.current_channel() !== this.channel_id)) { + if(typeof(this._first_unread_message) === "undefined") + this._first_unread_message = entry; + + this._first_unread_message_pointer.html_element.insertBefore(entry.html_element); + this._container_messages.trigger('scroll'); /* updates the new message stuff */ + } + if(update_view) + this.fix_scroll(true); + } + + /* update chat state */ + this._container_no_permissions.hide(); + this.handle.update_chat_box(); + } + + fix_scroll(animate: boolean) { + let offset; + if(this._first_unread_message) { + offset = this._first_unread_message.html_element[0].offsetTop; + } else if(typeof(this._scroll_position) !== "undefined") { + offset = this._scroll_position; + } else { + offset = this._container_messages[0].scrollHeight; + } + + if(animate) { + this._container_messages.stop(true).animate({ + scrollTop: offset + }, 'slow'); + } else { + this._container_messages.stop(true).scrollTop(offset); + } + } + + fix_view_size() { + this._view_older_messages.html_element.toggleClass('shown', !!this._has_older_messages); + + let count = 0; + for(let index = 0; index < this._view_entries.length; index++) { + if('timestamp' in this._view_entries[index]) + count++; + + if(count > this._view_max_messages) { + this._view_entries.splice(index, this._view_entries.length - index).forEach(e => { + clearTimeout(e.update_timer); + e.html_element.remove(); + }); + this._has_older_messages = true; + this._view_older_messages.html_element.toggleClass('shown', true); + break; + } + } + } + + chat_available() : boolean { + return !this._container_no_permissions.is(':visible') && !this._container_is_private.is(':visible'); + } + + text_send_failed(error: CommandResult | any) { + console.warn("Failed to send text message! (%o)", error); + //TODO: Log if message send failed? + if(error instanceof CommandResult) { + if(error.id == ErrorID.PERMISSION_ERROR) { + //TODO: Split up between channel_text_message_send permission and no view permission + if(error.json["failed_permid"] == 0) { + this._container_no_permissions.show(); + this.handle.update_chat_box(); + } + } + } + } + + set_flag_private(flag: boolean) { + if(this._flag_private === flag) + return; + + this._flag_private = flag; + this.update_private_state(); + if(!flag) + this.fetch_last_messages(); + } + + update_private_state() { + if(!this._flag_private) { + this._container_is_private.hide(); + } else { + const client = this.handle.handle.handle.getClient(); + if(client && client.currentChannel() && client.currentChannel().channelId === this.channel_id) + this._container_is_private.hide(); + else + this._container_is_private.show(); + } + } + + delete_message(message: MessageData) { + //TODO A lot of checks! + //conversationmessagedelete cid=2 timestamp_begin= timestamp_end= cldbid= limit=1 + this.handle.handle.handle.serverConnection.send_command('conversationmessagedelete', { + cid: this.channel_id, + cldbid: message.sender_database_id, + + timestamp_begin: message.timestamp - 1, + timestamp_end: message.timestamp + 1, + + limit: 1 + }).then(() => { + return; /* in general it gets deleted via notify */ + }).catch(error => { + console.error(tr("Failed to delete conversation message for conversation %o: %o"), this.channel_id, error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to delete message"), MessageHelper.formatMessage(tr("Failed to delete conversation message{:br:}Error: {}"), error)).open(); + }); + console.log(tr("Deleting message: %o"), message); + } + + delete_messages(begin: number, end: number, sender: number, limit: number) { + let count = 0; + for(const message of this._view_entries.slice()) { + if(!('sender_database_id' in message)) + continue; + + const cmsg = message as Message; + if(end != 0 && cmsg.timestamp > end) + continue; + if(begin != 0 && cmsg.timestamp < begin) + break; + + if(cmsg.sender_database_id !== sender) + continue; + + this._delete_message(message); + if(--count >= limit) + return; + } + + //TODO remove in cache? (_last_messages) + } + + private _delete_message(message: Message) { + if('html_element' in message) { + const cmessage = message as Message; + cmessage.html_element.remove(); + clearTimeout(cmessage.update_timer); + this._view_entries.remove(message as any); + } + + this._last_messages.remove(message); + } + } + + export class ConversationManager { + readonly handle: Frame; + + private _html_tag: JQuery; + private _chat_box: ChatBox; + + private _container_conversation: JQuery; + + private _conversations: Conversation[] = []; + private _current_conversation: Conversation | undefined; + + private _needed_listener = () => this.update_chat_box(); + + constructor(handle: Frame) { + this.handle = handle; + + this._chat_box = new ChatBox(); + this._build_html_tag(); + + this._chat_box.callback_text = text => { + if(!this._current_conversation) + return; + + const conv = this._current_conversation; + this.handle.handle.serverConnection.send_command("sendtextmessage", {targetmode: conv.channel_id == 0 ? 3 : 2, cid: conv.channel_id, msg: text}, {process_result: false}).catch(error => { + conv.text_send_failed(error); + }); + }; + this.update_chat_box(); + } + + initialize_needed_listener() { + this.handle.handle.permissions.register_needed_permission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND, this._needed_listener); + this.handle.handle.permissions.register_needed_permission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND, this._needed_listener); + } + + html_tag() : JQuery { return this._html_tag; } + destroy() { + if(this.handle.handle.permissions) + this.handle.handle.permissions.unregister_needed_permission(PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND, this._needed_listener); + this.handle.handle.permissions.unregister_needed_permission(PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND, this._needed_listener); + this._needed_listener = undefined; + + this._chat_box && this._chat_box.destroy(); + this._chat_box = undefined; + + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._container_conversation = undefined; + + for(const conversation of this._conversations) + conversation.destroy(); + this._conversations = []; + this._current_conversation = undefined; + } + + update_chat_box() { + let flag = true; + flag = flag && !!this._current_conversation; /* test if we have a conversation */ + flag = flag && !!this.handle.handle.permissions; /* test if we got permissions to test with */ + flag = flag && this.handle.handle.permissions.neededPermission(this._current_conversation.channel_id == 0 ? PermissionType.B_CLIENT_SERVER_TEXTMESSAGE_SEND : PermissionType.B_CLIENT_CHANNEL_TEXTMESSAGE_SEND).granted(1); + flag = flag && this._current_conversation.chat_available(); + this._chat_box.set_enabled(flag); + } + + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_channel").renderTag({ + chatbox: this._chat_box.html_tag() + }); + this._container_conversation = this._html_tag.find(".container-chat"); + this._chat_box.html_tag().on('focus', event => { + if(this._current_conversation) + this._current_conversation.mark_read(); + }); + } + + set_current_channel(channel_id: number, update_info_frame?: boolean) { + if(this._current_conversation && this._current_conversation.channel_id === channel_id) + return; + + let conversation = this.conversation(channel_id); + this._current_conversation = conversation; + + if(this._current_conversation) { + this._container_conversation.children().detach(); + this._container_conversation.append(conversation.html_tag()); + this._current_conversation.fix_view_size(); + this._current_conversation.fix_scroll(false); + this.update_chat_box(); + } + if(typeof(update_info_frame) === "undefined" || update_info_frame) + this.handle.info_frame().update_channel_text(); + } + + current_channel() : number { return this._current_conversation ? this._current_conversation.channel_id : 0; } + + /* Used by notifychanneldeleted */ + delete_conversation(channel_id: number) { + const entry = this._conversations.find(e => e.channel_id === channel_id); + if(!entry) + return; + + this._conversations.remove(entry); + entry.html_tag().detach(); + entry.destroy(); + } + + reset() { + while(this._conversations.length > 0) + this.delete_conversation(this._conversations[0].channel_id); + } + + conversation(channel_id: number, create?: boolean) : Conversation { + let conversation = this._conversations.find(e => e.channel_id === channel_id); + + if(!conversation && channel_id >= 0 && (typeof (create) === "undefined" || create)) { + conversation = new Conversation(this, channel_id); + this._conversations.push(conversation); + conversation.fetch_last_messages(); + } + return conversation; + } + + on_show() { + if(this._current_conversation) + this._current_conversation.fix_scroll(false); + } + } + } + + export class ClientInfo { + readonly handle: Frame; + private _html_tag: JQuery; + private _current_client: ClientEntry | undefined; + private _online_time_updater: number; + previous_frame_content: FrameContent; + + constructor(handle: Frame) { + this.handle = handle; + this._build_html_tag(); + } + + html_tag() : JQuery { + return this._html_tag; + } + + destroy() { + clearInterval(this._online_time_updater); + + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + + this._current_client = undefined; + this.previous_frame_content = undefined; + } + + private _build_html_tag() { + this._html_tag = $("#tmpl_frame_chat_client_info").renderTag(); + this._html_tag.find(".button-close").on('click', () => { + if(this.previous_frame_content === FrameContent.CLIENT_INFO) + this.previous_frame_content = FrameContent.NONE; + + this.handle.set_content(this.previous_frame_content); + }); + this._html_tag.find('.container-avatar-edit').on('click', () => this.handle.handle.update_avatar()); + } + + current_client() : ClientEntry { + return this._current_client; + } + + set_current_client(client: ClientEntry | undefined, enforce?: boolean) { + if(client) client.updateClientVariables(); /* just to ensure */ + if(client === this._current_client && (typeof(enforce) === "undefined" || !enforce)) + return; + + this._current_client = client; + + /* updating the header */ + { + const client_name = this._html_tag.find(".client-name"); + client_name.children().remove(); + htmltags.generate_client_object({ + add_braces: false, + client_name: client ? client.clientNickName() : "undefined", + client_unique_id: client ? client.clientUid() : "", + client_id: client ? client.clientId() : 0 + }).appendTo(client_name); + + const client_description = this._html_tag.find(".client-description"); + client_description.text(client ? client.properties.client_description : "").toggle(!!client.properties.client_description); + + const container_avatar = this._html_tag.find(".container-avatar"); + container_avatar.find(".avatar").remove(); + if(client) + this.handle.handle.fileManager.avatars.generate_chat_tag({id: client.clientId()}, client.clientUid()).appendTo(container_avatar); + else + this.handle.handle.fileManager.avatars.generate_chat_tag(undefined, undefined).appendTo(container_avatar); + + const container_avatar_edit = this._html_tag.find(".container-avatar-edit"); + container_avatar_edit.toggle(client instanceof LocalClientEntry); + } + /* updating the info fields */ + { + const online_time = this._html_tag.find(".client-online-time"); + online_time.text(format.time.format_online_time(client ? client.calculateOnlineTime() : 0)); + if(this._online_time_updater) { + clearInterval(this._online_time_updater); + this._online_time_updater = 0; + } + if(client) { + this._online_time_updater = setInterval(() => { + const client = this._current_client; + if(!client) { + clearInterval(this._online_time_updater); + this._online_time_updater = undefined; + return; + } + online_time.text(format.time.format_online_time(client.calculateOnlineTime())); + }, 1000); + } + + const country = this._html_tag.find(".client-country"); + country.children().detach(); + const country_code = (client ? client.properties.client_country : undefined) || "xx"; + $.spawn("div").addClass("country flag-" + country_code.toLowerCase()).appendTo(country); + $.spawn("a").text(i18n.country_name(country_code.toUpperCase())).appendTo(country); + + + const version = this._html_tag.find(".client-version"); + version.children().detach(); + if(client) { + $.spawn("a").attr("title", client.properties.client_version).text( + client.properties.client_version.split(" ")[0] + " on " + client.properties.client_platform + ).appendTo(version); + } + + const volume = this._html_tag.find(".client-local-volume"); + volume.text((client && client.get_audio_handle() ? (client.get_audio_handle().get_volume() * 100) : -1) + "%"); + } + + /* teaspeak forum */ + { + const container_forum = this._html_tag.find(".container-teaforo"); + if(client && client.properties.client_teaforo_id) { + container_forum.show(); + + const container_data = container_forum.find(".client-teaforo-account"); + container_data.children().remove(); + + let text = client.properties.client_teaforo_name; + if((client.properties.client_teaforo_flags & 0x01) > 0) + text += " (" + tr("Banned") + ")"; + if((client.properties.client_teaforo_flags & 0x02) > 0) + text += " (" + tr("Stuff") + ")"; + if((client.properties.client_teaforo_flags & 0x04) > 0) + text += " (" + tr("Premium") + ")"; + + $.spawn("a") + .attr("href", "https://forum.teaspeak.de/index.php?members/" + client.properties.client_teaforo_id) + .attr("target", "_blank") + .text(text) + .appendTo(container_data); + } else { + container_forum.hide(); + } + } + + /* update the client status */ + { + //TODO Implement client status! + const container_status = this._html_tag.find(".container-client-status"); + const container_status_entries = container_status.find(".client-status"); + container_status_entries.children().detach(); + if(client) { + if(client.properties.client_away) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-away"), + $.spawn("a").text(tr("Away")), + client.properties.client_away_message ? + $.spawn("a").addClass("away-message").text("(" + client.properties.client_away_message + ")") : + undefined + ) + ) + } + if(client.is_muted()) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-input_muted_local"), + $.spawn("a").text(tr("Client local muted")) + ) + ) + } + if(!client.properties.client_output_hardware) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-hardware_output_muted"), + $.spawn("a").text(tr("Speakers/Headphones disabled")) + ) + ) + } + if(!client.properties.client_input_hardware) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-hardware_input_muted"), + $.spawn("a").text(tr("Microphone disabled")) + ) + ) + } + if(client.properties.client_output_muted) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-output_muted"), + $.spawn("a").text(tr("Speakers/Headphones Muted")) + ) + ) + } + if(client.properties.client_input_muted) { + container_status_entries.append( + $.spawn("div").addClass("status-entry").append( + $.spawn("div").addClass("icon_em client-input_muted"), + $.spawn("a").text(tr("Microphone Muted")) + ) + ) + } + } + container_status.toggle(container_status_entries.children().length > 0); + } + /* update client server groups */ + { + const container_groups = this._html_tag.find(".client-group-server"); + container_groups.children().detach(); + if(client) { + const invalid_groups = []; + const groups = client.assignedServerGroupIds().map(group_id => { + const result = this.handle.handle.groups.serverGroup(group_id); + if(!result) + invalid_groups.push(group_id); + return result; + }).filter(e => !!e).sort(GroupManager.sorter()); + for(const invalid_id of invalid_groups) { + container_groups.append($.spawn("a").text("{" + tr("server group ") + invalid_groups + "}").attr("title", tr("Missing server group id!") + " (" + invalid_groups + ")")); + } + for(let group of groups) { + container_groups.append( + $.spawn("div").addClass("group-container") + .append( + this.handle.handle.fileManager.icons.generateTag(group.properties.iconid) + ).append( + $.spawn("a").text(group.name).attr("title", tr("Group id: ") + group.id) + ) + ); + } + } + } + /* update client channel group */ + { + const container_group = this._html_tag.find(".client-group-channel"); + container_group.children().detach(); + if(client) { + const group_id = client.assignedChannelGroup(); + let group = this.handle.handle.groups.channelGroup(group_id); + if(group) { + container_group.append( + $.spawn("div").addClass("group-container") + .append( + this.handle.handle.fileManager.icons.generateTag(group.properties.iconid) + ).append( + $.spawn("a").text(group.name).attr("title", tr("Group id: ") + group_id) + ) + ); + } else { + container_group.append($.spawn("a").text(tr("Invalid channel group!")).attr("title", tr("Missing channel group id!") + " (" + group_id + ")")); + } + } + } + } + } + + export enum FrameContent { + NONE, + PRIVATE_CHAT, + CHANNEL_CHAT, + CLIENT_INFO } export class Frame { @@ -255,26 +2376,56 @@ namespace chat { private _html_tag: JQuery; private _container_info: JQuery; private _container_chat: JQuery; + private _content_type: FrameContent; private _conversations: PrivateConverations; + private _client_info: ClientInfo; + private _channel_conversations: channel.ConversationManager; constructor(handle: ConnectionHandler) { this.handle = handle; + this._content_type = FrameContent.NONE; this._info_frame = new InfoFrame(this); this._conversations = new PrivateConverations(this); + this._channel_conversations = new channel.ConversationManager(this); + this._client_info = new ClientInfo(this); this._build_html_tag(); - this.show_private_conversations(); + this.show_channel_conversations(); + this.info_frame().update_chat_counter(); } html_tag() : JQuery { return this._html_tag; } info_frame() : InfoFrame { return this._info_frame; } + destroy() { + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + + this._info_frame && this._info_frame.destroy(); + this._info_frame = undefined; + + this._conversations && this._conversations.destroy(); + this._conversations = undefined; + + this._client_info && this._client_info.destroy(); + this._client_info = undefined; + + this._channel_conversations && this._channel_conversations.destroy(); + this._channel_conversations = undefined; + + this._container_info && this._container_info.remove(); + this._container_info = undefined; + + this._container_chat && this._container_chat.remove(); + this._container_chat = undefined; + } + private _build_html_tag() { this._html_tag = $("#tmpl_frame_chat").renderTag(); - this._container_info =this._html_tag.find(".container-info"); - this._container_chat =this._html_tag.find(".container-chat"); + this._container_info = this._html_tag.find(".container-info"); + this._container_chat = this._html_tag.find(".container-chat"); this._info_frame.html_tag().appendTo(this._container_info); } @@ -284,9 +2435,66 @@ namespace chat { return this._conversations; } - show_private_conversations() { + channel_conversations() : channel.ConversationManager { + return this._channel_conversations; + } + + client_info() : ClientInfo { + return this._client_info; + } + + private _clear() { + this._content_type = FrameContent.NONE; this._container_chat.children().detach(); + } + + show_private_conversations() { + if(this._content_type === FrameContent.PRIVATE_CHAT) + return; + this._clear(); + this._content_type = FrameContent.PRIVATE_CHAT; this._container_chat.append(this._conversations.html_tag()); + this._conversations.on_show(); + this._info_frame.set_mode(InfoFrameMode.PRIVATE_CHAT); + } + + show_channel_conversations() { + if(this._content_type === FrameContent.CHANNEL_CHAT) + return; + + this._clear(); + this._content_type = FrameContent.CHANNEL_CHAT; + this._container_chat.append(this._channel_conversations.html_tag()); + this._channel_conversations.on_show(); + this._info_frame.set_mode(InfoFrameMode.CHANNEL_CHAT); + } + + show_client_info(client: ClientEntry) { + this._client_info.set_current_client(client); + + if(this._content_type === FrameContent.CLIENT_INFO) + return; + + this._client_info.previous_frame_content = this._content_type; + this._clear(); + this._content_type = FrameContent.CLIENT_INFO; + this._container_chat.append(this._client_info.html_tag()); + this._info_frame.set_mode(InfoFrameMode.CLIENT_INFO); + } + + set_content(type: FrameContent) { + if(this._content_type === type) + return; + + if(type === FrameContent.CHANNEL_CHAT) + this.show_channel_conversations(); + else if(type === FrameContent.PRIVATE_CHAT) + this.show_private_conversations(); + else { + this._clear(); + this._content_type = FrameContent.NONE; + this._info_frame.set_mode(InfoFrameMode.NONE); + } } } } \ No newline at end of file diff --git a/shared/js/ui/frames/connection_handlers.ts b/shared/js/ui/frames/connection_handlers.ts index fdfa6d66..cab2c10b 100644 --- a/shared/js/ui/frames/connection_handlers.ts +++ b/shared/js/ui/frames/connection_handlers.ts @@ -7,6 +7,7 @@ class ServerConnectionManager { private _container_log_server: JQuery; private _container_channel_tree: JQuery; + private _container_hostbanner: JQuery; private _container_select_info: JQuery; private _container_chat: JQuery; @@ -32,6 +33,7 @@ class ServerConnectionManager { this._container_log_server = $("#server-log"); this._container_channel_tree = $("#channelTree"); + this._container_hostbanner = $("#hostbanner"); this._container_select_info = $("#select_info"); this._container_chat = $("#chat"); @@ -52,7 +54,7 @@ class ServerConnectionManager { destroy_server_connection_handler(handler: ConnectionHandler) { this.connection_handlers.remove(handler); - handler.tag_connection_handler.detach(); + handler.tag_connection_handler.remove(); this._update_scroll(); this._tag.toggleClass("shown", this.connection_handlers.length > 1); @@ -64,11 +66,14 @@ class ServerConnectionManager { if(handler === this.active_handler) this.set_active_connection_handler(this.connection_handlers[0]); + + /* destroy all elements */ + handler.destroy(); } set_active_connection_handler(handler: ConnectionHandler) { if(handler && this.connection_handlers.indexOf(handler) == -1) - throw "Handler hasn't been registrated or is already obsolete!"; + throw "Handler hasn't been registered or is already obsolete!"; if(this.active_handler) this.active_handler.select_info.close_popover(); @@ -77,19 +82,22 @@ class ServerConnectionManager { this._container_select_info.children().detach(); this._container_chat.children().detach(); this._container_log_server.children().detach(); + this._container_hostbanner.children().detach(); control_bar.set_connection_handler(handler); if(handler) { handler.tag_connection_handler.addClass("active"); + this._container_hostbanner.append(handler.hostbanner.html_tag); this._container_channel_tree.append(handler.channelTree.tag_tree()); this._container_select_info.append(handler.select_info.get_tag()); - this._container_chat.append(handler.chat_frame.html_tag()); + this._container_chat.append(handler.side_bar.html_tag()); this._container_log_server.append(handler.log.html_tag()); if(handler.invoke_resized_on_activate) handler.resize_elements(); } + top_menu.update_state(); this.active_handler = handler; } diff --git a/shared/js/ui/frames/hostbanner.ts b/shared/js/ui/frames/hostbanner.ts new file mode 100644 index 00000000..08f5fd84 --- /dev/null +++ b/shared/js/ui/frames/hostbanner.ts @@ -0,0 +1,112 @@ +class Hostbanner { + readonly html_tag: JQuery; + readonly client: ConnectionHandler; + + private _destryed = false; + private updater: NodeJS.Timer; + + constructor(client: ConnectionHandler) { + this.client = client; + this.html_tag = $.spawn("div").addClass("container-hostbanner"); + this.html_tag.on('click', event => { + const server = this.client.channelTree.server; + if(!server || !server.properties.virtualserver_hostbanner_url) + return; + window.open(server.properties.virtualserver_hostbanner_url, '_blank'); + }); + + this.update(); + } + + destroy() { + if(this.updater) { + clearTimeout(this.updater); + this.updater = undefined; + } + if(this.html_tag) { + this.html_tag.remove(); + } + this._destryed = true; + } + + update() { + if(this._destryed) return; + + if(this.updater) { + clearTimeout(this.updater); + this.updater = undefined; + } + + this.html_tag.toggleClass("no-background", !settings.static_global(Settings.KEY_HOSTBANNER_BACKGROUND)); + + const tag = this.generate_tag(); + tag.then(element => { + console.log("Regenrated result: %o", element); + if(!element) { + this.html_tag.empty().addClass("disabled"); + return; + } + const children = this.html_tag.children(); + this.html_tag.append(element).removeClass("disabled"); + + /* allow the new image be loaded from cache URL */ + { + children + .css('z-index', '2') + .css('position', 'absolute') + .css('height', '100%') + .css('width', '100%'); + setTimeout(() => { + children.detach(); + }, 250); + } + }).catch(error => { + console.warn(tr("Failed to load hostbanner: %o"), error); + this.html_tag.empty().addClass("disabled"); + }); + const server = this.client.channelTree.server; + this.html_tag.attr('title', server ? server.properties.virtualserver_hostbanner_url : undefined); + } + + private async generate_tag?() : Promise { + if(!this.client.connected) + return undefined; + + const server = this.client.channelTree.server; + if(!server) return undefined; + if(!server.properties.virtualserver_hostbanner_gfx_url) return undefined; + + let banner_url = server.properties.virtualserver_hostbanner_gfx_url; + if(server.properties.virtualserver_hostbanner_gfx_interval > 0) { + const update_interval = Math.max(server.properties.virtualserver_hostbanner_gfx_interval, 60); + const update_timestamp = (Math.floor((Date.now() / 1000) / update_interval) * update_interval).toString(); + try { + const url = new URL(server.properties.virtualserver_hostbanner_gfx_url); + if(url.search.length == 0) + banner_url += "?_ts=" + update_timestamp; + else + banner_url += "&_ts=" + update_timestamp; + } catch(error) { + console.warn(tr("Failed to parse banner URL: %o. Using default '&' append."), error); + banner_url += "&_ts=" + update_timestamp; + } + + this.updater = setTimeout(() => this.update(), update_interval * 1000); + } + + /* first now load the image */ + const image_element = document.createElement("img"); + await new Promise((resolve, reject) => { + image_element.onload = resolve; + image_element.onerror = reject; + image_element.src = banner_url; + image_element.style.display = 'none'; + document.body.append(image_element); + console.log("Loading image!"); + }); + + image_element.parentNode.removeChild(image_element); + image_element.style.display = 'unset'; + return $.spawn("div").addClass("hostbanner-image-container hostbanner-mode-" + server.properties.virtualserver_hostbanner_mode).append($(image_element)); + } +} diff --git a/shared/js/ui/frames/server_log.ts b/shared/js/ui/frames/server_log.ts index 07036465..efe8e4e8 100644 --- a/shared/js/ui/frames/server_log.ts +++ b/shared/js/ui/frames/server_log.ts @@ -10,9 +10,18 @@ namespace log { CONNECTION_FAILED = "connection_failed", CONNECTION_VOICE_SETUP_FAILED = "connection_voice_setup_failed", + CONNECTION_COMMAND_ERROR = "connection_command_error", GLOBAL_MESSAGE = "global_message", + SERVER_WELCOME_MESSAGE = "server_welcome_message", + SERVER_HOST_MESSAGE = "server_host_message", + SERVER_HOST_MESSAGE_DISCONNECT = "server_host_message_disconnect", + + SERVER_CLOSED = "server_closed", + SERVER_BANNED = "server_banned", + SERVER_REQUIRES_PASSWORD = "server_requires_password", + CLIENT_VIEW_ENTER = "client_view_enter", CLIENT_VIEW_LEAVE = "client_view_leave", CLIENT_VIEW_MOVE = "client_view_move", @@ -79,6 +88,14 @@ namespace log { permission: PermissionInfo; } + export type WelcomeMessage = { + message: string; + } + + export type HostMessageDisconnect = { + message: string; + } + //tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}") //tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}") //tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}") @@ -158,14 +175,35 @@ namespace log { reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */ } + export type ConnectionCommandError = { + error: any; + } + export type ClientNicknameChanged = { - own_action: boolean; + own_client: boolean; + client: base.Client; + + old_name: string; + new_name: string; } export type ClientNicknameChangeFailed = { reason: string; } + + export type ServerClosed = { + message: string; + } + + export type ServerRequiresPassword = {} + + export type ServerBanned = { + message: string; + time: number; + + invoker: base.Client; + } } export type LogMessage = { @@ -188,11 +226,20 @@ namespace log { "connection_login": event.ConnectionLogin; "connection_connected": event.ConnectionConnected; "connection_voice_setup_failed": event.ConnectionVoiceSetupFailed; + "connection_command_error": event.ConnectionCommandError; "reconnect_scheduled": event.ReconnectScheduled; "reconnect_canceled": event.ReconnectCanceled; "reconnect_execute": event.ReconnectExecute; + "server_welcome_message": event.WelcomeMessage; + "server_host_message": event.WelcomeMessage; + "server_host_message_disconnect": event.HostMessageDisconnect; + + "server_closed": event.ServerClosed; + "server_requires_password": event.ServerRequiresPassword; + "server_banned": event.ServerBanned; + "client_view_enter": event.ClientEnter; "client_view_move": event.ClientMove; "client_view_leave": event.ClientLeave; @@ -208,9 +255,6 @@ namespace log { type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined; export const MessageBuilders: {[key: string]: MessageBuilder} = { - "global_message": (data: event.GlobalMessage, options) => { - return []; - }, "error_custom": (data: event.ErrorCustom, options) => { return [$.spawn("div").addClass("log-error").text(data.message)] } @@ -242,7 +286,7 @@ namespace log { } this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight; - }) + }); } log(type: T, data: server.TypeInfo[T]) { @@ -263,6 +307,14 @@ namespace log { return this._html_tag; } + destroy() { + this._html_tag && this._html_tag.remove(); + this._html_tag = undefined; + this._log_container = undefined; + + this._log = undefined; + } + private append_log(message: server.LogMessage) { let container = $.spawn("div").addClass("log-message"); @@ -283,7 +335,7 @@ namespace log { MessageHelper.formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container)); } else { const elements = builder(message.data, {}); - if(!elements) + if(!elements || elements.length == 0) return; /* discard message */ container.append(...elements); } @@ -297,7 +349,7 @@ namespace log { while(messages.length - index > this.history_length) index++; const hide_elements = messages.filter(idx => idx < index); - hide_elements.hide(250, () => hide_elements.detach()); + hide_elements.hide(250, () => hide_elements.remove()); if(this.auto_follow) this._html_tag.scrollTop(this._html_tag[0].scrollHeight); @@ -339,7 +391,7 @@ namespace log { }; MessageBuilders["error_permission"] = (data: event.ErrorPermission, options) => { - return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission.name).map(e => e.addClass("log-error")); + return MessageHelper.formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error")); }; MessageBuilders["client_view_enter"] = (data: event.ClientEnter, options) => { @@ -442,6 +494,26 @@ namespace log { return MessageHelper.formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); } return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.message + ")")]; + }; + + MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage, options) => { + return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]"); + }; + + MessageBuilders["server_host_message"] = (data: event.WelcomeMessage, options) => { + return MessageHelper.bbcode_chat("[color=green]" + data.message + "[/color]"); + }; + + MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged, options) => { + if(data.own_client) { + return MessageHelper.formatMessage(tr("Nickname successfully changed.")); + } else { + return MessageHelper.formatMessage(tr("{0} changed his nickname from \"{1}\" to \"{2}\""), client_tag(data.client), data.old_name, data.new_name); + } + }; + + MessageBuilders["global_message"] = (data: event.GlobalMessage, options) => { + return []; /* we do not show global messages within log */ } } } diff --git a/shared/js/ui/htmltags.ts b/shared/js/ui/htmltags.ts index 17a78490..59e23d3b 100644 --- a/shared/js/ui/htmltags.ts +++ b/shared/js/ui/htmltags.ts @@ -33,11 +33,21 @@ namespace htmltags { if(properties.client_id) result = result + "client-id='" + properties.client_id + "' "; - if(properties.client_unique_id && properties.client_unique_id != "unknown") - result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' "; + if(properties.client_unique_id && properties.client_unique_id != "unknown") { + try { + result = result + "client-unique-id='" + encodeURIComponent(properties.client_unique_id) + "' "; + } catch(error) { + console.warn(tr("Failed to generate client tag attribute 'client-unique-id': %o"), error); + } + } - if(properties.client_name) - result = result + "client-name='" + encodeURIComponent(properties.client_name) + "' "; + if(properties.client_name) { + try { + result = result + "client-name='" + encodeURIComponent(properties.client_name) + "' "; + } catch(error) { + console.warn(tr("Failed to generate client tag attribute 'client-name': %o"), error); + } + } /* add the click handler */ result += "oncontextmenu='return htmltags.callbacks.callback_context_client($(this));'"; diff --git a/shared/js/ui/modal/ModalAbout.ts b/shared/js/ui/modal/ModalAbout.ts new file mode 100644 index 00000000..09063052 --- /dev/null +++ b/shared/js/ui/modal/ModalAbout.ts @@ -0,0 +1,46 @@ +/// +/// +/// + +namespace Modals { + function format_date(date: number) { + const d = new Date(date); + + return ('00' + d.getDay()).substr(-2) + "." + ('00' + d.getMonth()).substr(-2) + "." + d.getFullYear() + " - " + ('00' + d.getHours()).substr(-2) + ":" + ('00' + d.getMinutes()).substr(-2); + } + + export function spawnAbout() { + const app_version = (() => { + const version_node = document.getElementById("app_version"); + if(!version_node) return undefined; + + const version = version_node.hasAttribute("value") ? version_node.getAttribute("value") : undefined; + if(!version) return undefined; + + if(version == "unknown" || version.replace(/0+/, "").length == 0) + return undefined; + + return version; + })(); + + const connectModal = createModal({ + header: tr("About"), + body: () => { + let tag = $("#tmpl_about").renderTag({ + client: false, + + version_client: app_version || "in-dev", + version_ui: app_version || "in-dev", + + version_timestamp: !!app_version ? format_date(Date.now()) : "--" + }); + return tag; + }, + footer: null, + + width: 600 + }); + connectModal.htmlTag.find(".modal-body").addClass("modal-about"); + connectModal.open(); + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/ModalAvatar.ts b/shared/js/ui/modal/ModalAvatar.ts new file mode 100644 index 00000000..c5fcac24 --- /dev/null +++ b/shared/js/ui/modal/ModalAvatar.ts @@ -0,0 +1,74 @@ +/// +/// +/// + +namespace Modals { + //TODO: Test if we could render this image and not only the browser by knowing the type. + export function spawnAvatarUpload(callback_data: (data: ArrayBuffer | undefined | null) => any) { + const modal = createModal({ + header: tr("Avatar Upload"), + footer: undefined, + body: () => { + return $("#tmpl_avatar_upload").renderTag({}); + } + }); + + let _data_submitted = false; + let _current_avatar; + + modal.htmlTag.find(".button-select").on('click', event => { + modal.htmlTag.find(".file-inputs").trigger('click'); + }); + + modal.htmlTag.find(".button-delete").on('click', () => { + if(_data_submitted) + return; + _data_submitted = true; + modal.close(); + callback_data(null); + }); + + modal.htmlTag.find(".button-cancel").on('click', () => modal.close()); + const button_upload = modal.htmlTag.find(".button-upload"); + button_upload.on('click', event => (!_data_submitted) && (_data_submitted = true, modal.close(), true) && callback_data(_current_avatar)); + + const set_avatar = (data: string | undefined, type?: string) => { + _current_avatar = data ? arrayBufferBase64(data) : undefined; + button_upload.prop("disabled", !_current_avatar); + modal.htmlTag.find(".preview img").attr("src", data ? ("data:image/" + type + ";base64," + data) : "img/style/avatar.png"); + }; + + const input_node = modal.htmlTag.find(".file-inputs")[0] as HTMLInputElement; + input_node.multiple = false; + + modal.htmlTag.find(".file-inputs").on('change', event => { + console.log("Files: %o", input_node.files); + + const read_file = (file: File) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + + reader.readAsDataURL(file); + }); + + (async () => { + const data = await read_file(input_node.files[0]); + + if(!data.startsWith("data:image/")) { + console.error(tr("Failed to load file %s: Invalid data media type (%o)"), input_node.files[0].name, data); + createErrorModal(tr("Icon upload failed"), tra("Failed to select avatar {}.
File is not an image", input_node.files[0].name)).open(); + return; + } + const semi = data.indexOf(';'); + const type = data.substring(11, semi); + console.log(tr("Given image has type %s"), type); + + set_avatar(data.substr(semi + 8 /* 8 bytes := base64, */), type); + })(); + }); + set_avatar(undefined); + modal.close_listener.push(() => !_data_submitted && callback_data(undefined)); + modal.open(); + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/ModalAvatarList.ts b/shared/js/ui/modal/ModalAvatarList.ts index e30df518..021430f2 100644 --- a/shared/js/ui/modal/ModalAvatarList.ts +++ b/shared/js/ui/modal/ModalAvatarList.ts @@ -10,7 +10,7 @@ namespace Modals { const lower_nibble = id.charCodeAt(index + 1) - 97; buffer[index / 2] = (upper_nibble << 4) | lower_nibble; } - return base64ArrayBuffer(buffer); + return base64_encode_ab(buffer); }; export const human_file_size = (size: number) => { @@ -80,7 +80,7 @@ namespace Modals { .css("display", "none") .appendTo($("body")); element[0].click(); - element.detach(); + element.remove(); }; } })); diff --git a/shared/js/ui/modal/ModalBanList.ts b/shared/js/ui/modal/ModalBanList.ts index 53b9d627..39c9b5ed 100644 --- a/shared/js/ui/modal/ModalBanList.ts +++ b/shared/js/ui/modal/ModalBanList.ts @@ -216,7 +216,7 @@ namespace Modals { }; result.clear = () => { entries = []; - modal.htmlTag.find(".entry-container .entries").children().detach(); + modal.htmlTag.find(".entry-container .entries").children().remove(); update_function(); }; result.modal = modal; diff --git a/shared/js/ui/modal/ModalBookmarks.ts b/shared/js/ui/modal/ModalBookmarks.ts index 97162f92..8918145f 100644 --- a/shared/js/ui/modal/ModalBookmarks.ts +++ b/shared/js/ui/modal/ModalBookmarks.ts @@ -252,7 +252,10 @@ namespace Modals { width: 750 }); - modal.close_listener.push(() => control_bar.update_bookmarks()); + modal.close_listener.push(() => { + control_bar.update_bookmarks(); + top_menu.rebuild_bookmarks(); + }); modal.open(); } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalConnect.ts b/shared/js/ui/modal/ModalConnect.ts index 11b118b1..6ca9ac0f 100644 --- a/shared/js/ui/modal/ModalConnect.ts +++ b/shared/js/ui/modal/ModalConnect.ts @@ -1,7 +1,100 @@ /// +//FIXME: Move this shit out of this file! +namespace connection_log { + //TODO: Save password data + export type ConnectionData = { + name: string; + icon_id: number; + country: string; + clients_online: number; + clients_total: number; + + flag_password: boolean; + password_hash: string; + } + + export type ConnectionEntry = ConnectionData & { + address: { hostname: string; port: number }, + total_connection: number; + + first_timestamp: number; + last_timestamp: number; + } + + let _history: ConnectionEntry[] = []; + export function log_connect(address: { hostname: string; port: number }) { + let entry = _history.find(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port); + if(!entry) { + _history.push(entry = { + last_timestamp: Date.now(), + first_timestamp: Date.now(), + address: address, + clients_online: 0, + clients_total: 0, + country: 'unknown', + name: 'Unknown', + icon_id: 0, + total_connection: 0, + + flag_password: false, + password_hash: undefined + }); + } + entry.last_timestamp = Date.now(); + entry.total_connection++; + _save(); + } + + export function update_address_info(address: { hostname: string; port: number }, data: ConnectionData) { + _history.filter(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port).forEach(e => { + for(const key of Object.keys(data)) { + if(typeof(data[key]) !== "undefined") { + e[key] = data[key]; + } + } + }); + _save(); + } + + export function update_address_password(address: { hostname: string; port: number }, password_hash: string) { + _history.filter(e => e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port).forEach(e => { + e.password_hash = password_hash; + }); + _save(); + } + + function _save() { + settings.changeGlobal(Settings.KEY_CONNECT_HISTORY, JSON.stringify(_history)); + } + + export function history() : ConnectionEntry[] { + return _history.sort((a, b) => b.last_timestamp - a.last_timestamp); + } + + export function delete_entry(address: { hostname: string; port: number }) { + _history = _history.filter(e => !(e.address.hostname.toLowerCase() == address.hostname.toLowerCase() && e.address.port == address.port)); + _save(); + } + + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + name: 'connection history load', + priority: 1, + function: async () => { + _history = []; + try { + _history = JSON.parse(settings.global(Settings.KEY_CONNECT_HISTORY, "[]")); + } catch(error) { + log.warn(LogCategory.CLIENT, tr("Failed to load connection history: {}"), error); + } + } + }); +} + namespace Modals { - export function spawnConnectModal(defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) { + export function spawnConnectModal(options: { + default_connect_new_tab?: boolean /* default false */ + }, defaultHost: { url: string, enforce: boolean} = { url: "ts.TeaSpeak.de", enforce: false}, connect_profile?: { profile: profiles.ConnectionProfile, enforce: boolean}) { let selected_profile: profiles.ConnectionProfile; const random_id = (() => { @@ -10,12 +103,41 @@ namespace Modals { return array.join(""); })(); - const connect_modal = $("#tmpl_connect").renderTag({ - client: native_client, - forum_path: settings.static("forum_path"), - password_id: random_id, - multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false) - }).modalize((header, body, footer) => { + const modal = createModal({ + header: tr("Connect to a server"), + body: $("#tmpl_connect").renderTag({ + client: native_client, + forum_path: settings.static("forum_path"), + password_id: random_id, + multi_tab: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION), + default_connect_new_tab: typeof(options.default_connect_new_tab) === "boolean" && options.default_connect_new_tab + }), + footer: () => undefined, + min_width: "25em" + }); + + modal.htmlTag.find(".modal-body").addClass("modal-connect"); + + const container_last_servers = modal.htmlTag.find(".container-last-servers"); + /* server list toggle */ + { + const button = modal.htmlTag.find(".button-toggle-last-servers"); + const set_show = shown => { + container_last_servers.toggleClass('shown', shown); + button.find(".arrow").toggleClass('down', shown).toggleClass('up', !shown); + settings.changeGlobal("connect_show_last_servers", shown); + }; + button.on('click', event => { + set_show(!container_last_servers.hasClass("shown")); + }); + set_show(settings.static_global("connect_show_last_servers", false)); + } + + const apply = (header, body, footer) => { + const container = modal.htmlTag.find(".container-last-servers .table .body"); + const container_empty = container.find(".body-empty"); + let current_connect_data: connection_log.ConnectionEntry; + const button_connect = footer.find(".button-connect"); const button_connect_tab = footer.find(".button-connect-new-tab"); const button_manage = body.find(".button-manage-profiles"); @@ -25,7 +147,12 @@ namespace Modals { const input_nickname = body.find(".container-nickname input"); const input_password = body.find(".container-password input"); - let updateFields = function () { + let updateFields = (reset_current_data: boolean) => { + if(reset_current_data) { + current_connect_data = undefined; + container.find(".selected").removeClass("selected"); + } + console.log("Updating"); if(selected_profile) input_nickname.attr("placeholder", selected_profile.default_username); @@ -34,7 +161,7 @@ namespace Modals { let address = input_address.val().toString(); settings.changeGlobal(Settings.KEY_CONNECT_ADDRESS, address); - let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.DOMAIN); + let flag_address = !!address.match(Regex.IP_V4) || !!address.match(Regex.IP_V6) || !!address.match(Regex.DOMAIN); let nickname = input_nickname.val().toString(); settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, nickname); @@ -50,17 +177,14 @@ namespace Modals { input_address.val(defaultHost.enforce ? defaultHost.url : settings.static_global(Settings.KEY_CONNECT_ADDRESS, defaultHost.url)); input_address - .on("keyup", () => updateFields()) + .on("keyup", () => updateFields(true)) .on('keydown', event => { if(event.keyCode == KeyCode.KEY_ENTER && !event.shiftKey) button_connect.trigger('click'); }); button_manage.on('click', event => { - const modal = Modals.spawnSettingsModal(); - setTimeout(() => { - modal.htmlTag.find(".tab-profiles").parent(".entry").trigger('click'); - }, 100); + const modal = Modals.spawnSettingsModal("identity-profiles"); modal.close_listener.push(() => { input_profile.trigger('change'); }); @@ -82,7 +206,7 @@ namespace Modals { input_nickname.val(selected_profile.default_username); } input_profile.toggleClass("is-invalid", !selected_profile || !selected_profile.valid()); - updateFields(); + updateFields(true); }); input_profile.val(connect_profile && connect_profile.enforce ? connect_profile.profile.id : connect_profile && connect_profile.profile ? connect_profile.profile.id : 'default').trigger('change'); } @@ -90,20 +214,27 @@ namespace Modals { settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, last_nickname); input_nickname.val(last_nickname); - input_nickname.on("keyup", () => updateFields()); - setTimeout(() => updateFields(), 100); + input_nickname.on("keyup", () => updateFields(true)); + setTimeout(() => updateFields(false), 100); + const server_address = () => { + let address = input_address.val().toString(); + if(address.match(Regex.IP_V6) && !address.startsWith("[")) + return "[" + address + "]"; + return address; + }; button_connect.on('click', event => { - connect_modal.close(); + modal.close(); const connection = server_connections.active_connection_handler(); if(connection) { connection.startConnection( - input_address.val().toString(), + current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), selected_profile, + true, { nickname: input_nickname.val().toString() || selected_profile.default_username, - password: {password: input_password.val().toString(), hashed: false} + password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false} } ); } else { @@ -111,24 +242,77 @@ namespace Modals { } }); button_connect_tab.on('click', event => { - connect_modal.close(); + modal.close(); const connection = server_connections.spawn_server_connection_handler(); server_connections.set_active_connection_handler(connection); connection.startConnection( - input_address.val().toString(), + current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), selected_profile, + true, { nickname: input_nickname.val().toString() || selected_profile.default_username, - password: {password: input_password.val().toString(), hashed: false} + password: (current_connect_data && current_connect_data.password_hash) ? {password: current_connect_data.password_hash, hashed: true} : {password: input_password.val().toString(), hashed: false} } ); }); - }, { - width: '70%' - }); - connect_modal.open(); + + /* server list show */ + { + for(const entry of connection_log.history().slice(0, 10)) { + $.spawn("div").addClass("row").append( + $.spawn("div").addClass("column delete").append($.spawn("div").addClass("icon_em client-delete")).on('click', event => { + event.preventDefault(); + + const row = $(event.target).parents('.row'); + row.hide(250, () => { + row.detach(); + }); + connection_log.delete_entry(entry.address); + container_empty.toggle(container.children().length > 1); + }) + ).append( + $.spawn("div").addClass("column name").append([ + IconManager.generate_tag(IconManager.load_cached_icon(entry.icon_id)), + $.spawn("a").text(entry.name) + ]) + ).append( + $.spawn("div").addClass("column address").text(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : "")) + ).append( + $.spawn("div").addClass("column password").text(entry.flag_password ? tr("Yes") : tr("No")) + ).append( + $.spawn("div").addClass("column country-name").append([ + $.spawn("div").addClass("country flag-" + entry.country.toLowerCase()), + $.spawn("a").text(i18n.country_name(entry.country, tr("Global"))) + ]) + ).append( + $.spawn("div").addClass("column clients").text(entry.clients_online + "/" + entry.clients_total) + ).append( + $.spawn("div").addClass("column connections").text(entry.total_connection + "") + ).on('click', event => { + if(event.isDefaultPrevented()) + return; + + event.preventDefault(); + current_connect_data = entry; + container.find(".selected").removeClass("selected"); + $(event.target).parent('.row').addClass('selected'); + + input_address.val(entry.address.hostname + (entry.address.port != 9987 ? (":" + entry.address.port) : "")); + input_password.val(entry.password_hash ? "WolverinDEV Yeahr!" : "").trigger('change'); + }).on('dblclick', event => { + current_connect_data = entry; + button_connect.trigger('click'); + }).appendTo(container); + container_empty.toggle(false); + } + + } + }; + apply(modal.htmlTag, modal.htmlTag, modal.htmlTag); + + modal.open(); return; } diff --git a/shared/js/ui/modal/ModalCreateChannel.ts b/shared/js/ui/modal/ModalCreateChannel.ts index 1a7251bd..51a6e721 100644 --- a/shared/js/ui/modal/ModalCreateChannel.ts +++ b/shared/js/ui/modal/ModalCreateChannel.ts @@ -13,41 +13,67 @@ namespace Modals { }); render_properties["channel_icon_tab"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0); render_properties["channel_icon_general"] = connection.fileManager.icons.generateTag(channel ? channel.properties.channel_icon_id : 0); + render_properties["create"] = !channel; let template = $("#tmpl_channel_edit").renderTag(render_properties); - return template.tabify(); - }, - footer: () => { - let footer = $.spawn("div"); - footer.addClass("modal-button-group"); - footer.css("margin", "5px"); - - let buttonCancel = $.spawn("button"); - buttonCancel.text(tr("Cancel")).addClass("button_cancel"); - - let buttonOk = $.spawn("button"); - buttonOk.text(tr("Ok")).addClass("button_ok"); - - footer.append(buttonCancel); - footer.append(buttonOk); - - return footer; + + /* the tab functionality */ + { + const container_tabs = template.find(".container-advanced"); + container_tabs.find(".categories .entry").on('click', event => { + const entry = $(event.target); + + container_tabs.find(".bodies > .body").addClass("hidden"); + container_tabs.find(".categories > .selected").removeClass("selected"); + + entry.addClass("selected"); + container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); + }); + + container_tabs.find(".entry").first().trigger('click'); + } + + /* Advanced/normal switch */ + { + const input = template.find(".input-advanced-mode"); + const container_mode = template.find(".mode-container"); + const container_advanced = container_mode.find(".container-advanced"); + const container_simple = container_mode.find(".container-simple"); + input.on('change', event => { + const advanced = input.prop("checked"); + settings.changeGlobal(Settings.KEY_CHANNEL_EDIT_ADVANCED, advanced); + + container_mode.css("overflow", "hidden"); + container_advanced.show().toggleClass("hidden", !advanced); + container_simple.show().toggleClass("hidden", advanced); + + setTimeout(() => { + container_advanced.toggle(advanced); + container_simple.toggle(!advanced); + container_mode.css("overflow", "visible"); + }, 300); + }).prop("checked", settings.static_global(Settings.KEY_CHANNEL_EDIT_ADVANCED)).trigger('change'); + } + + return template.tabify().children(); /* the "render" div */ }, + footer: null, width: 500 }); + modal.htmlTag.find(".modal-body").addClass("modal-channel modal-blue"); - applyGeneralListener(connection, properties, modal.htmlTag.find(".general_properties"), modal.htmlTag.find(".button_ok"), channel); - applyStandardListener(connection, properties, modal.htmlTag.find(".settings_standard"), modal.htmlTag.find(".button_ok"), parent, !channel); - applyPermissionListener(connection, properties, modal.htmlTag.find(".settings_permissions"), modal.htmlTag.find(".button_ok"), permissions, channel); - applyAudioListener(connection, properties, modal.htmlTag.find(".container-channel-settings-audio"), modal.htmlTag.find(".button_ok"), channel); - applyAdvancedListener(connection, properties, modal.htmlTag.find(".settings_advanced"), modal.htmlTag.find(".button_ok"), channel); + applyGeneralListener(connection, properties, modal.htmlTag.find(".container-general"), modal.htmlTag.find(".button_ok"), channel); + applyStandardListener(connection, properties, modal.htmlTag.find(".container-standard"), modal.htmlTag.find(".container-simple"), parent, channel); + applyPermissionListener(connection, properties, modal.htmlTag.find(".container-permissions"), modal.htmlTag.find(".button_ok"), permissions, channel); + applyAudioListener(connection, properties, modal.htmlTag.find(".container-audio"), modal.htmlTag.find(".container-simple"), channel); + applyAdvancedListener(connection, properties, modal.htmlTag.find(".container-misc"), modal.htmlTag.find(".button_ok"), channel); let updated: PermissionValue[] = []; modal.htmlTag.find(".button_ok").click(() => { - modal.htmlTag.find(".settings_permissions").find("input[permission]").each((index, _element) => { + modal.htmlTag.find(".container-permissions").find("input[permission]").each((index, _element) => { let element = $(_element); - if(!element.prop("changed")) return; + if(element.val() == element.attr("original-value")) return; let permission = permissions.resolveInfo(element.attr("permission")); if(!permission) { log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission")); @@ -60,9 +86,13 @@ namespace Modals { console.log(tr("Updated permissions %o"), updated); }).click(() => { modal.close(); + for(const key of Object.keys(channel ? channel.properties : {})) + if(channel.properties[key] == properties[key]) + delete properties[key]; callback(properties, updated); //First may create the channel }); + tooltip(modal.htmlTag); modal.htmlTag.find(".button_cancel").click(() => { modal.close(); callback(); @@ -92,8 +122,8 @@ namespace Modals { tag.find(".button-select-icon").on('click', event => { Modals.spawnIconSelect(connection, id => { - const icon_node = tag.find(".button-select-icon").find(".icon-node"); - icon_node.empty(); + const icon_node = tag.find(".icon-preview"); + icon_node.children().remove(); icon_node.append(connection.fileManager.icons.generateTag(id)); console.log("Selected icon ID: %d", id); @@ -101,6 +131,15 @@ namespace Modals { }, channel ? channel.properties.channel_icon_id : 0); }); + tag.find(".button-icon-remove").on('click', event => { + const icon_node = tag.find(".icon-preview"); + icon_node.children().remove(); + icon_node.append(connection.fileManager.icons.generateTag(0)); + + console.log("Remove channel icon"); + properties.channel_icon_id = 0; + }); + { const channel_password = tag.find(".channel_password"); tag.find(".channel_password").change(function (this: HTMLInputElement) { @@ -120,6 +159,42 @@ namespace Modals { properties.channel_topic = this.value; }).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_TOPIC : PermissionType.B_CHANNEL_MODIFY_TOPIC).granted(1)); + { + const container = tag.find(".container-description"); + const input = container.find("textarea"); + + const insert_tag = (open: string, close: string) => { + if(input.prop("disabled")) + return; + + const node = input[0] as HTMLTextAreaElement; + if (node.selectionStart || node.selectionStart == 0) { + const startPos = node.selectionStart; + const endPos = node.selectionEnd; + node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos); + node.selectionEnd = endPos + open.length; + node.selectionStart = node.selectionEnd; + } else { + node.value += open + close; + node.selectionEnd = node.value.length - close.length; + node.selectionStart = node.selectionEnd; + } + + input.focus().trigger('change'); + }; + + input.on('change', event => { + console.log(tr("Channel description edited: %o"), input.val()); + properties.channel_description = input.val() as string; + }); + + container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]')); + container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]')); + container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]')); + container.find(".button-color input").on('change', event => { + insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]') + }) + } tag.find(".channel_description").change(function (this: HTMLInputElement) { properties.channel_description = this.value; }).prop("disabled", !connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DESCRIPTION : PermissionType.B_CHANNEL_MODIFY_DESCRIPTION).granted(1)); @@ -132,62 +207,278 @@ namespace Modals { } } - function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, parent: ChannelEntry, create: boolean) { - tag.find("input[name=\"channel_type\"]").change(function (this: HTMLInputElement) { - switch(this.value) { - case "semi": - properties.channel_flag_permanent = false; - properties.channel_flag_semi_permanent = true; - break; - case "perm": - properties.channel_flag_permanent = true; - properties.channel_flag_semi_permanent = false; - break; - default: - properties.channel_flag_permanent = false; - properties.channel_flag_semi_permanent = false; - break; + function applyStandardListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, parent: ChannelEntry, channel: ChannelEntry) { + /* Channel type */ + { + const input_advanced_type = tag.find("input[name='channel_type']"); + + let _in_update = false; + const update_simple_type = () => { + if(_in_update) + return; + + let type; + if(properties.channel_flag_default || (typeof(properties.channel_flag_default) === "undefined" && channel && channel.properties.channel_flag_default)) + type = "def"; + else if(properties.channel_flag_permanent || (typeof(properties.channel_flag_permanent) === "undefined" && channel && channel.properties.channel_flag_permanent)) + type = "perm"; + else if(properties.channel_flag_semi_permanent || (typeof(properties.channel_flag_semi_permanent) === "undefined" && channel && channel.properties.channel_flag_semi_permanent)) + type = "semi"; + else + type = "temp"; + + console.log(type); + console.log(Object.assign({}, properties)); + simple.find("option[name='channel-type'][value='" + type + "']").prop("selected", true); + }; + + input_advanced_type.on('change', event => { + switch(input_advanced_type.val()) { + case "semi": + properties.channel_flag_permanent = false; + properties.channel_flag_semi_permanent = true; + break; + case "perm": + properties.channel_flag_permanent = true; + properties.channel_flag_semi_permanent = false; + break; + default: + properties.channel_flag_permanent = false; + properties.channel_flag_semi_permanent = false; + break; + } + update_simple_type(); + }); + + const permission_temp = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1); + const permission_semi = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1); + const permission_perm = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1); + const permission_default = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) && + connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1); + + /* advanced type listeners */ + const container_types = tag.find(".container-channel-type"); + const tag_type_temp = container_types.find(".type-temp"); + const tag_type_semi = container_types.find(".type-semi"); + const tag_type_perm = container_types.find(".type-perm"); + const select_default = tag.find(".input-flag-default"); + + { + + if(!channel) { + if(permission_perm) + tag_type_perm.find("input").trigger('click'); + else if(permission_semi) + tag_type_semi.find("input").trigger('click'); + else + tag_type_temp.find("input").trigger('click'); + } + + select_default.on('change', event => { + const node = select_default[0] as HTMLInputElement; + console.log(node.checked); + + properties.channel_flag_default = node.checked; + + if(node.checked) + tag_type_perm.find("input").prop("checked", true); + + tag_type_temp + .toggleClass("disabled", node.checked || !permission_temp) + .find("input").prop("disabled", node.checked || !permission_temp); + + tag_type_semi + .toggleClass("disabled", node.checked || !permission_semi) + .find("input").prop("disabled", node.checked || !permission_semi); + + tag_type_perm + .toggleClass("disabled", node.checked || !permission_perm) + .find("input").prop("disabled", node.checked || !permission_perm); + + update_simple_type(); + }).prop("disabled", !permission_default).trigger('change').parent().toggleClass("disabled", !permission_default); } - }); - tag.find("input[name=\"channel_type\"][value=\"temp\"]") - .prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_TEMPORARY : PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY).granted(1)); - tag.find("input[name=\"channel_type\"][value=\"semi\"]") - .prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT).granted(1)); - tag.find("input[name=\"channel_type\"][value=\"perm\"]") - .prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1)); - if(create) - tag.find("input[name=\"channel_type\"]:not(:disabled)").last().prop("checked", true).trigger('change'); - tag.find("input[name=\"channel_default\"]").change(function (this: HTMLInputElement) { - console.log(this.checked); - properties.channel_flag_default = this.checked; + /* simple */ + { + simple.find("option[name='channel-type'][value='def']").prop("disabled", !permission_default); + simple.find("option[name='channel-type'][value='perm']").prop("disabled", !permission_perm); + simple.find("option[name='channel-type'][value='semi']").prop("disabled", !permission_semi); + simple.find("option[name='channel-type'][value='temp']").prop("disabled", !permission_temp); - let elements = tag.find("input[name=\"channel_type\"]"); - elements.prop("disabled", this.checked); - if(this.checked) { - elements.prop("checked", false); - tag.find("input[name=\"channel_type\"][value=\"perm\"]").prop("checked", true).trigger("change"); + simple.find("select[name='channel-type']").on('change', event => { + try { + _in_update = true; + switch ((event.target as HTMLSelectElement).value) { + case "temp": + properties.channel_flag_permanent = false; + properties.channel_flag_semi_permanent = false; + properties.channel_flag_default = false; + select_default.prop("checked", false).trigger('change'); + tag_type_temp.trigger('click'); + break; + case "semi": + properties.channel_flag_permanent = false; + properties.channel_flag_semi_permanent = true; + properties.channel_flag_default = false; + select_default.prop("checked", false).trigger('change'); + tag_type_semi.trigger('click'); + break; + case "perm": + properties.channel_flag_permanent = true; + properties.channel_flag_semi_permanent = false; + properties.channel_flag_default = false; + select_default.prop("checked", false).trigger('change'); + tag_type_perm.trigger('click'); + break; + case "def": + properties.channel_flag_permanent = true; + properties.channel_flag_semi_permanent = false; + properties.channel_flag_default = true; + select_default.prop("checked", true).trigger('change'); + break; + } + } finally { + _in_update = false; + /* We dont need to update the simple type because we changed the advanced part to the just changed simple part */ + //update_simple_type(); + } + }); } - }).prop("disabled", - !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_PERMANENT : PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT).granted(1) || - !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_DEFAULT : PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT).granted(1)); + } - tag.find("input[name=\"talk_power\"]").change(function (this: HTMLInputElement) { - properties.channel_needed_talk_power = parseInt(this.value); - }).prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1)); + /* Talk power */ + { + const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_NEEDED_TALK_POWER : PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER).granted(1); + const input_advanced = tag.find("input[name='talk_power']").prop("disabled", !permission); + const input_simple = simple.find("input[name='talk_power']").prop("disabled", !permission); - let orderTag = tag.find(".order_id"); - for(let channel of (parent ? parent.children() : connection.channelTree.rootChannel())) - $.spawn("option").attr("channelId", channel.channelId.toString()).text(channel.channelName()).appendTo(orderTag); + input_advanced.on('change', event => { + properties.channel_needed_talk_power = parseInt(input_advanced.val() as string); + input_simple.val(input_advanced.val()); + }); - orderTag.change(function (this: HTMLSelectElement) { - let selected = $(this.options.item(this.selectedIndex)); - properties.channel_order = parseInt(selected.attr("channelId")); - }).prop("disabled", !connection.permissions.neededPermission(create ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1)); - orderTag.find("option").last().prop("selected", true); + input_simple.on('change', event => { + properties.channel_needed_talk_power = parseInt(input_simple.val() as string); + input_advanced.val(input_simple.val()); + }); + } + + /* Channel order */ + { + const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_SORTORDER : PermissionType.B_CHANNEL_MODIFY_SORTORDER).granted(1); + + const advanced_order_id = tag.find(".order_id").prop("disabled", !permission) as JQuery; + const simple_order_id = simple.find(".order_id").prop("disabled", !permission) as JQuery; + + for(let previous_channel of (parent ? parent.children() : connection.channelTree.rootChannel())) { + let selected = channel && channel.properties.channel_order == previous_channel.channelId; + $.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(advanced_order_id); + $.spawn("option").attr("channelId", previous_channel.channelId.toString()).prop("selected", selected).text(previous_channel.channelName()).appendTo(simple_order_id); + } + + advanced_order_id.on('change', event => { + simple_order_id[0].selectedIndex = advanced_order_id[0].selectedIndex; + const selected = $(advanced_order_id[0].options.item(advanced_order_id[0].selectedIndex)); + properties.channel_order = parseInt(selected.attr("channelId")); + }); + + simple_order_id.on('change', event => { + advanced_order_id[0].selectedIndex = simple_order_id[0].selectedIndex; + const selected = $(simple_order_id[0].options.item(simple_order_id[0].selectedIndex)); + properties.channel_order = parseInt(selected.attr("channelId")); + }); + } + + + /* Advanced only */ + { + const container_max_users = tag.find(".container-max-users"); + + const container_unlimited = container_max_users.find(".container-unlimited"); + const container_limited = container_max_users.find(".container-limited"); + + const input_unlimited = container_unlimited.find("input[value='unlimited']"); + const input_limited = container_limited.find("input[value='limited']"); + const input_limit = container_limited.find(".channel_maxclients"); + + const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1); + + if(!permission) { + input_unlimited.prop("disabled", true); + input_limited.prop("disabled", true); + input_limit.prop("disabled", true); + + container_limited.addClass("disabled"); + container_unlimited.addClass("disabled"); + } else { + container_max_users.find("input[name='max_users']").on('change', event => { + const node = event.target as HTMLInputElement; + console.log(tr("Channel max user mode: %o"), node.value); + + const flag = node.value === "unlimited"; + input_limit + .prop("disabled", flag) + .parent().toggleClass("disabled", flag); + properties.channel_flag_maxclients_unlimited = flag; + }); + + input_limit.on('change', event => { + properties.channel_maxclients = parseInt(input_limit.val() as string); + console.log(tr("Changed max user limit to %o"), properties.channel_maxclients); + }); + + setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100); + } + } + + { + const container_max_users = tag.find(".container-max-family-users"); + + const container_unlimited = container_max_users.find(".container-unlimited"); + const container_inherited = container_max_users.find(".container-inherited"); + const container_limited = container_max_users.find(".container-limited"); + + const input_unlimited = container_unlimited.find("input[value='unlimited']"); + const input_inherited = container_inherited.find("input[value='inherited']"); + const input_limited = container_limited.find("input[value='limited']"); + const input_limit = container_limited.find(".channel_maxfamilyclients"); + + const permission = connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1); + + if(!permission) { + input_unlimited.prop("disabled", true); + input_inherited.prop("disabled", true); + input_limited.prop("disabled", true); + input_limit.prop("disabled", true); + + container_limited.addClass("disabled"); + container_unlimited.addClass("disabled"); + container_inherited.addClass("disabled"); + } else { + container_max_users.find("input[name='max_family_users']").on('change', event => { + const node = event.target as HTMLInputElement; + console.log(tr("Channel max family user mode: %o"), node.value); + + const flag_unlimited = node.value === "unlimited"; + const flag_inherited = node.value === "inherited"; + input_limit + .prop("disabled", flag_unlimited || flag_inherited) + .parent().toggleClass("disabled", flag_unlimited || flag_inherited); + properties.channel_flag_maxfamilyclients_unlimited = flag_unlimited; + properties.channel_flag_maxfamilyclients_inherited = flag_inherited; + }); + + input_limit.on('change', event => { + properties.channel_maxfamilyclients = parseInt(input_limit.val() as string); + console.log(tr("Changed max family user limit to %o"), properties.channel_maxfamilyclients); + }); + + setTimeout(() => container_max_users.find("input:checked").trigger('change'), 100); + } + } } - function applyPermissionListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, permissions: PermissionManager, channel?: ChannelEntry) { let apply_permissions = (channel_permissions: PermissionValue[]) => { console.log(tr("Got permissions: %o"), channel_permissions); @@ -200,6 +491,9 @@ namespace Modals { tag.find("input[permission]").each((index, _element) => { let element = $(_element); + element.attr("original-value", 0); + element.val(0); + let permission = permissions.resolveInfo(element.attr("permission")); if(!permission) { log.error(LogCategory.PERMISSIONS, tr("Failed to resolve channel permission for name %o"), element.attr("permission")); @@ -207,23 +501,16 @@ namespace Modals { return; } - let old_value: number = 0; - element.on("click keyup", () => { - console.log(tr("Permission triggered! %o"), element.val() != old_value); - element.prop("changed", element.val() != old_value); - }); - for(let cperm of channel_permissions) if(cperm.type == permission) { - element.val(old_value = cperm.value); + element.val(cperm.value); + element.attr("original-value", cperm.value); return; } - element.val(0); }); - if(!permissions.neededPermission(PermissionType.I_CHANNEL_MODIFY_POWER).granted(required_power, false)) { - tag.find("input[permission]").prop("disabled", false); //No permissions - } + const permission = permissions.neededPermission(PermissionType.I_CHANNEL_MODIFY_POWER).granted(required_power, false); + tag.find("input[permission]").prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); //No permissions }; if(channel) { @@ -234,7 +521,17 @@ namespace Modals { } else apply_permissions([]); } - function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) { + function applyAudioListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, simple: JQuery, channel?: ChannelEntry) { + const bandwidth_mapping = [ + /* SPEEX narrow */ [2.49, 2.69, 2.93, 3.17, 3.17, 3.56, 3.56, 4.05, 4.05, 4.44, 5.22], + /* SPEEX wide */ [2.69, 2.93, 3.17, 3.42, 3.76, 4.25, 4.74, 5.13, 5.62, 6.40, 7.37], + /* SPEEX ultra */ [2.73, 3.12, 3.37, 3.61, 4.00, 4.49, 4.93, 5.32, 5.81, 6.59, 7.57], + /* CELT */ [6.10, 6.10, 7.08, 7.08, 7.08, 8.06, 8.06, 8.06, 8.06, 10.01, 13.92], + + /* Opus Voice */ [2.73, 3.22, 3.71, 4.20, 4.74, 5.22, 5.71, 6.20, 6.74, 7.23, 7.71], + /* Opus Music */ [3.08, 3.96, 4.83, 5.71, 6.59, 7.47, 8.35, 9.23, 10.11, 10.99, 11.87] + ]; + let update_template = () => { let codec = properties.channel_codec; if(!codec && channel) @@ -246,14 +543,25 @@ namespace Modals { quality = channel.properties.channel_codec_quality; if(!quality) return; - if(codec == 4 && quality == 4) - tag.find("input[name=\"voice_template\"][value=\"voice_mobile\"]").prop("checked", true); - else if(codec == 4 && quality == 6) - tag.find("input[name=\"voice_template\"][value=\"voice_desktop\"]").prop("checked", true); - else if(codec == 5 && quality == 6) - tag.find("input[name=\"voice_template\"][value=\"music\"]").prop("checked", true); + let template_name = "custom"; + + { + if(codec == 4 && quality == 4) + template_name = "voice_mobile"; + else if(codec == 4 && quality == 6) + template_name = "voice_desktop"; + else if(codec == 5 && quality == 6) + template_name = "music"; + } + tag.find("input[name='voice_template'][value='" + template_name + "']").prop("checked", true); + simple.find("option[name='voice_template'][value='" + template_name + "']").prop("selected", true); + + let bandwidth; + if(codec < 0 || codec > bandwidth_mapping.length) + bandwidth = 0; else - tag.find("input[name=\"voice_template\"][value=\"custom\"]").prop("checked", true); + bandwidth = bandwidth_mapping[codec][quality] || 0; /* OOB access results in undefined, but is allowed */ + tag.find(".container-needed-bandwidth").text(bandwidth.toFixed(2) + " KiB/s"); }; let change_codec = codec => { @@ -264,20 +572,30 @@ namespace Modals { update_template(); }; - let quality_slider = tag.find(".voice_quality_slider"); - let quality_number = tag.find(".voice_quality_number"); + const container_quality = tag.find(".container-quality"); + const slider_quality = sliderfy(container_quality.find(".container-slider"), { + initial_value: properties.channel_codec_quality || 6, + unit: "", + min_value: 1, + max_value: 10, + step: 1, + value_field: container_quality.find(".container-value") + }); + let change_quality = (quality: number) => { if(properties.channel_codec_quality == quality) return; properties.channel_codec_quality = quality; - if(quality_slider.val() != quality) - quality_slider.val(quality); - if(parseInt(quality_number.text()) != quality) - quality_number.text(quality); + slider_quality.value(quality); update_template(); }; - tag.find("input[name=\"voice_template\"]").change(function (this: HTMLInputElement) { + container_quality.find(".container-slider").on('change', event => { + properties.channel_codec_quality = slider_quality.value(); + update_template(); + }); + + tag.find("input[name='voice_template']").change(function (this: HTMLInputElement) { switch(this.value) { case "custom": break; @@ -295,12 +613,43 @@ namespace Modals { break; } }); - tag.find("input[name=\"voice_template\"][value=\"voice_mobile\"]") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); - tag.find("input[name=\"voice_template\"][value=\"voice_desktop\"]") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); - tag.find("input[name=\"voice_template\"][value=\"music\"]") - .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); + + simple.find("select[name='voice_template']").change(function (this: HTMLInputElement) { + switch(this.value) { + case "custom": + break; + case "music": + change_codec(5); + change_quality(6); + break; + case "voice_desktop": + change_codec(4); + change_quality(6); + break; + case "voice_mobile": + change_codec(4); + change_quality(4); + break; + } + }); + + /* disable not granted templates */ + { + tag.find("input[name='voice_template'][value='voice_mobile']") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + simple.find("option[name='voice_template'][value='voice_mobile']") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + + tag.find("input[name='voice_template'][value=\"voice_desktop\"]") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + simple.find("option[name='voice_template'][value=\"voice_desktop\"]") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSVOICE).granted(1)); + + tag.find("input[name='voice_template'][value=\"music\"]") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); + simple.find("option[name='voice_template'][value=\"music\"]") + .prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_OPUSMUSIC).granted(1)); + } let codecs = tag.find(".voice_codec option"); codecs.eq(0).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_CODEC_SPEEX8).granted(1)); @@ -323,8 +672,6 @@ namespace Modals { change_quality(channel.properties.channel_codec_quality); } update_template(); - - quality_slider.on('input', event => change_quality(parseInt(quality_slider.val() as string))); } function applyAdvancedListener(connection: ConnectionHandler, properties: ChannelProperties, tag: JQuery, button: JQuery, channel?: ChannelEntry) { @@ -332,58 +679,26 @@ namespace Modals { properties.channel_topic = this.value; }); - tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) { - properties.channel_delete_delay = parseInt(this.value); - }).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1)); - - tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) { - properties.channel_codec_is_unencrypted = parseInt(this.value) == 0; - }).prop("disabled", !connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1)); - { - let tag_infinity = tag.find("input[name=\"max_users\"][value=\"infinity\"]"); - let tag_limited = tag.find("input[name=\"max_users\"][value=\"limited\"]"); - let tag_limited_value = tag.find(".channel_maxclients"); - - if(!connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) { - tag_infinity.prop("disabled", true); - tag_limited.prop("disabled", true); - tag_limited_value.prop("disabled", true); - } else { - tag.find("input[name=\"max_users\"]").change(function (this: HTMLInputElement) { - console.log(this.value); - let infinity = this.value == "infinity"; - tag_limited_value.prop("disabled", infinity); - properties.channel_flag_maxclients_unlimited = infinity; - }); - - tag_limited_value.change(event => properties.channel_maxclients = parseInt(tag_limited_value.val() as string)); - tag.find("input[name=\"max_users\"]:checked").trigger('change'); - } + const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY).granted(1); + tag.find(".channel_delete_delay").change(function (this: HTMLInputElement) { + properties.channel_delete_delay = parseInt(this.value); + }).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); } { - let tag_inherited = tag.find("input[name=\"max_users_family\"][value=\"inherited\"]"); - let tag_infinity = tag.find("input[name=\"max_users_family\"][value=\"infinity\"]"); - let tag_limited = tag.find("input[name=\"max_users_family\"][value=\"limited\"]"); - let tag_limited_value = tag.find(".channel_maxfamilyclients"); + tag.find(".button-delete-max").on('click', event => { + const power = connection.permissions.neededPermission(PermissionType.I_CHANNEL_CREATE_MODIFY_WITH_TEMP_DELETE_DELAY).value; + let value = power == -2 ? 0 : power == -1 ? (7 * 24 * 60 * 60) : power; + tag.find(".channel_delete_delay").val(value).trigger('change'); + }); + } - if(!connection.permissions.neededPermission(!channel ? PermissionType.B_CHANNEL_CREATE_WITH_MAXCLIENTS : PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS).granted(1)) { - tag_inherited.prop("disabled", true); - tag_infinity.prop("disabled", true); - tag_limited.prop("disabled", true); - tag_limited_value.prop("disabled", true); - } else { - tag.find("input[name=\"max_users_family\"]").change(function (this: HTMLInputElement) { - console.log(this.value); - tag_limited_value.prop("disabled", this.value != "limited"); - properties.channel_flag_maxfamilyclients_unlimited = this.value == "infinity"; - properties.channel_flag_maxfamilyclients_inherited = this.value == "inherited"; - }); - - tag_limited_value.change(event => properties.channel_maxfamilyclients = parseInt(tag_limited_value.val() as string)); - tag.find("input[name=\"max_users_family\"]:checked").trigger('change'); - } + { + const permission = connection.permissions.neededPermission(PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED).granted(1); + tag.find(".channel_codec_is_unencrypted").change(function (this: HTMLInputElement) { + properties.channel_codec_is_unencrypted = parseInt(this.value) == 0; + }).prop("disabled", !permission).parent(".input-boxed").toggleClass("disabled", !permission); } } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalServerGroupDialog.ts b/shared/js/ui/modal/ModalGroupAssignment.ts similarity index 52% rename from shared/js/ui/modal/ModalServerGroupDialog.ts rename to shared/js/ui/modal/ModalGroupAssignment.ts index feecbe0a..d4dafd0a 100644 --- a/shared/js/ui/modal/ModalServerGroupDialog.ts +++ b/shared/js/ui/modal/ModalGroupAssignment.ts @@ -1,13 +1,19 @@ namespace Modals { + let current_modal: Modal; export function createServerGroupAssignmentModal(client: ClientEntry, callback: (group: Group, flag: boolean) => Promise) { - const modal = createModal({ + if(current_modal) + current_modal.close(); + + current_modal = createModal({ header: tr("Server Groups"), body: () => { let tag: any = {}; let groups = tag["groups"] = []; - tag["client_name"] = client.clientNickName(); - for(let group of client.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { + tag["client"] = client.createChatTag(); + + const _groups = client.channelTree.client.groups.serverGroups.sort(GroupManager.sorter()); + for(let group of _groups) { if(group.type != GroupType.NORMAL) continue; let entry = {} as any; @@ -15,6 +21,7 @@ namespace Modals { entry["name"] = group.name; entry["assigned"] = client.groupAssigned(group); entry["disabled"] = !client.channelTree.client.permissions.neededPermission(PermissionType.I_GROUP_MEMBER_ADD_POWER).granted(group.requiredMemberRemovePower); + entry["default"] = client.channelTree.server.properties.virtualserver_default_server_group == group.id; tag["icon_" + group.id] = client.channelTree.client.fileManager.icons.generateTag(group.properties.iconid); groups.push(entry); } @@ -33,32 +40,32 @@ namespace Modals { } let target = entry.prop("checked"); - callback(group, target).then(flag => flag ? Promise.resolve() : Promise.reject()).catch(error => entry.prop("checked", !target)); + callback(group, target).then(flag => flag ? Promise.resolve() : Promise.reject()).then(() => { + template.find(".group-entry input[default]").prop("checked", template.find(".group-entry input:checked").length == 0); + }).catch(error => entry.prop("checked", !target)); }); }); + template.find(".button-close").on('click', () => current_modal.close()); + template.find(".button-remove-all").on('click', () => { + template.find(".group-entry input").each((_idx, _entry) => { + let entry = $(_entry); + if(entry.attr("default") !== undefined || !entry.prop("checked")) + return; + + entry.prop("checked", false).trigger('change'); + }); + }); + template.find(".group-entry input[default]").prop("checked", template.find(".group-entry input:checked").length == 0); return template; }, - footer: () => { - let footer = $.spawn("div"); - footer.addClass("modal-button-group"); - footer.css("margin", "5px"); - - let button_close = $.spawn("button"); - button_close.text(tr("Close")).addClass("button_close"); - - footer.append(button_close); - - return footer; - }, + footer: null, width: "max-content" }); - modal.htmlTag.find(".button_close").click(() => { - modal.close(); - }); - - modal.open(); + current_modal.htmlTag.find(".modal-body").addClass("modal-server-group-assignments"); + current_modal.close_listener.push(() => current_modal = undefined); + current_modal.open(); } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalIdentity.ts b/shared/js/ui/modal/ModalIdentity.ts new file mode 100644 index 00000000..beaafba5 --- /dev/null +++ b/shared/js/ui/modal/ModalIdentity.ts @@ -0,0 +1,193 @@ +namespace Modals { + export function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity): Modal { + let modal: Modal; + let elapsed_timer: NodeJS.Timer; + + modal = createModal({ + header: tr("Improve identity"), + body: () => { + let template = $("#tmpl_settings-teamspeak_improve").renderTag(); + template = $.spawn("div").append(template); + + let active; + const button_start_stop = template.find(".button-start-stop"); + const button_close = template.find(".button-close"); + const input_current_level = template.find(".identity-level input"); + const input_target_level = template.find(".identity-target-level input"); + const input_threads = template.find(".threads input"); + const input_hash_rate = template.find(".hash-rate input"); + const input_elapsed = template.find(".time-elapsed input"); + + button_close.on('click', event => { + if (active) + button_start_stop.trigger('click'); + + if (modal.shown) + modal.close(); + }); + + button_start_stop.on('click', event => { + button_start_stop + .toggleClass('btn-success', active) + .toggleClass('btn-danger', !active) + .text(active ? tr("Start") : tr("Stop")); + + input_threads.prop("disabled", !active); + input_target_level.prop("disabled", !active); + if (active) { + input_hash_rate.val(0); + clearInterval(elapsed_timer); + active = false; + return; + } + active = true; + input_hash_rate.val("nan"); + + const threads = parseInt(input_threads.val() as string); + const target_level = parseInt(input_target_level.val() as string); + if (target_level == 0) { + identity.improve_level(-1, threads, () => active, current_level => { + input_current_level.val(current_level); + }, hash_rate => { + input_hash_rate.val(hash_rate); + }).catch(error => { + console.error(error); + createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.
Error:") + error).open(); + if (active) + button_start_stop.trigger('click'); + }); + } else { + identity.improve_level(target_level, threads, () => active, current_level => { + input_current_level.val(current_level); + }, hash_rate => { + input_hash_rate.val(hash_rate); + }).then(success => { + if (success) { + identity.level().then(level => { + input_current_level.val(level); + createInfoModal(tr("Identity successfully improved"), MessageHelper.formatMessage(tr("Identity successfully improved to level {}"), level)).open(); + }).catch(error => { + input_current_level.val("error: " + error); + }); + } + if (active) + button_start_stop.trigger('click'); + }).catch(error => { + console.error(error); + createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.
Error:") + error).open(); + if (active) + button_start_stop.trigger('click'); + }); + } + + const begin = Date.now(); + elapsed_timer = setInterval(() => { + const time = (Date.now() - begin) / 1000; + let seconds = Math.floor(time % 60).toString(); + let minutes = Math.floor(time / 60).toString(); + + if (seconds.length < 2) + seconds = "0" + seconds; + + if (minutes.length < 2) + minutes = "0" + minutes; + + input_elapsed.val(minutes + ":" + seconds); + }, 1000); + }); + + + template.find(".identity-unique-id input").val(identity.uid()); + identity.level().then(level => { + input_current_level.val(level); + }).catch(error => { + input_current_level.val("error: " + error); + }); + return template; + }, + footer: undefined, + width: 750 + }); + modal.close_listener.push(() => modal.htmlTag.find(".button-close").trigger('click')); + modal.open(); + return modal; + } + + export function spawnTeamSpeakIdentityImport(callback: (identity: profiles.identities.TeaSpeakIdentity) => any): Modal { + let modal: Modal; + let loaded_identity: profiles.identities.TeaSpeakIdentity; + + modal = createModal({ + header: tr("Import identity"), + body: () => { + let template = $("#tmpl_settings-teamspeak_import").renderTag(); + template = $.spawn("div").append(template); + + template.find(".button-load-file").on('click', event => template.find(".input-file").trigger('click')); + + const button_import = template.find(".button-import"); + const set_error = message => { + template.find(".success").hide(); + if (message) { + template.find(".error").text(message).show(); + button_import.prop("disabled", true); + } else + template.find(".error").hide(); + }; + + const import_identity = (data: string, ini: boolean) => { + profiles.identities.TeaSpeakIdentity.import_ts(data, ini).then(identity => { + loaded_identity = identity; + set_error(""); + button_import.prop("disabled", false); + template.find(".success").show(); + }).catch(error => { + set_error("Failed to load identity: " + error); + }); + }; + + { /* file select button */ + template.find(".input-file").on('change', event => { + const element = event.target as HTMLInputElement; + const file_reader = new FileReader(); + + file_reader.onload = function () { + import_identity(file_reader.result as string, true); + }; + + file_reader.onerror = ev => { + console.error(tr("Failed to read give identity file: %o"), ev); + set_error(tr("Failed to read file!")); + return; + }; + + if (element.files && element.files.length > 0) + file_reader.readAsText(element.files[0]); + }); + } + + { /* text input */ + template.find(".button-load-text").on('click', event => { + createInputModal("Import identity from text", "Please paste your idenity bellow
", text => text.length > 0 && text.indexOf('V') != -1, result => { + if (result) + import_identity(result as string, false); + }).open(); + }); + } + + button_import.on('click', event => { + modal.close(); + callback(loaded_identity); + }); + + set_error(""); + button_import.prop("disabled", true); + return template; + }, + footer: undefined, + width: 750 + }); + modal.open(); + return modal; + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/ModalKeySelect.ts b/shared/js/ui/modal/ModalKeySelect.ts new file mode 100644 index 00000000..af6ecda2 --- /dev/null +++ b/shared/js/ui/modal/ModalKeySelect.ts @@ -0,0 +1,40 @@ +namespace Modals { + export function spawnKeySelect(callback: (key?: ppt.KeyEvent) => void) { + let modal = createModal({ + header: tr("Select a key"), + body: () => $("#tmpl_key_select").renderTag().children(), + footer: null, + + width: "", + closeable: false + }); + + const container_key = modal.htmlTag.find(".container-key a"); + const button_save = modal.htmlTag.find(".button-save"); + const button_cancel = modal.htmlTag.find(".button-cancel"); + + let current_key; + const listener = (event: ppt.KeyEvent) => { + if(event.type === ppt.EventType.KEY_PRESS) { + //console.log(tr("Key select got key press for %o"), event); + current_key = event; + + container_key.text(ppt.key_description(event)); + button_save.prop("disabled", false); + } + }; + + + button_save.on('click', () => { + callback(current_key); + modal.close(); + }).prop("disabled", true); + button_cancel.on('click', () => modal.close()); + + ppt.register_key_listener(listener); + modal.close_listener.push(() => ppt.unregister_key_listener(listener)); + + modal.htmlTag.find(".modal-body").addClass("modal-keyselect modal-green"); + modal.open(); + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/ModalServerEdit.ts b/shared/js/ui/modal/ModalServerEdit.ts index cff5ce83..dc76ed36 100644 --- a/shared/js/ui/modal/ModalServerEdit.ts +++ b/shared/js/ui/modal/ModalServerEdit.ts @@ -1,369 +1,795 @@ -/// - namespace Modals { - export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => any) { - let properties: ServerProperties = {} as ServerProperties; //The changes properties + export function createServerModal(server: ServerEntry, callback: (properties?: ServerProperties) => Promise) { + const properties = Object.assign({}, server.properties); - const render_properties = {}; - Object.assign(render_properties, server.properties); - render_properties["virtualserver_icon_tab"] = server.channelTree.client.fileManager.icons.generateTag(server.properties.virtualserver_icon_id); - render_properties["virtualserver_icon_general"] = server.channelTree.client.fileManager.icons.generateTag(server.properties.virtualserver_icon_id); + let _valid_states: {[key: string]:boolean} = { + general: false + }; - const modal_template = $("#tmpl_server_edit").renderTag(render_properties); - const modal = modal_template.modalize((header, body, footer) => { - return { - body: body.tabify() + let _toggle_valid = (key: string | undefined, value?: boolean) => { + if(typeof(key) === "string") { + _valid_states[key] = value; } + + let flag = true; + for(const key of Object.keys(_valid_states)) + if(!_valid_states[key]) { + flag = false; + break; + } + + if(flag) { + flag = false; + for(const property_name of Object.keys(properties)) { + if(server.properties[property_name] !== properties[property_name]) { + flag = true; + break; + } + } + } + + button_save.prop("disabled", !flag); + }; + + const modal = createModal({ + header: tr("Manage the Virtual Server"), + body: () => { + const template = $("#tmpl_server_edit").renderTag(Object.assign(Object.assign({}, server.properties), { + server_icon: server.channelTree.client.fileManager.icons.generateTag(server.properties.virtualserver_icon_id) + })); + + /* the tab functionality */ + { + const container_tabs = template.find(".container-categories"); + container_tabs.find(".categories .entry").on('click', event => { + const entry = $(event.target); + + container_tabs.find(".bodies > .body").addClass("hidden"); + container_tabs.find(".categories > .selected").removeClass("selected"); + + entry.addClass("selected"); + container_tabs.find(".bodies > .body." + entry.attr("container")).removeClass("hidden"); + }); + + container_tabs.find(".entry").first().trigger('click'); + } + + apply_general_listener(template.find(".container-general"), server, properties, _toggle_valid); + apply_host_listener(template.find(".container-host"), server, properties, _toggle_valid); + apply_network_listener(template.find(".container-network"), server, properties, _toggle_valid, modal); + apply_security_listener(template.find(".container-security"), server, properties, _toggle_valid); + apply_messages_listener(template.find(".container-messages"), server, properties, _toggle_valid); + apply_misc_listener(template.find(".container-misc"), server, properties, _toggle_valid); + + return template.contents(); + }, + footer: null, + min_width: "35em" }); - server_applyGeneralListener(properties, server, modal.htmlTag.find(".properties-general"), modal.htmlTag.find(".button_ok")); - server_applyTransferListener(properties, server, modal.htmlTag.find('.properties_transfer')); - server_applyHostListener(server, properties, server.properties, modal.htmlTag.find(".properties_host"), modal.htmlTag.find(".button_ok")); - server_applyMessages(properties, server, modal.htmlTag.find(".properties_messages")); - server_applyFlood(properties, server, modal.htmlTag.find(".properties_flood")); - server_applySecurity(properties, server, modal.htmlTag.find(".properties_security")); - server_applyMisc(properties, server, modal.htmlTag.find(".properties_misc")); + tooltip(modal.htmlTag); - modal.htmlTag.find(".button_ok").click(() => { - modal.close(); - callback(properties); //First may create the channel + const button_save = modal.htmlTag.find(".button-save"); + button_save.on('click', event => { + const changed = {} as ServerProperties; + for(const property_name of Object.keys(properties)) + if(server.properties[property_name] !== properties[property_name]) + changed[property_name] = properties[property_name]; + callback(changed).then(() => { + _toggle_valid(undefined); + }); }); - modal.htmlTag.find(".button_cancel").click(() => { + modal.htmlTag.find(".button-cancel").on('click', event => { modal.close(); callback(); }); + _toggle_valid("general", true); + modal.htmlTag.find(".modal-body").addClass("modal-server-edit modal-blue"); modal.open(); } - function server_applyGeneralListener(properties: ServerProperties, server: ServerEntry, tag: JQuery, button: JQuery) { - const connection_handler = server.channelTree.client; - let updateButton = () => { - if(tag.find(".input_error").length == 0) - button.removeAttr("disabled"); - else button.attr("disabled", "true"); + function apply_general_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + /* name */ + { + const container = tag.find(".virtualserver_name"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1); + + container.on('change', event => { + properties.virtualserver_name = container.val() as string; + + const invalid = properties.virtualserver_name.length > 70 || properties.virtualserver_name.length < 1; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_name", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* icon */ + { + tag.find(".button-select-icon").on('click', event => { + Modals.spawnIconSelect(server.channelTree.client, id => { + const icon_node = tag.find(".icon-preview"); + icon_node.children().remove(); + icon_node.append(server.channelTree.client.fileManager.icons.generateTag(id)); + + console.log("Selected icon ID: %d", id); + properties.virtualserver_icon_id = id; + callback_valid(undefined); //Toggle save button update + }, properties.virtualserver_icon_id); + }); + + tag.find(".button-icon-remove").on('click', event => { + const icon_node = tag.find(".icon-preview"); + icon_node.children().remove(); + icon_node.append(server.channelTree.client.fileManager.icons.generateTag(0)); + + console.log("Remove server icon"); + properties.virtualserver_icon_id = 0; + callback_valid(undefined); //Toggle save button update + }); + } + + /* password */ + { + //TODO: On save let the user retype his password? + const container = tag.find(".virtualserver_password"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PASSWORD).granted(1); + + container.on('change', event => { + const password = container.val() as string; + properties.virtualserver_flag_password = !!password; + if(properties.virtualserver_flag_password) { + helpers.hashPassword(password).then(pass => properties.virtualserver_password = pass); + } + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* slots */ + { + const container_max = tag.find(".virtualserver_maxclients"); + const container_reserved = tag.find(".virtualserver_reserved_slots"); + + /* max users */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_MAXCLIENTS).granted(1); + + container_max.on('change', event => { + properties.virtualserver_maxclients = parseInt(container_max.val() as string); + + const invalid = properties.virtualserver_maxclients < 1 || properties.virtualserver_maxclients > 1024; + container_max.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_maxclients", !invalid); + + container_reserved.trigger('change'); /* update the flag */ + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* reserved */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS).granted(1); + + container_reserved.on('change', event => { + properties.virtualserver_reserved_slots = parseInt(container_reserved.val() as string); + + const invalid = properties.virtualserver_reserved_slots > properties.virtualserver_maxclients; + container_reserved.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_reserved_slots", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + } + + /* Welcome message */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE).granted(1); + const container = tag.find(".container-welcome-message"); + const input = container.find("textarea"); + + const insert_tag = (open: string, close: string) => { + if(input.prop("disabled")) + return; + + const node = input[0] as HTMLTextAreaElement; + if (node.selectionStart || node.selectionStart == 0) { + const startPos = node.selectionStart; + const endPos = node.selectionEnd; + node.value = node.value.substring(0, startPos) + open + node.value.substring(startPos, endPos) + close + node.value.substring(endPos); + node.selectionEnd = endPos + open.length; + node.selectionStart = node.selectionEnd; + } else { + node.value += open + close; + node.selectionEnd = node.value.length - close.length; + node.selectionStart = node.selectionEnd; + } + + input.focus().trigger('change'); + }; + + input.on('change', event => { + console.log(tr("Welcome message edited: %o"), input.val()); + properties.virtualserver_welcomemessage = input.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + container.find(".button-bold").on('click', () => insert_tag('[b]', '[/b]')); + container.find(".button-italic").on('click', () => insert_tag('[i]', '[/i]')); + container.find(".button-underline").on('click', () => insert_tag('[u]', '[/u]')); + container.find(".button-color input").on('change', event => { + insert_tag('[color=' + (event.target as HTMLInputElement).value + ']', '[/color]') + }); + } + } + + function apply_network_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void, modal: Modal) { + /* binding */ + { + /* host */ + { + const container = tag.find(".virtualserver_host"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOST).granted(1); + + container.on('change', event => { + properties.virtualserver_host = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_host)); + } + + /* port */ + { + const container = tag.find(".virtualserver_port"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PORT).granted(1); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_port = value; + + const valid = value >= 1 && value < 65536; + callback_valid("virtualserver_port", valid); + container.firstParent(".input-boxed").toggleClass("is-invalid", !valid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_port)); + } + + /* TeamSpeak server list */ + { + const container = tag.find(".virtualserver_weblist_enabled"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WEBLIST).granted(1); + + container.on('change', event => { + properties.virtualserver_weblist_enabled = container.prop("checked"); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".checkbox").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.prop("checked", server.properties.virtualserver_weblist_enabled)); + } + } + + /* file download */ + { + /* bandwidth */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1); + const container = tag.find(".virtualserver_max_download_total_bandwidth"); + + container.on('change', event => { + properties.virtualserver_max_download_total_bandwidth = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_max_download_total_bandwidth)); + } + + /* Quota */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1); + const container = tag.find(".virtualserver_download_quota"); + + container.on('change', event => { + properties.virtualserver_download_quota = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_download_quota)); + } + } + + /* file upload */ + { + /* bandwidth */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1); + const container = tag.find(".virtualserver_max_upload_total_bandwidth"); + + container.on('change', event => { + properties.virtualserver_max_upload_total_bandwidth = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_max_upload_total_bandwidth)); + } + + /* Quota */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1); + const container = tag.find(".virtualserver_upload_quota"); + + container.on('change', event => { + properties.virtualserver_upload_quota = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_upload_quota)); + } + } + + const format_unit = (value: number) => { + const KB = 1024; + const MB = 1024 * KB; + const GB = 1024 * MB; + const TB = 1024 * GB; + + let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); + + let v, unit; + if(value > 2 * TB) { + unit = "TB"; + v = value / TB; + } else if(value > GB) { + unit = "GB"; + v = value / GB; + } else if(value > MB) { + unit = "MB"; + v = value / MB; + } else if(value > KB) { + unit = "KB"; + v = value / KB; + } else { + unit = ""; + v = value; + } + return points + " Bytes" + (unit ? (" / " + v.toFixed(2) + " " + unit) : ""); }; - tag.find(".virtualserver_name").change(function (this: HTMLInputElement) { - properties.virtualserver_name = this.value; + /* quota info */ + { + server.updateProperties().then(() => { + tag.find(".value.virtualserver_month_bytes_downloaded").text(format_unit(server.properties.virtualserver_month_bytes_downloaded)); + tag.find(".value.virtualserver_month_bytes_uploaded").text(format_unit(server.properties.virtualserver_month_bytes_uploaded)); - $(this).removeClass("input_error"); - if(this.value.length < 1 || this.value.length > 70) - $(this).addClass("input_error"); - updateButton(); - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1)); + tag.find(".value.virtualserver_total_bytes_downloaded").text(format_unit(server.properties.virtualserver_total_bytes_downloaded)); + tag.find(".value.virtualserver_total_bytes_uploaded").text(format_unit(server.properties.virtualserver_total_bytes_uploaded)); + }); + } - tag.find(".virtualserver_name_phonetic").change(function (this: HTMLInputElement) { - properties.virtualserver_name_phonetic = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NAME).granted(1)); + /* quota update task */ + if(server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CONNECTIONINFO_VIEW).granted(1)) { + const month_bytes_downloaded = tag.find(".value.virtualserver_month_bytes_downloaded")[0]; + const month_bytes_uploaded = tag.find(".value.virtualserver_month_bytes_uploaded")[0]; + const total_bytes_downloaded = tag.find(".value.virtualserver_total_bytes_downloaded")[0]; + const total_bytes_uploaded = tag.find(".value.virtualserver_total_bytes_uploaded")[0]; - tag.find(".virtualserver_password").change(function (this: HTMLInputElement) { - properties.virtualserver_flag_password = this.value.length != 0; - if(properties.virtualserver_flag_password) - helpers.hashPassword(this.value).then(pass => properties.virtualserver_password = pass); + let id = setInterval(() => { + if(!modal.shown) { + clearInterval(id); + return; + } - $(this).removeClass("input_error"); - if(!properties.virtualserver_flag_password) - if(connection_handler.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_MODIFY_WITH_FORCE_PASSWORD).granted(1)) - $(this).addClass("input_error"); - updateButton(); - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PASSWORD).granted(1)); + server.request_connection_info().then(info => { + if(info.connection_filetransfer_bytes_sent_month && month_bytes_downloaded) + month_bytes_downloaded.innerText = format_unit(info.connection_filetransfer_bytes_sent_month); + if(info.connection_filetransfer_bytes_received_month && month_bytes_uploaded) + month_bytes_uploaded.innerText = format_unit(info.connection_filetransfer_bytes_received_month); - - - tag.find(".virtualserver_maxclients").change(function (this: HTMLInputElement) { - properties.virtualserver_maxclients = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_MAXCLIENTS).granted(1)); - - tag.find(".virtualserver_reserved_slots").change(function (this: HTMLInputElement) { - properties.virtualserver_reserved_slots = this.valueAsNumber; - $(this).removeClass("input_error"); - if(this.valueAsNumber > properties.virtualserver_maxclients) - $(this).addClass("input_error"); - updateButton(); - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_RESERVED_SLOTS).granted(1)); - - tag.find(".virtualserver_welcomemessage").change(function (this: HTMLInputElement) { - properties.virtualserver_welcomemessage = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WELCOMEMESSAGE).granted(1)); - - tag.find(".button-select-icon").on('click', event => { - Modals.spawnIconSelect(connection_handler, id => { - const icon_node = tag.find(".button-select-icon").find(".icon-node"); - icon_node.empty(); - icon_node.each((_, e) => { $(e).append(connection_handler.fileManager.icons.generateTag(id)); }); - //icon_node.append(connection_handler.fileManager.icons.generateTag(id)); - - console.log("Selected icon ID: %d", id); - properties.virtualserver_icon_id = id; - }, server.properties.virtualserver_icon_id); - }) - } - - - function server_applyHostListener(server: ServerEntry, properties: ServerProperties, original_properties: ServerProperties, tag: JQuery, button: JQuery) { - const connection_handler = server.channelTree.client; - - tag.find(".virtualserver_host").change(function (this: HTMLInputElement) { - properties.virtualserver_host = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOST).granted(1)); - - tag.find(".virtualserver_port").change(function (this: HTMLInputElement) { - properties.virtualserver_port = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PORT).granted(1)); - - - tag.find(".virtualserver_hostmessage").change(function (this: HTMLInputElement) { - properties.virtualserver_hostmessage = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1)); - - tag.find(".virtualserver_hostmessage_mode").change(function (this: HTMLSelectElement) { - properties.virtualserver_hostmessage_mode = this.selectedIndex; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1)) - .find("option").eq(original_properties.virtualserver_hostmessage_mode).prop('selected', true); - - - - tag.find(".virtualserver_hostbanner_url").change(function (this: HTMLInputElement) { - properties.virtualserver_hostbanner_url = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1)); - - tag.find(".virtualserver_hostbanner_gfx_url").change(function (this: HTMLInputElement) { - properties.virtualserver_hostbanner_gfx_url = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1)); - - tag.find(".virtualserver_hostbanner_gfx_interval").change(function (this: HTMLInputElement) { - properties.virtualserver_hostbanner_gfx_interval = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1)); - - tag.find(".virtualserver_hostbanner_mode").change(function (this: HTMLSelectElement) { - properties.virtualserver_hostbanner_mode = this.selectedIndex; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1)) - .find("option").eq(original_properties.virtualserver_hostbanner_mode).prop('selected', true); - - tag.find(".virtualserver_hostbutton_tooltip").change(function (this: HTMLInputElement) { - properties.virtualserver_hostbutton_tooltip = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1)); - - tag.find(".virtualserver_hostbutton_url").change(function (this: HTMLInputElement) { - properties.virtualserver_hostbutton_url = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1)); - - tag.find(".virtualserver_hostbutton_gfx_url").change(function (this: HTMLInputElement) { - properties.virtualserver_hostbutton_gfx_url = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1)); - - server.updateProperties().then(() => { - tag.find(".virtualserver_host").val(server.properties.virtualserver_host); - tag.find(".virtualserver_port").val(server.properties.virtualserver_port); - }); - } - - function server_applyMessages(properties: ServerProperties, server: ServerEntry, tag: JQuery) { - const connection_handler = server.channelTree.client; - - server.updateProperties().then(() => { - tag.find(".virtualserver_default_client_description").val(server.properties.virtualserver_default_client_description); - tag.find(".virtualserver_default_channel_description").val(server.properties.virtualserver_default_channel_description); - tag.find(".virtualserver_default_channel_topic").val(server.properties.virtualserver_default_channel_topic); - }); - - tag.find(".virtualserver_default_client_description").change(function (this: HTMLInputElement) { - properties.virtualserver_default_client_description = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1)); - - tag.find(".virtualserver_default_channel_description").change(function (this: HTMLInputElement) { - properties.virtualserver_default_channel_description = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1)); - - tag.find(".virtualserver_default_channel_topic").change(function (this: HTMLInputElement) { - properties.virtualserver_default_channel_topic = this.value; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1)); - } - - function server_applyFlood(properties: ServerProperties, server: ServerEntry, tag: JQuery) { - const connection_handler = server.channelTree.client; - - server.updateProperties().then(() => { - tag.find(".virtualserver_antiflood_points_tick_reduce").val(server.properties.virtualserver_antiflood_points_tick_reduce); - tag.find(".virtualserver_antiflood_points_needed_command_block").val(server.properties.virtualserver_antiflood_points_needed_command_block); - tag.find(".virtualserver_antiflood_points_needed_ip_block").val(server.properties.virtualserver_antiflood_points_needed_ip_block); - }); - - tag.find(".virtualserver_antiflood_points_tick_reduce").change(function (this: HTMLInputElement) { - properties.virtualserver_antiflood_points_tick_reduce = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1)); - - tag.find(".virtualserver_antiflood_points_needed_command_block").change(function (this: HTMLInputElement) { - properties.virtualserver_antiflood_points_needed_command_block = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1)); - - tag.find(".virtualserver_antiflood_points_needed_ip_block").change(function (this: HTMLInputElement) { - properties.virtualserver_antiflood_points_needed_ip_block = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1)); - } - - - function server_applySecurity(properties: ServerProperties, server: ServerEntry, tag: JQuery) { - const connection_handler = server.channelTree.client; - - server.updateProperties().then(() => { - tag.find(".virtualserver_needed_identity_security_level").val(server.properties.virtualserver_needed_identity_security_level); - }); - - tag.find(".virtualserver_needed_identity_security_level").change(function (this: HTMLInputElement) { - properties.virtualserver_needed_identity_security_level = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL).granted(1)); - - tag.find(".virtualserver_codec_encryption_mode").change(function (this: HTMLSelectElement) { - properties.virtualserver_codec_encryption_mode = this.selectedIndex; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1)) - .find("option").eq(server.properties.virtualserver_codec_encryption_mode).prop('selected', true); - } - - function server_applyMisc(properties: ServerProperties, server: ServerEntry, tag: JQuery) { - const connection_handler = server.channelTree.client; - - { //TODO notify on tmp channeladmin group and vice versa - { - let groups_tag = tag.find(".default_server_group"); - groups_tag.change(function (this: HTMLSelectElement) { - properties.virtualserver_default_server_group = parseInt($(this.item(this.selectedIndex)).attr("group-id")); + if(info.connection_filetransfer_bytes_sent_total && total_bytes_downloaded) + total_bytes_downloaded.innerText = format_unit(info.connection_filetransfer_bytes_sent_total); + if(info.connection_filetransfer_bytes_received_total && total_bytes_uploaded) + total_bytes_uploaded.innerText = format_unit(info.connection_filetransfer_bytes_received_total); }); + }, 1000); + modal.close_listener.push(() => clearInterval(id)); + } + } - for(let group of server.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { + function apply_host_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + /* host message */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTMESSAGE).granted(1); + + /* message */ + { + const container = tag.find(".virtualserver_hostmessage"); + + container.on('change', event => { + properties.virtualserver_hostmessage = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* mode */ + { + const container = tag.find(".virtualserver_hostmessage_mode"); + + container.on('change', event => { + properties.virtualserver_hostmessage_mode = Math.min(3, Math.max(0, parseInt(container.val() as string))); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + } + + /* host banner */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBANNER).granted(1); + + /* URL */ + { + const container = tag.find(".virtualserver_hostbanner_url"); + + container.on('change', event => { + properties.virtualserver_hostbanner_url = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* Image URL/Image Preview */ + { + const container = tag.find(".virtualserver_hostbanner_gfx_url"); + const container_preview = tag.find(".container-host-message .container-gfx-preview img"); + + container.on('change', event => { + properties.virtualserver_hostbanner_gfx_url = container.val() as string; + container_preview.attr("src", properties.virtualserver_hostbanner_gfx_url).toggle(!!properties.virtualserver_hostbanner_gfx_url); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* Image Refresh */ + { + const container = tag.find(".virtualserver_hostbanner_gfx_interval"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_hostbanner_gfx_interval = value; + + const invalid = value < 60 && value != 0; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_hostbanner_gfx_interval", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* mode */ + { + const container = tag.find(".virtualserver_hostbanner_mode"); + + container.on('change', event => { + properties.virtualserver_hostbanner_mode = Math.min(2, Math.max(0, parseInt(container.val() as string))); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + } + + /* host button */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_HOSTBUTTON).granted(1); + + /* URL */ + { + const container = tag.find(".virtualserver_hostbutton_url"); + + container.on('change', event => { + properties.virtualserver_hostbutton_url = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* Tooltip */ + { + const container = tag.find(".virtualserver_hostbutton_tooltip"); + + container.on('change', event => { + properties.virtualserver_hostbutton_tooltip = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* Icon URL/Icon Preview */ + { + const container = tag.find(".virtualserver_hostbutton_gfx_url"); + const container_preview = tag.find(".container-host-button .container-gfx-preview img"); + + container.on('change', event => { + properties.virtualserver_hostbutton_gfx_url = container.val() as string; + container_preview.attr("src", properties.virtualserver_hostbutton_gfx_url).toggle(!!properties.virtualserver_hostbutton_gfx_url); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + } + } + + function apply_security_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + /* Anti flood */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1); + + /* reduce */ + { + const container = tag.find(".virtualserver_antiflood_points_tick_reduce"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_antiflood_points_tick_reduce = value; + + const invalid = value < 1 || value > 999999; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_antiflood_points_tick_reduce", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_tick_reduce)); + } + + /* block commands */ + { + const container = tag.find(".virtualserver_antiflood_points_needed_command_block"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_antiflood_points_needed_command_block = value; + + const invalid = value < 1 || value > 999999; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_antiflood_points_needed_command_block", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_needed_command_block)); + } + + /* block ip */ + { + const container = tag.find(".virtualserver_antiflood_points_needed_ip_block"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_antiflood_points_needed_ip_block = value; + + const invalid = value < 1 || value > 999999; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_antiflood_points_needed_ip_block", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_antiflood_points_needed_ip_block)); + } + } + + /* encryption */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CODEC_ENCRYPTION_MODE).granted(1); + const container = tag.find(".virtualserver_codec_encryption_mode"); + + container.on('change', event => { + properties.virtualserver_codec_encryption_mode = Math.min(2, Math.max(0, parseInt(container.val() as string))); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* security level */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_NEEDED_IDENTITY_SECURITY_LEVEL).granted(1); + const container = tag.find(".virtualserver_needed_identity_security_level"); + + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_needed_identity_security_level = value; + + const invalid = value < 8 || value > 99; + container.firstParent(".input-boxed").toggleClass("is-invalid", invalid); + callback_valid("virtualserver_needed_identity_security_level", !invalid); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_needed_identity_security_level)); + } + } + + function apply_messages_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MESSAGES).granted(1); + + /* channel topic */ + { + const container = tag.find(".virtualserver_default_channel_topic"); + + container.on('change', event => { + properties.virtualserver_default_channel_topic = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + + /* channel description */ + { + const container = tag.find(".virtualserver_default_channel_description"); + + container.on('change', event => { + properties.virtualserver_default_channel_description = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_default_channel_description)); + } + + /* client description */ + { + const container = tag.find(".virtualserver_default_client_description"); + + container.on('change', event => { + properties.virtualserver_default_client_description = container.val() as string; + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + server.updateProperties().then(() => container.val(server.properties.virtualserver_default_client_description)); + } + } + + function apply_misc_listener(tag: JQuery, server: ServerEntry, properties: ServerProperties, callback_valid: (key: string | undefined, flag?: boolean) => void) { + /* default groups */ + { + /* Server Group */ + { + const container = tag.find(".virtualserver_default_server_group"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_SERVERGROUP).granted(1); + + container.on('change', event => { + properties.virtualserver_default_server_group = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + for(const group of server.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { if(group.type != 2) continue; let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); if(group.id == server.properties.virtualserver_default_server_group) group_tag.prop("selected", true); - group_tag.appendTo(groups_tag); + group_tag.appendTo(container); } } - { - let groups_tag = tag.find(".default_music_group"); - groups_tag.change(function (this: HTMLSelectElement) { - properties.virtualserver_default_music_group = parseInt($(this.item(this.selectedIndex)).attr("group-id")); - }); - for(let group of server.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { + /* Music Group */ + { + const container = tag.find(".virtualserver_default_music_group"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_MUSICGROUP).granted(1); + + container.on('change', event => { + properties.virtualserver_default_music_group = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + for(const group of server.channelTree.client.groups.serverGroups.sort(GroupManager.sorter())) { if(group.type != 2) continue; let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); if(group.id == server.properties.virtualserver_default_music_group) group_tag.prop("selected", true); - group_tag.appendTo(groups_tag); + group_tag.appendTo(container); } } + /* Channel Admin Group */ { - let groups_tag = tag.find(".default_channel_group"); - groups_tag.change(function (this: HTMLSelectElement) { - properties.virtualserver_default_channel_group = parseInt($(this.item(this.selectedIndex)).attr("group-id")); - }); + const container = tag.find(".virtualserver_default_channel_admin_group"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELADMINGROUP).granted(1); - for(let group of server.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { - if(group.type != 2) continue; - let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); - if(group.id == server.properties.virtualserver_default_channel_group) - group_tag.prop("selected", true); - group_tag.appendTo(groups_tag); - } - } + container.on('change', event => { + properties.virtualserver_default_channel_admin_group = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - { - let groups_tag = tag.find(".default_channel_admin_group"); - groups_tag.change(function (this: HTMLSelectElement) { - properties.virtualserver_default_channel_admin_group = parseInt($(this.item(this.selectedIndex)).attr("group-id")); - }); - - for(let group of server.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { + for(const group of server.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { if(group.type != 2) continue; let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); if(group.id == server.properties.virtualserver_default_channel_admin_group) group_tag.prop("selected", true); - group_tag.appendTo(groups_tag); + group_tag.appendTo(container); + } + } + + /* Channel Guest Group */ + { + const container = tag.find(".virtualserver_default_channel_group"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_DEFAULT_CHANNELGROUP).granted(1); + + container.on('change', event => { + properties.virtualserver_default_channel_group = parseInt(container.val() as string); + callback_valid(undefined); //Toggle save button update + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + + for(const group of server.channelTree.client.groups.channelGroups.sort(GroupManager.sorter())) { + if(group.type != 2) continue; + let group_tag = $.spawn("option").text(group.name + " [" + (group.properties.savedb ? "perm" : "tmp") + "]").attr("group-id", group.id); + if(group.id == server.properties.virtualserver_default_channel_group) + group_tag.prop("selected", true); + group_tag.appendTo(container); } } } - server.updateProperties().then(() => { - //virtualserver_antiflood_points_needed_ip_block - //virtualserver_antiflood_points_needed_command_block - //virtualserver_antiflood_points_tick_reduce + /* complains */ + { + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1); - //virtualserver_complain_autoban_count - //virtualserver_complain_autoban_time - //virtualserver_complain_remove_time - tag.find(".virtualserver_antiflood_points_needed_ip_block").val(server.properties.virtualserver_antiflood_points_needed_ip_block); - tag.find(".virtualserver_antiflood_points_needed_command_block").val(server.properties.virtualserver_antiflood_points_needed_command_block); - tag.find(".virtualserver_antiflood_points_tick_reduce").val(server.properties.virtualserver_antiflood_points_tick_reduce); - tag.find(".virtualserver_complain_autoban_count").val(server.properties.virtualserver_complain_autoban_count); - tag.find(".virtualserver_complain_autoban_time").val(server.properties.virtualserver_complain_autoban_time); - tag.find(".virtualserver_complain_remove_time").val(server.properties.virtualserver_complain_remove_time); + /* ban threshold */ + { + const container = tag.find(".virtualserver_complain_autoban_count"); - tag.find(".virtualserver_weblist_enabled").prop("checked", server.properties.virtualserver_weblist_enabled); - }); + container.on('change', event => { + properties.virtualserver_complain_autoban_count = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - tag.find(".virtualserver_antiflood_points_needed_ip_block").change(function (this: HTMLInputElement) { - properties.virtualserver_antiflood_points_needed_ip_block = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1)); + server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_autoban_count)); + } - tag.find(".virtualserver_antiflood_points_needed_command_block").change(function (this: HTMLInputElement) { - properties.virtualserver_antiflood_points_needed_command_block = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1)); + /* ban time */ + { + const container = tag.find(".virtualserver_complain_autoban_time"); - tag.find(".virtualserver_antiflood_points_tick_reduce").change(function (this: HTMLInputElement) { - properties.virtualserver_antiflood_points_tick_reduce = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_ANTIFLOOD).granted(1)); + container.on('change', event => { + properties.virtualserver_complain_autoban_time = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_autoban_time)); + } - tag.find(".virtualserver_complain_autoban_count").change(function (this: HTMLInputElement) { - properties.virtualserver_complain_autoban_count = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1)); + /* auto remove time */ + { + const container = tag.find(".virtualserver_complain_remove_time"); - tag.find(".virtualserver_complain_autoban_time").change(function (this: HTMLInputElement) { - properties.virtualserver_complain_autoban_time = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1)); + container.on('change', event => { + properties.virtualserver_complain_remove_time = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); - tag.find(".virtualserver_complain_remove_time").change(function (this: HTMLInputElement) { - properties.virtualserver_complain_remove_time = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_COMPLAIN).granted(1)); + server.updateProperties().then(() => container.val(server.properties.virtualserver_complain_remove_time)); + } + } + /* others */ + { + /* clients before silence */ + { + const container = tag.find(".virtualserver_min_clients_in_channel_before_forced_silence"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CHANNEL_FORCED_SILENCE).granted(1); - tag.find(".virtualserver_weblist_enabled").change(function (this: HTMLInputElement) { - properties.virtualserver_weblist_enabled = $(this).prop("checked"); - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_WEBLIST).granted(1)); - } + container.on('change', event => { + const value = parseInt(container.val() as string); + properties.virtualserver_min_clients_in_channel_before_forced_silence = value; + callback_valid("virtualserver_min_clients_in_channel_before_forced_silence", value > 1); + container.firstParent(".input-boxed").toggleClass("is-invalid", value <= 1); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + server.updateProperties().then(() => container.val(server.properties.virtualserver_min_clients_in_channel_before_forced_silence)); + } - function server_applyTransferListener(properties: ServerProperties, server: ServerEntry, tag: JQuery) { - const connection_handler = server.channelTree.client; - server.updateProperties().then(() => { - //virtualserver_max_upload_total_bandwidth - //virtualserver_upload_quota - //virtualserver_max_download_total_bandwidth - //virtualserver_download_quota + /* priority speaker dim factor */ + { + const container = tag.find(".virtualserver_priority_speaker_dimm_modificator"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_PRIORITY_SPEAKER_DIMM_MODIFICATOR).granted(1); - tag.find(".virtualserver_max_upload_total_bandwidth").val(server.properties.virtualserver_max_upload_total_bandwidth); - tag.find(".virtualserver_upload_quota").val(server.properties.virtualserver_upload_quota); - tag.find(".virtualserver_max_download_total_bandwidth").val(server.properties.virtualserver_max_download_total_bandwidth); - tag.find(".virtualserver_download_quota").val(server.properties.virtualserver_download_quota); - }); + container.on('change', event => { + properties.virtualserver_priority_speaker_dimm_modificator = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + /* channel delete delay */ + { + const container = tag.find(".virtualserver_channel_temp_delete_delay_default"); + const permission = server.channelTree.client.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_CHANNEL_TEMP_DELETE_DELAY_DEFAULT).granted(1); - tag.find(".virtualserver_max_upload_total_bandwidth").change(function (this: HTMLInputElement) { - properties.virtualserver_max_upload_total_bandwidth = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1)); - tag.find(".virtualserver_max_download_total_bandwidth").change(function (this: HTMLInputElement) { - properties.virtualserver_max_download_total_bandwidth = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_SETTINGS).granted(1)); - - tag.find(".virtualserver_upload_quota").change(function (this: HTMLInputElement) { - properties.virtualserver_upload_quota = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1)); - tag.find(".virtualserver_download_quota").change(function (this: HTMLInputElement) { - properties.virtualserver_download_quota = this.valueAsNumber; - }).prop("disabled", !connection_handler.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_MODIFY_FT_QUOTAS).granted(1)); + container.on('change', event => { + properties.virtualserver_channel_temp_delete_delay_default = parseInt(container.val() as string); + callback_valid(undefined); + }).prop("disabled", !permission).firstParent(".input-boxed").toggleClass("disabled", !permission); + } + } } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalSettings.ts b/shared/js/ui/modal/ModalSettings.ts index a6605ad8..d0716347 100644 --- a/shared/js/ui/modal/ModalSettings.ts +++ b/shared/js/ui/modal/ModalSettings.ts @@ -1,898 +1,1027 @@ -/// -/// -/// - namespace Modals { - function spawnTeamSpeakIdentityImprove(identity: profiles.identities.TeaSpeakIdentity): Modal { - let modal: Modal; - let elapsed_timer: NodeJS.Timer; - - modal = createModal({ - header: tr("Improve identity"), - body: () => { - let template = $("#tmpl_settings-teamspeak_improve").renderTag(); - template = $.spawn("div").append(template); - - let active; - const button_start_stop = template.find(".button-start-stop"); - const button_close = template.find(".button-close"); - const input_current_level = template.find(".identity-level input"); - const input_target_level = template.find(".identity-target-level input"); - const input_threads = template.find(".threads input"); - const input_hash_rate = template.find(".hash-rate input"); - const input_elapsed = template.find(".time-elapsed input"); - - button_close.on('click', event => { - if (active) - button_start_stop.trigger('click'); - - if (modal.shown) - modal.close(); - }); - - button_start_stop.on('click', event => { - button_start_stop - .toggleClass('btn-success', active) - .toggleClass('btn-danger', !active) - .text(active ? tr("Start") : tr("Stop")); - - input_threads.prop("disabled", !active); - input_target_level.prop("disabled", !active); - if (active) { - input_hash_rate.val(0); - clearInterval(elapsed_timer); - active = false; - return; - } - active = true; - input_hash_rate.val("nan"); - - const threads = parseInt(input_threads.val() as string); - const target_level = parseInt(input_target_level.val() as string); - if (target_level == 0) { - identity.improve_level(-1, threads, () => active, current_level => { - input_current_level.val(current_level); - }, hash_rate => { - input_hash_rate.val(hash_rate); - }).catch(error => { - console.error(error); - createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.
Error:") + error).open(); - if (active) - button_start_stop.trigger('click'); - }); - } else { - identity.improve_level(target_level, threads, () => active, current_level => { - input_current_level.val(current_level); - }, hash_rate => { - input_hash_rate.val(hash_rate); - }).then(success => { - if (success) { - identity.level().then(level => { - input_current_level.val(level); - createInfoModal(tr("Identity successfully improved"), MessageHelper.formatMessage(tr("Identity successfully improved to level {}"), level)).open(); - }).catch(error => { - input_current_level.val("error: " + error); - }); - } - if (active) - button_start_stop.trigger('click'); - }).catch(error => { - console.error(error); - createErrorModal(tr("Failed to improve identity"), tr("Failed to improve identity.
Error:") + error).open(); - if (active) - button_start_stop.trigger('click'); - }); - } - - const begin = Date.now(); - elapsed_timer = setInterval(() => { - const time = (Date.now() - begin) / 1000; - let seconds = Math.floor(time % 60).toString(); - let minutes = Math.floor(time / 60).toString(); - - if (seconds.length < 2) - seconds = "0" + seconds; - - if (minutes.length < 2) - minutes = "0" + minutes; - - input_elapsed.val(minutes + ":" + seconds); - }, 1000); - }); - - - template.find(".identity-unique-id input").val(identity.uid()); - identity.level().then(level => { - input_current_level.val(level); - }).catch(error => { - input_current_level.val("error: " + error); - }); - return template; - }, - footer: undefined, - width: 750 - }); - modal.close_listener.push(() => modal.htmlTag.find(".button-close").trigger('click')); - modal.open(); - return modal; - } - - function spawnTeamSpeakIdentityImport(callback: (identity: profiles.identities.TeaSpeakIdentity) => any): Modal { - let modal: Modal; - let loaded_identity: profiles.identities.TeaSpeakIdentity; - - modal = createModal({ - header: tr("Import identity"), - body: () => { - let template = $("#tmpl_settings-teamspeak_import").renderTag(); - template = $.spawn("div").append(template); - - template.find(".button-load-file").on('click', event => template.find(".input-file").trigger('click')); - - const button_import = template.find(".button-import"); - const set_error = message => { - template.find(".success").hide(); - if (message) { - template.find(".error").text(message).show(); - button_import.prop("disabled", true); - } else - template.find(".error").hide(); - }; - - const import_identity = (data: string, ini: boolean) => { - profiles.identities.TeaSpeakIdentity.import_ts(data, ini).then(identity => { - loaded_identity = identity; - set_error(""); - button_import.prop("disabled", false); - template.find(".success").show(); - }).catch(error => { - set_error("Failed to load identity: " + error); - }); - }; - - { /* file select button */ - template.find(".input-file").on('change', event => { - const element = event.target as HTMLInputElement; - const file_reader = new FileReader(); - - file_reader.onload = function () { - import_identity(file_reader.result as string, true); - }; - - file_reader.onerror = ev => { - console.error(tr("Failed to read give identity file: %o"), ev); - set_error(tr("Failed to read file!")); - return; - }; - - if (element.files && element.files.length > 0) - file_reader.readAsText(element.files[0]); - }); - } - - { /* text input */ - template.find(".button-load-text").on('click', event => { - createInputModal("Import identity from text", "Please paste your idenity bellow
", text => text.length > 0 && text.indexOf('V') != -1, result => { - if (result) - import_identity(result as string, false); - }).open(); - }); - } - - button_import.on('click', event => { - modal.close(); - callback(loaded_identity); - }); - - set_error(""); - button_import.prop("disabled", true); - return template; - }, - footer: undefined, - width: 750 - }); - modal.open(); - return modal; - } - - export function spawnSettingsModal(): Modal { + export function spawnSettingsModal(default_page?: string) : Modal { let modal: Modal; modal = createModal({ header: tr("Settings"), body: () => { - let template = $("#tmpl_settings").renderTag({ - client: native_client, - valid_forum_identity: profiles.identities.valid_static_forum_identity(), - forum_path: settings.static("forum_path"), - voice_available: !settings.static_global(Settings.KEY_DISABLE_VOICE, false) - }); + const tag = $("#tmpl_settings").renderTag().dividerfy(); - initialiseVoiceListeners(modal, (template = template.tabify()).find(".settings_audio")); - initialise_translations(template.find(".settings-translations")); - initialise_profiles(modal, template.find(".settings-profiles")); - initialise_global(modal, template.find(".settings-general")); + /* general "tab" mechanic */ + const left = tag.find("> .left"); + const right = tag.find("> .right"); + { + left.find(".entry:not(.group)").on('click', event => { + const entry = $(event.target); + right.find("> .container").addClass("hidden"); + left.find(".selected").removeClass("selected"); - return template; + const target = entry.attr("container"); + console.log(target); + if(!target) return; + + right.find("> .container." + target).removeClass("hidden"); + entry.addClass("selected"); + }) + } + + /* initialize all tabs */ + + /* enable one tab */ + { + left.find(".entry[container" + (default_page ? ("='" + default_page + "'") : "") + "]").first().trigger('click'); + } + + return tag; }, - footer: undefined, - width: 750 + footer: null }); + modal.htmlTag.find(".modal-body").addClass("modal-settings"); + + settings_general_application(modal.htmlTag.find(".right .container.general-application"), modal); + settings_general_language(modal.htmlTag.find(".right .container.general-language"), modal); + settings_general_chat(modal.htmlTag.find(".right .container.general-chat"), modal); + settings_audio_microphone(modal.htmlTag.find(".right .container.audio-microphone"), modal); + settings_audio_speaker(modal.htmlTag.find(".right .container.audio-speaker"), modal); + settings_audio_sounds(modal.htmlTag.find(".right .container.audio-sounds"), modal); + const update_profiles = settings_identity_profiles(modal.htmlTag.find(".right .container.identity-profiles"), modal); + settings_identity_forum(modal.htmlTag.find(".right .container.identity-forum"), modal, update_profiles as any); + modal.open(); return modal; } - function initialise_global(modal: Modal, tag: JQuery) { - console.log(tag); - {/* setup the forum */ - const identity = profiles.identities.static_forum_identity(); - if (identity && identity.valid()) { - tag.find(".not-connected").hide(); + function settings_general_application(container: JQuery, modal: Modal) { + /* hostbanner */ + { + const option = container.find(".option-hostbanner-background") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_HOSTBANNER_BACKGROUND, option[0].checked); + for(const sc of server_connections.server_connection_handlers()) + sc.hostbanner.update(); + }).prop("checked", settings.static_global(Settings.KEY_HOSTBANNER_BACKGROUND)); + } - tag.find(".property.username .value").text(identity.name()); - const premium_tag = tag.find(".property.premium .value").text(""); - if (identity.is_stuff() || identity.is_premium()) - premium_tag.append($.spawn("div").addClass("premium").text(tr("yes"))); - else - premium_tag.append($.spawn("div").addClass("non-premium").text(tr("no"))); - } else { - tag.find(".connected").hide(); - } + /* font size */ + { + const current_size = parseInt(getComputedStyle(document.body).fontSize); //settings.static_global(Settings.KEY_FONT_SIZE, 12); + const select = container.find(".option-font-size"); - tag.find(".button-logout").on('click', event => { - if(native_client) { - modal.close(); /* we cant update the modal so we close it */ - forum.logout(); - } else { - window.location.href = settings.static("forum_path") + "auth.php?type=logout"; - } - }); - tag.find(".button-login").on('click', event => { - if(native_client) { - modal.close(); /* we cant update the modal so we close it */ - forum.open(); - } else { - window.location.href = settings.static("forum_path") + "login.php"; - } + if(select.find("option[value='" + current_size + "']").length) + select.find("option[value='" + current_size + "']").prop("selected", true); + else + select.find("option[value='-1']").prop("selected", true); + select.on('change', event => { + const value = parseInt(select.val() as string); + settings.static_global(Settings.KEY_FONT_SIZE, value); + console.log("Changed font size of %dpx", value); + + $(document.body).css("font-size", value + "px"); }); } } + function settings_general_language(container: JQuery, modal: Modal) { - let vad_mapping = { - "threshold": "vad", - "push_to_talk": "ppt", - "active": "pt" - }; + const container_entries = container.find(".container-list .entries"); - function initialiseVoiceListeners(modal: Modal, tag: JQuery) { - let currentVAD = vad_mapping[default_recorder.get_vad_type()] || "vad"; + const tag_loading = container.find(".cover-loading"); + const template = $("#settings-translations-list-entry"); - const display_error = (message: string) => { - const alert = tag.find(".settings-device-error").first(); - alert.clone() - .alert() - .css("display", "block") - .insertAfter(alert) - .find(".message") - .text(message); + const restart_hint = container.find(".restart-note").hide(); + + const display_repository_info = (repository: i18n.TranslationRepository) => { + const info_modal = createModal({ + header: tr("Repository info"), + body: () => { + return $("#settings-translations-list-entry-info").renderTag({ + type: "repository", + name: repository.name, + url: repository.url, + contact: repository.contact, + translations: repository.translations || [] + }); + }, + footer: () => { + let footer = $.spawn("div"); + footer.addClass("modal-button-group"); + footer.css("margin-top", "5px"); + footer.css("margin-bottom", "5px"); + footer.css("text-align", "right"); + + let buttonOk = $.spawn("button"); + buttonOk.text(tr("Close")); + buttonOk.click(() => info_modal.close()); + footer.append(buttonOk); + + return footer; + } + }); + info_modal.open() + }; + const display_translation_info = (translation: i18n.RepositoryTranslation, repository: i18n.TranslationRepository) => { + const info_modal = createModal({ + header: tr("Translation info"), + body: () => { + const tag = $("#settings-translations-list-entry-info").renderTag({ + type: "translation", + name: translation.name, + url: translation.path, + repository_name: repository.name, + contributors: translation.contributors || [] + }); + + tag.find(".button-info").on('click', () => display_repository_info(repository)); + + return tag; + }, + footer: () => { + let footer = $.spawn("div"); + footer.addClass("modal-button-group"); + footer.css("margin-top", "5px"); + footer.css("margin-bottom", "5px"); + footer.css("text-align", "right"); + + let buttonOk = $.spawn("button"); + buttonOk.text(tr("Close")); + buttonOk.click(() => info_modal.close()); + footer.append(buttonOk); + + return footer; + } + }); + info_modal.open() }; - if (!settings.static_global(Settings.KEY_DISABLE_VOICE, false)) { - { //Initialized voice activation detection - const vad_tag = tag.find(".settings-vad-container"); + const update_current_selected = () => { + const container_current = container.find(".selected-language"); + container_current.empty().text(tr("Loading")); - vad_tag.find('input[type=radio]').on('change', event => { - const select = event.currentTarget as HTMLSelectElement; - { - vad_tag.find(".settings-vad-impl-entry").hide(); - vad_tag.find(".setting-vad-" + select.value).show(); + let current_translation: i18n.RepositoryTranslation; + i18n.iterate_repositories(repository => { + if(current_translation) return; + for(const entry of repository.translations) + if(i18n.config.translation_config().current_translation_path == entry.path) { + current_translation = entry; + return; } + }).then(() => { + container_current.empty(); - switch (select.value) { - case "ppt": - default_recorder.set_vad_type("push_to_talk"); + const language = current_translation ? current_translation.country_code : "gb"; + $.spawn("div").addClass("country flag-" + language.toLowerCase()).attr('title', i18n.country_name(language, tr("Unknown language"))).appendTo(container_current); + $.spawn("a").text(current_translation ? current_translation.name : tr("English (Default)")).appendTo(container_current); + }).catch(error => { + /* This shall never happen */ + }); + }; - vad_tag.find(".vat_ppt_key").text(ppt.key_description(default_recorder.get_vad_ppt_key())); - vad_tag.find(".ppt-delay input").val(default_recorder.get_vad_ppt_delay()); + const initially_selected = i18n.config.translation_config().current_translation_url; + const update_list = () => { + container_entries.empty(); - break; - case "vad": - default_recorder.set_vad_type("threshold"); - - let slider = vad_tag.find(".vad_vad_slider"); - slider.val(default_recorder.get_vad_threshold()); - slider.trigger("change"); - - const filter = default_recorder.input.get_filter(audio.recorder.filter.Type.THRESHOLD) as audio.recorder.filter.ThresholdFilter; - filter.callback_level = level => vad_tag.find(".vad_vad_bar_filler").css("width", (100 - level) + "%"); - break; - - case "pt": - default_recorder.set_vad_type("active"); - break; - } + const currently_selected = i18n.config.translation_config().current_translation_url; + //Default translation + { + const tag = template.renderTag({ + type: "default", + selected: !currently_selected || currently_selected == "default" }); + tag.on('click', () => { + i18n.select_translation(undefined, undefined); + container_entries.find(".selected").removeClass("selected"); + tag.addClass("selected"); - { //Initialized push to talk - vad_tag.find(".vat_ppt_key").click(function () { - let modal = createModal({ - body: "", - header: () => { - let head = $.spawn("div"); - head.text(tr("Type the key you wish")); - head.css("background-color", "blue"); - return head; - }, - footer: "" + update_current_selected(); + restart_hint.toggle(initially_selected !== i18n.config.translation_config().current_translation_url); + }); + tag.appendTo(container_entries); + } + + { + tag_loading.show(); + i18n.iterate_repositories(repo => { + let repo_tag = container_entries.find("[repository=\"" + repo.unique_id + "\"]"); + if (repo_tag.length == 0) { + repo_tag = template.renderTag({ + type: "repository", + name: repo.name || repo.url, + id: repo.unique_id }); - let listener = (event: ppt.KeyEvent) => { - if (event.type == ppt.EventType.KEY_TYPED) { - settings.changeGlobal('vad_ppt_key', undefined); //TODO remove that because its legacy shit - console.log(tr("Got key %o"), event); + repo_tag.find(".button-delete").on('click', e => { + e.preventDefault(); - default_recorder.set_vad_ppt_key(event); + Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this repository?"), answer => { + if (answer) { + i18n.delete_repository(repo); + update_list(); + } + }); + }); + repo_tag.find(".button-info").on('click', e => { + e.preventDefault(); + display_repository_info(repo); + }); - ppt.unregister_key_listener(listener); - modal.close(); - vad_tag.find(".vat_ppt_key").text(ppt.key_description(event)); - } - }; - ppt.register_key_listener(listener); - modal.open(); - }); - - vad_tag.find(".ppt-delay input").on('change', event => { - default_recorder.set_vad_ppt_delay((event.target).valueAsNumber); - }); - } - - { //Initialized voice activation detection - let slider = vad_tag.find(".vad_vad_slider"); - slider.on("input change", () => { - settings.changeGlobal("vad_threshold", slider.val().toString()); - default_recorder.set_vad_threshold(slider.val() as number); - vad_tag.find(".vad_vad_slider_value").text(slider.val().toString()); - }); - modal.properties.registerCloseListener(() => { - const filter = default_recorder.input.get_filter(audio.recorder.filter.Type.THRESHOLD) as audio.recorder.filter.ThresholdFilter; - filter.callback_level = undefined; - }); - } - - let target_tag = vad_tag.find('input[type=radio][name="vad_type"][value="' + currentVAD + '"]'); - if (target_tag.length == 0) { - //TODO tr - console.warn("Failed to find tag for " + currentVAD + ". Using latest tag!"); - target_tag = vad_tag.find('input[type=radio][name="vad_type"]').last(); - } - target_tag.prop("checked", true); - setTimeout(() => target_tag.trigger('change'), 0); - } - - { //Initialize microphone - - const setting_tag = tag.find(".settings-microphone"); - const tag_select = setting_tag.find(".audio-select-microphone"); - - const update_devices = () => { //List devices - tag_select.empty(); - - $.spawn("option") - .attr("device-id", "") - .text(tr("No device")) - .appendTo(tag_select); - - const active_device = default_recorder.current_device(); - audio.recorder.devices().forEach(device => { - console.debug(tr("Got device %o"), device); - - $.spawn("option") - .attr("device-id", device.unique_id) - .text(device.name) - .prop("selected", active_device && device.unique_id == active_device.unique_id) - .appendTo(tag_select); - }); - if (tag_select.find("option:selected").length == 0) - tag_select.find("option").prop("selected", true); - - }; - - { - tag_select.on('change', event => { - let selected_tag = tag_select.find("option:selected"); - let deviceId = selected_tag.attr("device-id"); - console.log(tr("Selected microphone device: id: %o"), deviceId); - const device = audio.recorder.devices().find(e => e.unique_id === deviceId); - if(!device) - console.warn(tr("Failed to find device!")); - - default_recorder.set_device(device); - }); - } - - update_devices(); - setting_tag.find(".button-device-update").on('click', event => update_devices()); - } - } - - { //Initialize speaker - const setting_tag = tag.find(".settings-speaker"); - const tag_select = setting_tag.find(".audio-select-speaker"); - - const update_devices = () => { - tag_select.empty(); - - const active_device = audio.player.current_device(); - audio.player.available_devices().then(devices => { - for (const device of devices) { - $.spawn("option") - .attr("device-id", device.device_id) - .text(device.name) - .prop("selected", device.device_id == active_device.device_id) - .appendTo(tag_select); + container_entries.append(repo_tag); } - }).catch(error => { - console.error(tr("Could not enumerate over devices!")); + + for(const translation of repo.translations) { + const tag = template.renderTag({ + type: "translation", + name: translation.name || translation.path, + id: repo.unique_id, + country_code: translation.country_code, + selected: i18n.config.translation_config().current_translation_path == translation.path + }); + tag.find(".button-info").on('click', e => { + e.preventDefault(); + display_translation_info(translation, repo); + }); + tag.on('click', e => { + if (e.isDefaultPrevented()) return; + i18n.select_translation(repo, translation); + container_entries.find(".selected").removeClass("selected"); + tag.addClass("selected"); + + update_current_selected(); + restart_hint.toggle(initially_selected !== i18n.config.translation_config().current_translation_url); + }); + tag.insertAfter(repo_tag); + } + }).then(() => tag_loading.hide()).catch(error => { console.error(error); - display_error(tr("Could not get speaker device list!")); - }); - - - if (tag_select.find("option:selected").length == 0) - tag_select.find("option").prop("selected", true); - }; - - { - tag_select.on('change', event => { - let selected_tag = tag_select.find("option:selected"); - let deviceId = selected_tag.attr("device-id"); - console.log(tr("Selected speaker device: id: %o"), deviceId); - audio.player.set_device(deviceId).catch(error => { - console.error(error); - display_error(tr("Failed to change device!")); - }); - }); + /* this should NEVER happen */ + }) } - update_devices(); - setting_tag.find(".button-device-update").on('click', event => update_devices()); + }; - { /* master sound volume */ - const master_tag = setting_tag.find(".master-volume"); - master_tag.find("input").on('change input', event => { - const value = parseInt((event.target).value); - master_tag.find('a').text("(" + value + "%)"); - - if(audio.player.set_master_volume) - audio.player.set_master_volume(value / 100); - settings.changeGlobal(Settings.KEY_SOUND_MASTER, value); - }).val((audio.player.get_master_volume ? audio.player.get_master_volume() * 100 : 100).toString()).trigger('change'); - } - } - - { /* initialize sounds */ - const sound_tag = tag.find(".sound-settings"); - - { /* master sound volume */ - const master_tag = sound_tag.find(".sound-master-volume"); - master_tag.find("input").on('change input', event => { - const value = parseInt((event.target).value); - master_tag.find('a').text("(" + value + "%)"); - - sound.set_master_volume(value / 100); - settings.changeGlobal(Settings.KEY_SOUND_MASTER_SOUNDS, value); - }).val((sound.get_master_volume() * 100).toString()).trigger('change'); - } - - { - const overlap_tag = sound_tag.find(".overlap-sounds input"); - overlap_tag.on('change', event => { - const activated = (event.target).checked; - sound.set_overlap_activated(activated); - }).prop("checked", sound.overlap_activated()); - } - - { - const muted_tag = sound_tag.find(".muted-sounds input"); - muted_tag.on('change', event => { - const activated = (event.target).checked; - sound.set_ignore_output_muted(!activated); - }).prop("checked", !sound.ignore_output_muted()); - } - - { /* sound elements */ - const template_tag = $("#tmpl_settings-sound_entry"); - const entry_tag = sound_tag.find(".sound-list-entries"); - - for (const _sound in Sound) { - const sound_name = Sound[_sound]; - - console.log(sound.get_sound_volume(sound_name as Sound)); - const data = { - name: sound_name, - activated: sound.get_sound_volume(sound_name as Sound) > 0 - }; - - const entry = template_tag.renderTag(data); - entry.find("input").on('change', event => { - const activated = (event.target).checked; - console.log(tr("Sound %s had changed to %o"), sound_name, activated); - sound.set_sound_volume(sound_name as Sound, activated ? 1 : 0); - }); - - entry.find(".button-playback").on('click', event => { - sound.manager.play(sound_name as Sound); - }); - - entry_tag.append(entry); - } - - setTimeout(() => { - const entry_container = sound_tag.find(".sound-list-entries-container"); - if (entry_container.hasScrollBar()) - entry_container.addClass("scrollbar"); - }, 100); - - /* filter */ - const filter_tag = sound_tag.find(".sound-list-filter input"); - filter_tag.on('change keyup', event => { - const filter = ((event.target).value || "").toLowerCase(); - if (!filter) - entry_tag.find(".entry").show(); - else { - entry_tag.find(".entry").each((_, _entry) => { - const entry = $(_entry); - if (entry.text().toLowerCase().indexOf(filter) == -1) - entry.hide(); - else - entry.show(); - }); + /* button add repository */ + { + container.find(".button-add-repository").on('click', () => { + createInputModal(tr("Enter repository URL"), tr("Enter repository URL:"), text => { + try { + new URL(text); + return true; + } catch(error) { + return false; } - }); - } - - modal.close_listener.push(sound.save); - } - } - - function initialise_translations(tag: JQuery) { - { //Initialize the list - const tag_list = tag.find(".setting-list .list"); - const tag_loading = tag.find(".setting-list .loading"); - const template = $("#settings-translations-list-entry"); - const restart_hint = tag.find(".setting-list .restart-note"); - restart_hint.hide(); - - const update_list = () => { - tag_list.empty(); - - const currently_selected = i18n.config.translation_config().current_translation_url; - { //Default translation - const tag = template.renderTag({ - type: "default", - selected: !currently_selected || currently_selected == "default" - }); - tag.on('click', () => { - i18n.select_translation(undefined, undefined); - tag_list.find(".selected").removeClass("selected"); - tag.addClass("selected"); - - restart_hint.show(); - }); - tag.appendTo(tag_list); - } - - { - const display_repository_info = (repository: i18n.TranslationRepository) => { - const info_modal = createModal({ - header: tr("Repository info"), - body: () => { - return $("#settings-translations-list-entry-info").renderTag({ - type: "repository", - name: repository.name, - url: repository.url, - contact: repository.contact, - translations: repository.translations || [] - }); - }, - footer: () => { - let footer = $.spawn("div"); - footer.addClass("modal-button-group"); - footer.css("margin-top", "5px"); - footer.css("margin-bottom", "5px"); - footer.css("text-align", "right"); - - let buttonOk = $.spawn("button"); - buttonOk.text(tr("Close")); - buttonOk.click(() => info_modal.close()); - footer.append(buttonOk); - - return footer; - } - }); - info_modal.open() - }; + }, url => { + if (!url) return; tag_loading.show(); - i18n.iterate_repositories(repo => { - let repo_tag = tag_list.find("[repository=\"" + repo.unique_id + "\"]"); - if (repo_tag.length == 0) { - repo_tag = template.renderTag({ - type: "repository", - name: repo.name || repo.url, - id: repo.unique_id - }); - - repo_tag.find(".button-delete").on('click', e => { - e.preventDefault(); - - Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this repository?"), answer => { - if (answer) { - i18n.delete_repository(repo); - update_list(); - } - }); - }); - repo_tag.find(".button-info").on('click', e => { - e.preventDefault(); - - display_repository_info(repo); - }); - - tag_list.append(repo_tag); - } - - for(const translation of repo.translations) { - const tag = template.renderTag({ - type: "translation", - name: translation.name || translation.path, - id: repo.unique_id, - country_code: translation.country_code, - selected: i18n.config.translation_config().current_translation_path == translation.path - }); - tag.find(".button-info").on('click', e => { - e.preventDefault(); - - const info_modal = createModal({ - header: tr("Translation info"), - body: () => { - const tag = $("#settings-translations-list-entry-info").renderTag({ - type: "translation", - name: translation.name, - url: translation.path, - repository_name: repo.name, - contributors: translation.contributors || [] - }); - - tag.find(".button-info").on('click', () => display_repository_info(repo)); - - return tag; - }, - footer: () => { - let footer = $.spawn("div"); - footer.addClass("modal-button-group"); - footer.css("margin-top", "5px"); - footer.css("margin-bottom", "5px"); - footer.css("text-align", "right"); - - let buttonOk = $.spawn("button"); - buttonOk.text(tr("Close")); - buttonOk.click(() => info_modal.close()); - footer.append(buttonOk); - - return footer; - } - }); - info_modal.open() - }); - tag.on('click', e => { - if (e.isDefaultPrevented()) return; - i18n.select_translation(repo, translation); - tag_list.find(".selected").removeClass("selected"); - tag.addClass("selected"); - - restart_hint.show(); - }); - tag.insertAfter(repo_tag); - } - }).then(() => tag_loading.hide()).catch(error => { - console.error(error); - /* this should NEVER happen */ + i18n.load_repository(url as string).then(repository => { + i18n.register_repository(repository); + update_list(); + }).catch(error => { + tag_loading.hide(); + createErrorModal("Failed to load repository", tr("Failed to query repository.
Ensure that this repository is valid and reachable.
Error: ") + error).open(); }) - } + }).open(); + }); + } - }; - - { - tag.find(".button-add-repository").on('click', () => { - createInputModal("Enter URL", tr("Enter repository URL:
"), text => true, url => { //FIXME test valid url - if (!url) return; - - tag_loading.show(); - i18n.load_repository(url as string).then(repository => { - i18n.register_repository(repository); - update_list(); - }).catch(error => { - tag_loading.hide(); - createErrorModal("Failed to load repository", tr("Failed to query repository.
Ensure that this repository is valid and reachable.
Error: ") + error).open(); - }) - }).open(); - }); - } - - restart_hint.find(".button-reload").on('click', () => { + container.find(".button-restart").on('click', () => { + if(app.is_web()) { location.reload(); + } else { + createErrorModal(tr("Not implemented"), tr("Client restart isn't implemented.
Please do it manually!")).open(); + } + }); + + update_list(); + update_current_selected(); + } + + function settings_general_chat(container: JQuery, modal: Modal) { + /* timestamp format */ + { + const option_fixed = container.find(".option-fixed-timestamps") as JQuery; + const option_colloquial = container.find(".option-colloquial-timestamps") as JQuery; + + option_colloquial.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS, option_colloquial[0].checked); }); - update_list(); + option_fixed.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_FIXED_TIMESTAMPS, option_fixed[0].checked); + option_colloquial + .prop("disabled", option_fixed[0].checked) + .parents("label").toggleClass("disabled", option_fixed[0].checked); + if(option_fixed[0].checked) { + option_colloquial.prop("checked", false); + } else { + option_colloquial.prop("checked", settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS)); + } + }).prop("checked", settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)).trigger('change'); + } + + { + const option = container.find(".option-instant-channel-switch") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_SWITCH_INSTANT_CHAT, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)); + } + { + const option = container.find(".option-instant-client-switch") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_SWITCH_INSTANT_CLIENT, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)); + } + { + const option = container.find(".option-colored-emojies") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_COLORED_EMOJIES, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)); + } + + { + const option = container.find(".option-support-markdown") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_ENABLE_MARKDOWN, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN)); + } + { + const option = container.find(".option-url-tagging") as JQuery; + option.on('change', event => { + settings.changeGlobal(Settings.KEY_CHAT_TAG_URLS, option[0].checked); + }).prop("checked", settings.static_global(Settings.KEY_CHAT_TAG_URLS)); } } - function initialise_profiles(modal: Modal, tag: JQuery) { - const settings_tag = tag.find(".profile-settings"); - let selected_profile: profiles.ConnectionProfile; - let nickname_listener: () => any; - let status_listener: () => any; + function settings_audio_microphone(container: JQuery, modal: Modal) { + let _callbacks_filter_change: (() => any)[] = []; - const display_settings = (profile: profiles.ConnectionProfile) => { - selected_profile = profile; + /* devices */ + { + const container_devices = container.find(".container-devices"); - settings_tag.find(".setting-name").val(profile.profile_name); - settings_tag.find(".setting-default-nickname").val(profile.default_username); - settings_tag.find(".setting-default-password").val(profile.default_password); + let level_meters: audio.recorder.LevelMeter[] = []; + modal.close_listener.push(() => { + for(const meter of level_meters) + meter.destory(); + level_meters = []; + }); + const update_devices = () => { + container_devices.children().remove(); + for(const meter of level_meters) + meter.destory(); + level_meters = []; - { - //change listener - const select_tag = settings_tag.find(".select-container select")[0] as HTMLSelectElement; - const type = profile.selected_identity_type.toLowerCase(); + const current_selected = default_recorder.current_device(); + const generate_device = (device: audio.recorder.InputDevice | undefined) => { + const selected = device === current_selected || (typeof(current_selected) !== "undefined" && typeof(device) !== "undefined" && current_selected.unique_id == device.unique_id); - select_tag.onchange = () => { - console.log("Selected: " + select_tag.value); - settings_tag.find(".identity-settings.active").removeClass("active"); - settings_tag.find(".identity-settings-" + select_tag.value).addClass("active"); + let tag_volume: JQuery, tag_volume_error: JQuery; + const tag = $.spawn("div").addClass("device").toggleClass("selected", selected).append( + $.spawn("div").addClass("container-selected").append( + $.spawn("div").addClass("icon_em client-apply") + ), + $.spawn("div").addClass("container-name").append( + $.spawn("div").addClass("device-driver").text( + device ? (device.driver || "Unknown driver") : "No device" + ), + $.spawn("div").addClass("device-name").text( + device ? (device.name || "Unknown name") : "No device" + ), + ), + $.spawn("div").addClass("container-activity").append( + $.spawn("div").addClass("container-activity-bar").append( + tag_volume = $.spawn("div").addClass("bar-hider"), + tag_volume_error = $.spawn("div").addClass("bar-error") + ) + ) + ); - profile.selected_identity_type = select_tag.value.toLowerCase(); - const selected_type = profile.selected_type(); - const identity = profile.selected_identity(); + tag.on('click', event => { + if(tag.hasClass("selected")) + return; - profiles.mark_need_save(); + const _old = container_devices.find(".selected"); + _old.removeClass("selected"); + tag.addClass("selected"); - let tag: JQuery; - if (selected_type == profiles.identities.IdentitifyType.TEAFORO) { - const forum_tag = tag = settings_tag.find(".identity-settings-teaforo"); + default_recorder.set_device(device).then(() => { + console.debug(tr("Changed default microphone device")); + for(const cb of _callbacks_filter_change) + cb(); + }).catch((error) => { + _old.addClass("selected"); + tag.removeClass("selected"); - forum_tag.find(".connected, .disconnected").hide(); - if (identity && identity.valid()) { - forum_tag.find(".connected").show(); - } else { - forum_tag.find(".disconnected").show(); - } - } else if (selected_type == profiles.identities.IdentitifyType.TEAMSPEAK) { - console.log("Set: " + identity); - const teamspeak_tag = tag = settings_tag.find(".identity-settings-teamspeak"); - teamspeak_tag.find(".identity_string").val(""); - if (identity) - (identity as profiles.identities.TeaSpeakIdentity).export_ts().then(e => teamspeak_tag.find(".identity_string").val(e)); - } else if (selected_type == profiles.identities.IdentitifyType.NICKNAME) { - const name_tag = tag = settings_tag.find(".identity-settings-nickname"); - if (identity) - name_tag.find("input").val(identity.name()); - else - name_tag.find("input").val(""); + console.error(tr("Failed to change microphone to device %o: %o"), device, error); + createErrorModal(tr("Failed to change microphone"), MessageHelper.formatMessage(tr("Failed to change the microphone to the target microphone{:br:}{}"), error)).open(); + }); + }); + + tag_volume.css('width', '100%'); + if(device) { + audio.recorder.create_levelmeter(device).then(meter => { + level_meters.push(meter); + meter.set_observer(value => { + tag_volume.css('width', (100 - value) + '%'); + }); + }).catch(error => { + console.warn(tr("Failed to generate levelmeter for device %o: %o"), device, error); + tag_volume_error.attr('title', error).text(error); + }); } - if (tag) - tag.trigger('show'); + return tag; }; - select_tag.value = type; - select_tag.onchange(undefined); - } - }; + generate_device(undefined).appendTo(container_devices); + audio.recorder.devices().forEach(e => generate_device(e).appendTo(container_devices)); + }; + update_devices(); - const update_profile_list = () => { - const profile_list = tag.find(".profile-list .list").empty(); - const profile_template = $("#settings-profile-list-entry"); - for (const profile of profiles.profiles()) { - const list_tag = profile_template.renderTag({ - profile_name: profile.profile_name, - id: profile.id - }); + const button_update = container.find(".button-update"); + button_update.on('click', async event => { + button_update.prop("disabled", true); + if(audio.recorder.device_refresh_available()) { + try { + await audio.recorder.refresh_devices(); + } catch(error) { + console.warn(tr("Failed to refresh input devices: %o"), error); + } + } + try { + update_devices(); + } catch(error) { + console.error(tr("Failed to build new device list: %o"), error); + } + button_update.prop("disabled", false); + }); + } - const profile_status_update = () => { - list_tag.find(".status").hide(); - if (profile.valid()) - list_tag.find(".status-valid").show(); - else - list_tag.find(".status-invalid").show(); - }; - list_tag.on('click', event => { - /* update ui */ - profile_list.find(".selected").removeClass("selected"); - list_tag.addClass("selected"); - - if (profile == selected_profile) return; - nickname_listener = () => list_tag.find(".name").text(profile.profile_name); - status_listener = profile_status_update; - - display_settings(profile); - }); - - - profile_list.append(list_tag); - if ((!selected_profile && profile.id == "default") || selected_profile == profile) - setTimeout(() => list_tag.trigger('click'), 1); - profile_status_update(); - } - }; - - const display_error = (error?: string) => { - if (error) { - settings_tag.find(".settings-profile-error").show().find(".message").html(error); - } else - settings_tag.find(".settings-profile-error").hide(); - status_listener(); - }; - - /* identity settings */ + /* settings */ { - { //TeamSpeak change listener - const teamspeak_tag = settings_tag.find(".identity-settings-teamspeak"); - const identity_info_tag = teamspeak_tag.find(".identity-info"); - const button_export = teamspeak_tag.find(".button-export"); - const button_import = teamspeak_tag.find(".button-import"); - const button_generate = teamspeak_tag.find(".button-generate"); - const button_improve = teamspeak_tag.find(".button-improve"); + /* volume */ + { + const container_volume = container.find(".container-volume"); + const slider = container_volume.find(".container-slider"); + sliderfy(slider, { + min_value: 0, + max_value: 100, + step: 1, + initial_value: default_recorder.get_volume() + }); + slider.on('change', event => { + const value = parseInt(slider.attr("value")); + default_recorder.set_volume(value); + }); + } - button_import.on('click', event => { - const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; + /* vad select */ + { + const container_select = container.find(".container-select-vad"); + container_select.find("input").on('change', event => { + if(!(event.target).checked) + return; - const set_identity = (identity: profiles.identities.TeaSpeakIdentity) => { - selected_profile.set_identity(profiles.identities.IdentitifyType.TEAMSPEAK, identity); - teamspeak_tag.trigger('show'); - createInfoModal(tr("Identity imported"), tr("Your identity has been successfully imported!")).open(); + const mode = (event.target).value; + if(mode == "active") + default_recorder.set_vad_type("active"); + else if(mode == "threshold") + default_recorder.set_vad_type("threshold"); + else + default_recorder.set_vad_type("push_to_talk"); + + for(const cb of _callbacks_filter_change) + cb(); + }); + + let elements = container_select.find('input[value="' + default_recorder.get_vad_type() + '"]'); + if(elements.length < 1) + elements = container_select.find('input[value]'); + elements.first().trigger('click'); + } + + /* Sensitivity */ + { + const container_sensitivity = container.find(".container-sensitivity"); + + const container_bar = container_sensitivity.find(".container-activity-bar"); + const bar_hider = container_bar.find(".bar-hider"); + + sliderfy(container_bar, { + min_value: 0, + max_value: 100, + step: 1, + initial_value: default_recorder.get_vad_threshold() + }); + container_bar.on('change', event => { + const threshold = parseInt(container_bar.attr("value")); + default_recorder.set_vad_threshold(threshold); + }); + + const _set_level = level => { + bar_hider.css("width", (100 - level) + "%"); + }; + + let _last_filter: audio.recorder.filter.ThresholdFilter; + modal.close_listener.push(() => { + if(_last_filter) { + _last_filter.callback_level = undefined; + _last_filter = undefined; + } + }); + _callbacks_filter_change.push(() => { + container_sensitivity.toggleClass("disabled", default_recorder.get_vad_type() !== "threshold"); + + if(_last_filter) { + _last_filter.callback_level = undefined; + _last_filter = undefined; + } + + if(default_recorder.get_vad_type() !== "threshold") { + container_sensitivity.addClass("disabled"); + return; + } + container_sensitivity.removeClass("disabled"); + + _set_level(0); + if(!default_recorder.input) + return; + + if(default_recorder.input.current_state() === audio.recorder.InputState.PAUSED) + default_recorder.input.start().then(result => { + if(result === audio.recorder.InputStartResult.EOK) { + for(const cb of _callbacks_filter_change) + cb(); + } + }); /* for us to show the VAD */ + + const filter = default_recorder.input.get_filter(audio.recorder.filter.Type.THRESHOLD) as audio.recorder.filter.ThresholdFilter; + if(!filter) + return; + + _last_filter = filter; + filter.callback_level = _set_level; + }); + } + + /* push to talk */ + { + /* PPT Key */ + { + const button_key = container.find(".container-ppt button"); + _callbacks_filter_change.push(() => { + button_key.prop('disabled', default_recorder.get_vad_type() !== "push_to_talk"); + }); + + button_key.on('click', event => { + Modals.spawnKeySelect(key => { + if(!key) + return; + default_recorder.set_vad_ppt_key(key); + button_key.text(ppt.key_description(key)); + }); + }); + + button_key.text(ppt.key_description(default_recorder.get_vad_ppt_key())); + } + + /* Delay */ + { + const container_delay = container.find(".container-ppt-delay"); + const input_time = container_delay.find("input.delay-time"); + const input_enabled = container_delay.find("input.delay-enabled"); + + input_enabled.on('change', event => { + const enabled = input_enabled.prop("checked"); + if(enabled) { + if(default_recorder.get_vad_type() === "push_to_talk") + input_time.prop("disabled", false).parent().removeClass("disabled"); + default_recorder.set_vad_ppt_delay(Math.abs(default_recorder.get_vad_ppt_delay())); + } else { + input_time.prop("disabled", true).parent().addClass("disabled"); + default_recorder.set_vad_ppt_delay(-Math.abs(default_recorder.get_vad_ppt_delay())); + } + }); + + input_time.on('change', event => { + const value = parseFloat(input_time.val() as any); + default_recorder.set_vad_ppt_delay(value * 1000); + }).val(Math.abs(default_recorder.get_vad_ppt_delay() / 1000).toFixed(2)); + + input_enabled.prop("checked", default_recorder.get_vad_ppt_delay() >= 0); + + _callbacks_filter_change.push(() => { + let enabled = default_recorder.get_vad_type() === "push_to_talk"; + input_enabled.prop("disabled", !enabled).parent().toggleClass("disabled", !enabled); + + enabled = enabled && input_enabled.prop("checked"); + input_time.prop("disabled", !enabled).parent().toggleClass("disabled", !enabled); + }); + } + + //delay-time + } + } + + for(const cb of _callbacks_filter_change) + cb(); + } + + function settings_audio_speaker(container: JQuery, modal: Modal) { + /* devices */ + { + const container_devices = container.find(".left .container-devices"); + const contianer_error = container.find(".left .container-error"); + + const update_devices = () => { + container_devices.children().remove(); + + const current_selected = audio.player.current_device(); + const generate_device = (device: audio.player.Device | undefined) => { + const selected = device === current_selected || (typeof(current_selected) !== "undefined" && typeof(device) !== "undefined" && current_selected.device_id == device.device_id); + + const tag = $.spawn("div").addClass("device").toggleClass("selected", selected).append( + $.spawn("div").addClass("container-selected").append( + $.spawn("div").addClass("icon_em client-apply") + ), + $.spawn("div").addClass("container-name").append( + $.spawn("div").addClass("device-driver").text( + device ? (device.driver || "Unknown driver") : "No device" + ), + $.spawn("div").addClass("device-name").text( + device ? (device.name || "Unknown name") : "No device" + ) + ) + ); + + tag.on('click', event => { + if(tag.hasClass("selected")) + return; + + const _old = container_devices.find(".selected"); + _old.removeClass("selected"); + tag.addClass("selected"); + + audio.player.set_device(device ? device.device_id : null).then(() => { + console.debug(tr("Changed default speaker device")); + }).catch((error) => { + _old.addClass("selected"); + tag.removeClass("selected"); + + console.error(tr("Failed to change speaker to device %o: %o"), device, error); + createErrorModal(tr("Failed to change speaker"), MessageHelper.formatMessage(tr("Failed to change the speaker device to the target speaker{:br:}{}"), error)).open(); + }); + }); + + return tag; + }; + + generate_device(undefined).appendTo(container_devices); + audio.player.available_devices().then(result => { + contianer_error.text("").hide(); + result.forEach(e => generate_device(e).appendTo(container_devices)); + }).catch(error => { + if(typeof(error) === "string") + contianer_error.text(error).show(); + + console.log(tr("Failed to query available speaker devices: %o"), error); + contianer_error.text(tr("Errors occurred (View console)")).show(); + }); + }; + update_devices(); + + const button_update = container.find(".button-update"); + button_update.on('click', async event => { + button_update.prop("disabled", true); + try { + update_devices(); + } catch(error) { + console.error(tr("Failed to build new speaker device list: %o"), error); + } + button_update.prop("disabled", false); + }); + } + + /* slider */ + { + + { + const container_master = container.find(".container-volume-master"); + const slider = container_master.find(".container-slider"); + sliderfy(slider, { + min_value: 0, + max_value: 100, + step: 1, + initial_value: settings.static_global(Settings.KEY_SOUND_MASTER, 100), + value_field: [container_master.find(".container-value")] + }); + slider.on('change', event => { + const volume = parseInt(slider.attr('value')); + + if(audio.player.set_master_volume) + audio.player.set_master_volume(volume / 100); + settings.changeGlobal(Settings.KEY_SOUND_MASTER, volume); + }); + } + + { + const container_soundpack = container.find(".container-volume-soundpack"); + const slider = container_soundpack.find(".container-slider"); + sliderfy(slider, { + min_value: 0, + max_value: 100, + step: 1, + initial_value: settings.static_global(Settings.KEY_SOUND_MASTER_SOUNDS, 100), + value_field: [container_soundpack.find(".container-value")] + }); + slider.on('change', event => { + const volume = parseInt(slider.attr('value')); + sound.set_master_volume(volume / 100); + settings.changeGlobal(Settings.KEY_SOUND_MASTER_SOUNDS, volume); + }); + } + } + + /* button test sound */ + { + container.find(".button-test-sound").on('click', event => { + sound.manager.play(Sound.SOUND_TEST, { + default_volume: 1, + ignore_muted: true, + ignore_overlap: true + }) + }); + } + } + + function settings_audio_sounds(contianer: JQuery, modal: Modal) { + /* initialize sound list */ + { + const container_sounds = contianer.find(".container-sounds"); + + const generate_sound = (_sound: Sound) => { + let tag_play_pause: JQuery, tag_play: JQuery, tag_pause: JQuery, tag_input_muted: JQuery; + let tag = $.spawn("div").addClass("sound").append( + tag_play_pause = $.spawn("div").addClass("container-button-play_pause").append( + tag_play = $.spawn("img").attr("src", "img/icon_sound_play.svg"), + tag_pause = $.spawn("img").attr("src", "img/icon_sound_pause.svg") + ), + $.spawn("div").addClass("container-name").text(_sound), + $.spawn("label").addClass("container-button-toggle").append( + $.spawn("div").addClass("switch").append( + tag_input_muted = $.spawn("input").attr("type", "checkbox"), + $.spawn("span").addClass("slider").append( + $.spawn("div").addClass("dot") + ) + ) + ) + ); + + tag_play_pause.on('click', event => { + if(tag_pause.is(":visible")) + return; + tag_play.hide(); + tag_pause.show(); + + const _done = flag => { + tag_pause.hide(); + tag_play.show(); + }; + const _timeout = setTimeout(() => _done(false), 10 * 1000); /* the sounds are not longer than 10 seconds */ + + sound.manager.play(_sound, { + ignore_overlap: true, + ignore_muted: true, + default_volume: 1, + + callback: flag => { + clearTimeout(_timeout); + _done(flag); + } + }); + }); + tag_pause.hide(); + + tag_input_muted.prop("checked", sound.get_sound_volume(_sound, 1) > 0); + tag_input_muted.on('change', event => { + const volume = tag_input_muted.prop("checked") ? 1 : 0; + sound.set_sound_volume(_sound, volume); + console.log(tr("Changed sound volume to %o for sound %o"), volume, _sound); + }); + + return tag; + }; + + //container-sounds + for(const sound_key in Sound) + generate_sound(Sound[sound_key as any] as any).appendTo(container_sounds); + + /* the filter */ + const input_filter = contianer.find(".input-sounds-filter"); + input_filter.on('change keyup', event => { + const filter = input_filter.val() as string; + + container_sounds.find(".sound").each((_, _element) => { + const element = $(_element); + element.toggle(filter.length == 0 || element.text().toLowerCase().indexOf(filter) !== -1); + }) + }); + } + + const overlap_tag = contianer.find(".option-overlap-same"); + overlap_tag.on('change', event => { + const activated = (event.target).checked; + sound.set_overlap_activated(activated); + }).prop("checked", sound.overlap_activated()); + + const mute_tag = contianer.find(".option-mute-output"); + mute_tag.on('change', event => { + const activated = (event.target).checked; + sound.set_ignore_output_muted(!activated); + }).prop("checked", !sound.ignore_output_muted()); + + modal.close_listener.push(sound.save); + } + + type SelectedIdentity = { + identity: profiles.ConnectionProfile; + + update_name(text?: string); + update_valid_flag(); + update_type(); + + update_avatar(); + } + function settings_identity_profiles(container: JQuery, modal: Modal) { + let selected_profile: SelectedIdentity; + let selected_profile_changed: (() => void)[] = []; + let profile_identity_changed: (() => void)[] = []; + + let update_profiles: (selected_id: string) => void; + + /* profile list */ + { + const container_profiles = container.find(".container-profiles"); + + const build_profile = (profile: profiles.ConnectionProfile, selected: boolean) => { + let tag_name: JQuery, tag_default: JQuery, tag_valid: JQuery, tag_type: JQuery, tag_avatar: JQuery; + let tag = $.spawn("div").addClass("profile").append( + tag_avatar = $.spawn("div").addClass("container-avatar"), + $.spawn("div").addClass("container-info").append( + $.spawn("div").addClass("container-type").append( + tag_type = $.spawn("div").text(profile.selected_identity_type || tr("Type unset")), + tag_default = $.spawn("div").addClass("tag-default").text(tr("(Default)")), + tag_valid = $.spawn("div").addClass("icon_em icon-status") + .toggleClass("client-apply", profile.valid()) + .toggleClass("client-delete", !profile.valid()) + ), + tag_name = $.spawn("div").addClass("profile-name").text(profile.profile_name || tr("Unnamed")) + ) + ); + tag_avatar.hide(); /* no avatars yet */ + + tag_default.toggle(profile.id === "default"); + tag.on('click', event => { + if(tag.hasClass('selected')) + return; + container_profiles.find(".selected").removeClass("selected"); + tag.addClass("selected"); + + /* reset profile name if may in change */ + if(selected_profile) + selected_profile.update_name(); + + selected_profile = { + identity: profile, + update_name(text) { + tag_name.text(typeof(text) === "string" ? text : (profile.profile_name || tr("Unnamed"))) + }, + update_type() { + tag_type.text(profile.selected_identity_type || tr("Type unset")); + }, + update_valid_flag() { + tag_valid + .toggleClass("client-apply", profile.valid()) + .toggleClass("client-delete", !profile.valid()) + }, + update_avatar() { + //TODO HERE! + } }; - if (profile && profile.valid()) { - spawnYesNo(tr("Are you sure"), tr("Do you really want to import a new identity and override the old identity?"), result => { - if (result) - spawnTeamSpeakIdentityImport(set_identity); - }); - } else - spawnTeamSpeakIdentityImport(set_identity); + for(const listener of selected_profile_changed) + listener(); }); - button_export.on('click', event => { - const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; + + if(selected) + tag.trigger('click'); + + return tag; + }; + + update_profiles = (selected_id) => { + selected_id = selected_id || "default"; + container_profiles.children().remove(); + profiles.profiles().forEach(e => build_profile(e, e.id == selected_id).appendTo(container_profiles)); + + }; + + } + + /* profile general info */ + { + const input_name = container.find(".right input.profile-name"); + const input_default_name = container.find(".right input.profile-default-name"); + const select_type = container.find(".right select.profile-identity-type"); + + selected_profile_changed.push(() => { + //profile-identity-type + if(!selected_profile.identity) { + input_name.val(tr("No profile selected")).prop("disabled", true); + input_default_name.val("").prop("disabled", true); + select_type.val("unset").prop("disabled", true); + select_type.parent().toggleClass("is-invalid", true); + } else { + input_name.val(selected_profile.identity.profile_name).prop("disabled", false); + input_default_name.val(selected_profile.identity.default_username).prop("disabled", false); + select_type.val(selected_profile.identity.selected_identity_type || "unset").prop("disabled", false); + } + + for(const listener of profile_identity_changed) + listener(); + }); + + input_name.on('keyup', event => { + const text = input_name.val() as string; + const profile = profiles.find_profile_by_name(text); + input_name.parent().toggleClass("is-invalid", text.length < 3 || (profile && profile != selected_profile.identity)); + selected_profile.update_name(text); + }).on('change', event => { + const text = input_name.val() as string; + const profile = profiles.find_profile_by_name(text); + if(text.length < 3 || (profile && profile != selected_profile.identity)) return; + selected_profile.identity.profile_name = text; + profiles.mark_need_save(); + }); + + input_default_name.on('change', event => { + selected_profile.identity.default_username = input_default_name.val() as string; + profiles.mark_need_save(); + }); + + select_type.on('change', event => { + selected_profile.identity.selected_identity_type = (select_type.val() as string).toLowerCase(); + profiles.mark_need_save(); + + selected_profile.update_type(); + for(const listener of profile_identity_changed) + listener(); + selected_profile.update_valid_flag(); + }); + + profile_identity_changed.push(() => { + select_type.parent() + .toggleClass("is-invalid", typeof(profiles.identities.IdentitifyType[selected_profile.identity.selected_identity_type.toUpperCase()]) === "undefined"); + }); + } + + /* profile special info */ + { + /* teamspeak */ + { + const container_settings = container.find(".container-teamspeak"); + const container_valid = container_settings.find(".container-valid"); + const container_invalid = container_settings.find(".container-invalid"); + + const input_current_level = container_settings.find(".current-level"); + const input_unique_id = container_settings.find(".unique-id"); + + const button_new = container_settings.find(".button-new"); + const button_improve = container_settings.find(".button-improve"); + + const button_import = container_settings.find(".button-import"); + const button_export = container_settings.find(".button-export"); + + button_improve.on('click', event => { + const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; if (!profile) return; - createInputModal(tr("File name"), tr("Please enter the file name"), text => !!text, name => { - if (name) { - profile.export_ts(true).then(data => { - const element = $.spawn("a") - .text("donwload") - .attr("href", "data:test/plain;charset=utf-8," + encodeURIComponent(data)) - .attr("download", name + ".ini") - .css("display", "none") - .appendTo($("body")); - element[0].click(); - element.detach(); - }).catch(error => { - console.error(error); - createErrorModal(tr("Failed to export identity"), tr("Failed to export and save identity.
Error: ") + error).open(); - }); - } - }).open(); + Modals.spawnTeamSpeakIdentityImprove(profile).close_listener.push(() => { + profiles.mark_need_save(); + for(const listener of profile_identity_changed) + listener(); + }); }); - button_generate.on('click', event => { - const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; + button_new.on('click', event => { + const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; const generate_identity = () => { profiles.identities.TeaSpeakIdentity.generate_new().then(identity => { - selected_profile.set_identity(profiles.identities.IdentitifyType.TEAMSPEAK, identity); - teamspeak_tag.trigger('show'); - createInfoModal(tr("Identity generate"), tr("A new identity had been successfully generated")).open(); + selected_profile.identity.set_identity(profiles.identities.IdentitifyType.TEAMSPEAK, identity); + createInfoModal(tr("Identity generated"), tr("A new identity had been successfully generated")).open(); + + profiles.mark_need_save(); + for(const listener of profile_identity_changed) + listener(); }).catch(error => { console.error(tr("Failed to generate a new identity. Error object: %o"), error); createErrorModal(tr("Failed to generate identity"), tr("Failed to generate a new identity.
Error:") + error).open(); @@ -908,150 +1037,312 @@ namespace Modals { generate_identity(); }); - button_improve.on('click', event => { - const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; - if (!profile) return; + button_import.on('click', event => { + const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; - spawnTeamSpeakIdentityImprove(profile).close_listener.push(() => teamspeak_tag.trigger('show')); - }); + const set_identity = (identity: profiles.identities.TeaSpeakIdentity) => { + selected_profile.identity.set_identity(profiles.identities.IdentitifyType.TEAMSPEAK, identity); + createInfoModal(tr("Identity imported"), tr("Your identity has been successfully imported!")).open(); - /* updates the data */ - teamspeak_tag.on('show', event => { - const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; + profiles.mark_need_save(); + for(const listener of profile_identity_changed) + listener(); + }; - if (!profile || !profile.valid()) { - identity_info_tag.hide(); - teamspeak_tag.find(".identity-undefined").show(); - button_export.prop("disabled", true); + if (profile && profile.valid()) { + spawnYesNo(tr("Are you sure"), tr("Do you really want to import a new identity and override the old identity?"), result => { + if (result) + spawnTeamSpeakIdentityImport(set_identity); + }); } else { - identity_info_tag.show(); - teamspeak_tag.find(".identity-undefined").hide(); - button_export.prop("disabled", false); - - identity_info_tag.find(".unique-id input").val(profile.uid()); - const input_level = identity_info_tag.find(".level input").val("loading..."); - profile.level().then(level => input_level.val(level.toString())).catch(error => input_level.val("error: " + error)); + spawnTeamSpeakIdentityImport(set_identity); } - display_error(); + }); + + button_export.on('click', event => { + const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; + if(!profile) return; + + createInputModal(tr("File name"), tr("Please enter the file name"), text => !!text, name => { + if (name) { + profile.export_ts(true).then(data => { + const element = $.spawn("a") + .text("donwload") + .attr("href", "data:test/plain;charset=utf-8," + encodeURIComponent(data)) + .attr("download", name + ".ini") + .css("display", "none") + .appendTo($("body")); + element[0].click(); + element.remove(); + }).catch(error => { + console.error(error); + createErrorModal(tr("Failed to export identity"), tr("Failed to export and save identity.
Error: ") + error).open(); + }); + } + }).open(); + }); + + profile_identity_changed.push(() => { + const enabled = selected_profile && selected_profile.identity.selected_identity_type === "teamspeak"; + container_settings.toggle(enabled); + if(!enabled) return; + + const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.TEAMSPEAK) as profiles.identities.TeaSpeakIdentity; + button_improve.prop("disabled", !profile); + button_export.toggle(!!profile); + + button_import.toggleClass("btn-danger", !!profile).toggleClass("btn-success", !profile); + button_new.toggleClass("btn-danger", !!profile).toggleClass("btn-success", !profile); + + container_invalid.toggle(!profile); + container_valid.toggle(!!profile); + if (!profile) { + input_current_level.val("no profile"); + input_unique_id.val("no profile"); + } else { + input_current_level.val("loading...."); + profile.level().then(level => input_current_level.val(level + "")); + input_unique_id.val(profile.uid()); + } + + selected_profile.update_valid_flag(); }); } - { //The - const teaforo_tag = settings_tag.find(".identity-settings-teaforo"); - if (native_client) { - teaforo_tag.find(".native-teaforo-login").on('click', event => { - setTimeout(() => { - const call = () => { - if (modal.shown) { - display_settings(selected_profile); - status_listener(); - } - }; - forum.register_callback(call); - forum.open(); - }, 0); - }); - } + /* teaspeak forum */ + { + const container_settings = container.find(".container-teaforo"); + const continer_valid = container_settings.find(".container-valid"); + const continer_invalid = container_settings.find(".container-invalid"); - teaforo_tag.on('show', event => { - display_error(); - /* clear error */ + const button_setup = container_settings.find(".button-setup"); + + profile_identity_changed.push(() => { + container_settings.toggle(selected_profile && selected_profile.identity.selected_identity_type === "teaforo"); + const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.TEAFORO) as profiles.identities.TeaForumIdentity; + const valid = profile && profile.valid(); + + continer_valid.toggle(valid); + continer_invalid.toggle(!valid); + }); + + button_setup.on('click', event => { + if(app.is_web()) { + modal.htmlTag.find('.entry[container="identity-forum"]').trigger('click'); + } else { + const call = () => { + if (modal.shown) + update_profiles(selected_profile ? selected_profile.identity.id : undefined); + }; + forum.register_callback(call); + forum.open(); + } }); } - { //The name - const name_tag = settings_tag.find(".identity-settings-nickname"); - name_tag.find(".setting-name").on('change keyup', event => { - const name = name_tag.find(".setting-name").val() as string; - selected_profile.set_identity(profiles.identities.IdentitifyType.NICKNAME, new profiles.identities.NameIdentity(name)); - profiles.mark_need_save(); + /* nickname */ + { + const container_settings = container.find(".container-nickname"); + const input_nickname = container_settings.find(".nickname"); - if (name.length < 3) { - display_error("Name must be at least 3 characters long!"); + profile_identity_changed.push(() => { + const active = selected_profile && selected_profile.identity.selected_identity_type === "nickname"; + container_settings.toggle(active); + if(!active) return; + + let profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.NICKNAME) as profiles.identities.NameIdentity; + if(!profile) + selected_profile.identity.set_identity(profiles.identities.IdentitifyType.NICKNAME, profile = new profiles.identities.NameIdentity()); + input_nickname.val(profile.name()).trigger('change'); + }); + + input_nickname.on('keydown', event => { + const profile = selected_profile.identity.selected_identity(profiles.identities.IdentitifyType.NICKNAME) as profiles.identities.NameIdentity; + if(!profile) return; - } - display_error(); - }); - name_tag.on('show', event => { - const profile = selected_profile.selected_identity(profiles.identities.IdentitifyType.NICKNAME); - if (!profile) - display_error("invalid profile"); - else if (!profile.valid()) - display_error("Name must be at least 3 characters long!"); - else - display_error(); + profile.set_name(input_nickname.val() as string); + profiles.mark_need_save(); + + selected_profile.update_valid_flag(); + input_nickname.parent().toggleClass('is-invalid', !profile.valid()); }); } } - /* general settings */ + /* change avatar button */ { - settings_tag.find(".setting-name").on('change', event => { - const value = settings_tag.find(".setting-name").val() as string; - if (value && selected_profile) { - selected_profile.profile_name = value; - if (nickname_listener) - nickname_listener(); - profiles.mark_need_save(); - status_listener(); - } - }); - settings_tag.find(".setting-default-nickname").on('change', event => { - const value = settings_tag.find(".setting-default-nickname").val() as string; - if (value && selected_profile) { - selected_profile.default_username = value; - profiles.mark_need_save(); - status_listener(); - } - }); - settings_tag.find(".setting-default-password").on('change', event => { - const value = settings_tag.find(".setting-default-password").val() as string; - if (value && selected_profile) { - selected_profile.default_username = value; - profiles.mark_need_save(); - status_listener(); - } - }); + container.find(".button-change-avatar").hide(); } - /* general buttons */ + /* create new button */ { - tag.find(".button-add-profile").on('click', event => { - createInputModal(tr("Please enter a name"), tr("Please enter a name for the new profile:
"), text => text.length > 0 && !profiles.find_profile_by_name(text), value => { + container.find(".button-create").on('click', event => { + createInputModal(tr("Please enter a name"), tr("Please enter a name for the new profile:"), text => text.length >= 3 && !profiles.find_profile_by_name(text), value => { if (value) { - display_settings(profiles.create_new_profile(value as string)); - update_profile_list(); + const profile = profiles.create_new_profile(value as string); + update_profiles(profile.id); profiles.mark_need_save(); } }).open(); }); + } - tag.find(".button-set-default").on('click', event => { - if (selected_profile && selected_profile.id != 'default') { - profiles.set_default_profile(selected_profile); - update_profile_list(); - profiles.mark_need_save(); - } + /* set as default button */ + { + const button = container.find(".button-set-default"); + button.on('click', event => { + profiles.set_default_profile(selected_profile.identity); + profiles.mark_need_save(); + update_profiles(selected_profile.identity.id); }); - tag.find(".button-delete").on('click', event => { - if (selected_profile && selected_profile.id != 'default') { - event.preventDefault(); + selected_profile_changed.push(() => { + button.prop("disabled", !selected_profile || selected_profile.identity.id === "default"); + }); + } + + /* delete button */ + { + const button = container.find(".button-delete"); + button.on('click', event => { + if (selected_profile && selected_profile.identity.id != 'default') { spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this profile?"), result => { if (result) { - profiles.delete_profile(selected_profile); - update_profile_list(); + profiles.delete_profile(selected_profile.identity); + profiles.mark_need_save(); + update_profiles(undefined); } }); } }); + + selected_profile_changed.push(() => { + button.prop("disabled", !selected_profile || selected_profile.identity.id === "default"); + }); } + update_profiles(undefined); + modal.close_listener.push(() => { - if (profiles.requires_save()) + if(profiles.requires_save()) profiles.save(); }); - update_profile_list(); + + return update_profiles; + } + + function settings_identity_forum(container: JQuery, modal: Modal, update_profiles: () => any) { + const containers_connected = container.find(".show-connected"); + const containers_disconnected = container.find(".show-disconnected"); + + const update_state = () => { + const logged_in = forum.logged_in(); + containers_connected.toggle(logged_in); + containers_disconnected.toggle(!logged_in); + + if(logged_in) { + container.find(".forum-username").text(forum.data().name()); + container.find(".forum-premium").text(forum.data().is_premium() ? tr("Yes") : tr("No")); + } + }; + + /* login */ + { + const button_login = container.find(".button-login"); + const input_username = container.find(".input-username"); + const input_password = container.find(".input-password"); + const container_error = container.find(".container-login .container-error"); + + const container_captcha_g = container.find(".g-recaptcha"); + let captcha: boolean | string = false; + + const update_button_state = () => { + let enabled = true; + enabled = enabled && !!input_password.val(); + enabled = enabled && !!input_username.val(); + enabled = enabled && (typeof(captcha) === "boolean" ? !captcha : !!captcha); + button_login.prop("disabled", !enabled); + }; + + /* username */ + input_username.on('change keyup', update_button_state); + + /* password */ + input_password.on('change keyup', update_button_state); + + button_login.on('click', event => { + input_username.prop("disabled", true); + input_password.prop("disabled", true); + button_login.prop("disabled", true); + container_error.removeClass("shown"); + + forum.login(input_username.val() as string, input_password.val() as string, typeof(captcha) === "string" ? captcha : undefined).then(state => { + captcha = false; + + console.debug(tr("Forum login result: %o"), state); + if(state.status === "success") { + update_state(); + update_profiles(); + return; + } + + setTimeout(() => { + if(!!state.error_message) /* clear password if we have an error */ + input_password.val(""); + input_password.focus(); + update_button_state(); + }, 0); + if(state.status === "captcha") { + //TODO Works currently only with localhost! + button_login.hide(); + container_error.text(state.error_message || tr("Captcha required")).addClass("shown"); + + captcha = ""; + + console.log(tr("Showing captcha for site-key: %o"), state.captcha.data); + forum.gcaptcha.spawn(container_captcha_g, state.captcha.data, token => { + captcha = token; + console.debug(tr("Got captcha token: %o"), token); + container_captcha_g.hide(); + button_login.show(); + update_button_state(); + }).catch(error => { + console.error(tr("Failed to initialize forum captcha: %o"), error); + container_error.text("Failed to initialize GReCaptcha! No authentication possible.").addClass("shown"); + container_captcha_g.hide(); + button_login.hide(); + }); + container_captcha_g.show(); + } else { + container_error.text(state.error_message || tr("Unknown error")).addClass("shown"); + } + }).catch(error => { + console.error(tr("Failed to login within the forum. Error: %o"), error); + createErrorModal(tr("Forum login failed."), tr("Forum login failed. Lookup the console for more information")).open(); + }).then(() => { + input_username.prop("disabled", false); + input_password.prop("disabled", false); + update_button_state(); + }); + }); + update_button_state(); + } + + /* logout */ + { + container.find(".button-logout").on('click', event => { + forum.logout().catch(error => { + console.error(tr("Failed to logout from forum: %o"), error); + createErrorModal(tr("Forum logout failed"), MessageHelper.formatMessage(tr("Failed to logout from forum account.{:br:}Error: {}"), error)).open(); + }).then(() => { + if (modal.shown) + update_state(); + update_profiles(); + }); + }); + } + + update_state(); } } \ No newline at end of file diff --git a/shared/js/ui/modal/permission/CanvasPermissionEditor.ts b/shared/js/ui/modal/permission/CanvasPermissionEditor.ts new file mode 100644 index 00000000..79158ee3 --- /dev/null +++ b/shared/js/ui/modal/permission/CanvasPermissionEditor.ts @@ -0,0 +1,1616 @@ +/* Canvas Permission Editor */ +namespace pe { + namespace ui { + export namespace scheme { + export interface CheckBox { + border: string; + checkmark: string; + checkmark_font: string; + + background_checked: string; + background_checked_hovered: string; + + background: string; + background_hovered: string; + } + + export interface TextField { + color: string; + font: string; + + background: string; + background_hovered: string; + } + + export interface ColorScheme { + permission: { + background: string; + background_selected: string; + + name: string; + name_unset: string; + name_font: string; + + value: TextField; + value_b: CheckBox; + granted: TextField; + negate: CheckBox; + skip: CheckBox; + } + + group: { + name: string; + name_font: string; + } + } + } + + export enum RepaintMode { + NONE, + REPAINT, + REPAINT_OBJECT_FULL, + REPAINT_FULL + } + + export interface AxisAlignedBoundingBox { + x: number; + y: number; + + width: number; + height: number; + } + + export enum ClickEventType { + SIGNLE, + DOUBLE, + CONTEXT_MENU + } + + export interface InteractionClickEvent { + type: ClickEventType; + consumed: boolean; + offset_x: number; + offset_y: number; + } + + export interface InteractionListener { + region: AxisAlignedBoundingBox; + region_weight: number; + + /** + * @return true if a redraw is required + */ + on_mouse_enter?: () => RepaintMode; + + /** + * @return true if a redraw is required + */ + on_mouse_leave?: () => RepaintMode; + + /** + * @return true if a redraw is required + */ + on_click?: (event: InteractionClickEvent) => RepaintMode; + + mouse_cursor?: string; + + set_full_draw?: () => any; + disabled?: boolean; + } + + abstract class DrawableObject { + abstract draw(context: CanvasRenderingContext2D, full: boolean); + + private _object_full_draw = false; + private _width: number = 0; + + set_width(value: number) { + this._width = value; + } + + request_full_draw() { + this._object_full_draw = true; + } + + pop_full_draw() { + const result = this._object_full_draw; + this._object_full_draw = false; + return result; + } + + width() { + return this._width; + } + + abstract height(); + + private _transforms: DOMMatrix[] = []; + + protected push_transform(context: CanvasRenderingContext2D) { + this._transforms.push(context.getTransform()); + } + + protected pop_transform(context: CanvasRenderingContext2D) { + const transform = this._transforms.pop(); + context.setTransform( + transform.a, + transform.b, + transform.c, + transform.d, + transform.e, + transform.f + ); + } + + protected original_x(context: CanvasRenderingContext2D, x: number) { + return context.getTransform().e + x; + } + + protected original_y(context: CanvasRenderingContext2D, y: number) { + return context.getTransform().f + y; + } + + protected colors: scheme.ColorScheme = {} as any; + + set_color_scheme(scheme: scheme.ColorScheme) { + this.colors = scheme; + } + + protected manager: PermissionEditor; + + set_manager(manager: PermissionEditor) { + this.manager = manager; + } + + abstract initialize(); + + abstract finalize(); + } + + class PermissionGroup extends DrawableObject { + public static readonly HEIGHT = parseFloat(getComputedStyle(document.documentElement).fontSize) * (3 / 2); /* 24 */ + public static readonly ARROW_SIZE = 10; /* 12 */ + + group: GroupedPermissions; + _sub_elements: PermissionGroup[] = []; + _element_permissions: PermissionList; + + collapsed = false; + private _listener_colaps: InteractionListener; + + constructor(group: GroupedPermissions) { + super(); + + this.group = group; + + this._element_permissions = new PermissionList(this.group.permissions); + for (const sub of this.group.children) + this._sub_elements.push(new PermissionGroup(sub)); + } + + draw(context: CanvasRenderingContext2D, full: boolean) { + const _full = this.pop_full_draw() || full; + this.push_transform(context); + context.translate(PermissionGroup.ARROW_SIZE + 20, PermissionGroup.HEIGHT); + + let sum_height = 0; + /* let first draw the elements, because if the sum height is zero then we could hide ourselves */ + if (!this.collapsed) { /* draw the next groups */ + for (const group of this._sub_elements) { + group.draw(context, full); + + const height = group.height(); + sum_height += height; + context.translate(0, height); + } + + this._element_permissions.draw(context, full); + if (sum_height == 0) + sum_height += this._element_permissions.height(); + } else { + const process_group = (group: PermissionGroup) => { + for (const g of group._sub_elements) + process_group(g); + group._element_permissions.handle_hide(); + if (sum_height == 0 && group._element_permissions.height() > 0) { + sum_height = 1; + } + }; + process_group(this); + } + this.pop_transform(context); + + if (_full && sum_height > 0) { + const arrow_stretch = 2 / 3; + if (!full) { + context.clearRect(0, 0, this.width(), PermissionGroup.HEIGHT); + } + context.fillStyle = this.colors.group.name; + + /* arrow */ + { + const x1 = this.collapsed ? PermissionGroup.ARROW_SIZE * arrow_stretch / 2 : 0; + const y1 = (PermissionGroup.HEIGHT - PermissionGroup.ARROW_SIZE) / 2 + (this.collapsed ? 0 : PermissionGroup.ARROW_SIZE * arrow_stretch / 2); /* center arrow */ + + const x2 = this.collapsed ? x1 + PermissionGroup.ARROW_SIZE * arrow_stretch : x1 + PermissionGroup.ARROW_SIZE / 2; + const y2 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE / 2 : y1 + PermissionGroup.ARROW_SIZE * arrow_stretch; + + const x3 = this.collapsed ? x1 : x1 + PermissionGroup.ARROW_SIZE; + const y3 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE : y1; + + context.beginPath(); + context.moveTo(x1, y1); + + context.lineTo(x2, y2); + context.lineTo(x3, y3); + + context.moveTo(x2, y2); + context.lineTo(x3, y3); + context.fill(); + + this._listener_colaps.region.x = this.original_x(context, 0); + this._listener_colaps.region.y = this.original_y(context, y1); + } + /* text */ + { + context.font = this.colors.group.name_font; + context.textBaseline = "middle"; + context.textAlign = "start"; + + context.fillText(this.group.group.name, PermissionGroup.ARROW_SIZE + 5, PermissionGroup.HEIGHT / 2); + } + } + } + + set_width(value: number) { + super.set_width(value); + for (const element of this._sub_elements) + element.set_width(value - PermissionGroup.ARROW_SIZE - 20); + this._element_permissions.set_width(value - PermissionGroup.ARROW_SIZE - 20); + } + + set_color_scheme(scheme: scheme.ColorScheme) { + super.set_color_scheme(scheme); + for (const child of this._sub_elements) + child.set_color_scheme(scheme); + this._element_permissions.set_color_scheme(scheme); + } + + set_manager(manager: PermissionEditor) { + super.set_manager(manager); + for (const child of this._sub_elements) + child.set_manager(manager); + this._element_permissions.set_manager(manager); + } + + height() { + let result = 0; + + if (!this.collapsed) { + for (const element of this._sub_elements) + result += element.height(); + + result += this._element_permissions.height(); + } else { + //We've to figure out if we have permissions + const process_group = (group: PermissionGroup) => { + if (result == 0 && group._element_permissions.height() > 0) { + result = 1; + } else { + for (const g of group._sub_elements) + process_group(g); + } + }; + process_group(this); + + if (result > 0) + return PermissionGroup.HEIGHT; + + return 0; + } + if (result > 0) { + result += PermissionGroup.HEIGHT; + return result; + } else { + return 0; + } + } + + initialize() { + for (const child of this._sub_elements) + child.initialize(); + this._element_permissions.initialize(); + + + this._listener_colaps = { + region: { + x: 0, + y: 0, + height: PermissionGroup.ARROW_SIZE, + width: PermissionGroup.ARROW_SIZE + }, + region_weight: 10, + /* + on_mouse_enter: () => { + this.collapsed_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.collapsed_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + */ + on_click: () => { + this.collapsed = !this.collapsed; + return RepaintMode.REPAINT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + + this.manager.intercept_manager().register_listener(this._listener_colaps); + } + + finalize() { + for (const child of this._sub_elements) + child.finalize(); + this._element_permissions.finalize(); + } + + collapse_group() { + for (const child of this._sub_elements) + child.collapse_group(); + + this.collapsed = true; + } + + expend_group() { + for (const child of this._sub_elements) + child.expend_group(); + + this.collapsed = false; + } + } + + class PermissionList extends DrawableObject { + permissions: PermissionEntry[] = []; + + constructor(permissions: PermissionInfo[]) { + super(); + + for (const permission of permissions) + this.permissions.push(new PermissionEntry(permission)); + } + + set_width(value: number) { + super.set_width(value); + for (const entry of this.permissions) + entry.set_width(value); + } + + + draw(context: CanvasRenderingContext2D, full: boolean) { + this.push_transform(context); + + for (const permission of this.permissions) { + permission.draw(context, full); + context.translate(0, permission.height()); + } + + this.pop_transform(context); + } + + height() { + let height = 0; + for (const permission of this.permissions) + height += permission.height(); + return height; + } + + + set_color_scheme(scheme: scheme.ColorScheme) { + super.set_color_scheme(scheme); + for (const entry of this.permissions) + entry.set_color_scheme(scheme); + } + + set_manager(manager: PermissionEditor) { + super.set_manager(manager); + + for (const entry of this.permissions) + entry.set_manager(manager); + } + + initialize() { + for (const entry of this.permissions) + entry.initialize(); + } + + finalize() { + for (const entry of this.permissions) + entry.finalize(); + } + + handle_hide() { + for (const entry of this.permissions) + entry.handle_hide(); + } + } + + class PermissionEntry extends DrawableObject { + public static readonly HEIGHT = PermissionGroup.HEIGHT; /* 24 */ + public static readonly HALF_HEIGHT = PermissionEntry.HEIGHT / 2; + public static readonly CHECKBOX_HEIGHT = PermissionEntry.HEIGHT - 2; + + public static readonly COLUMN_PADDING = 2; + public static readonly COLUMN_VALUE = 75; + public static readonly COLUMN_GRANTED = 75; + //public static readonly COLUMN_NEGATE = 25; + //public static readonly COLUMN_SKIP = 25; + public static readonly COLUMN_NEGATE = 75; + public static readonly COLUMN_SKIP = 75; + + private _permission: PermissionInfo; + + hidden: boolean; + + granted: number = 22; + value: number; + flag_skip: boolean = true; + flag_negate: boolean; + + private _prev_selected = false; + selected: boolean; + + flag_skip_hovered = false; + flag_negate_hovered = false; + flag_value_hovered = false; + flag_grant_hovered = false; + + private _listener_checkbox_skip: InteractionListener; + private _listener_checkbox_negate: InteractionListener; + private _listener_value: InteractionListener; + private _listener_grant: InteractionListener; + private _listener_general: InteractionListener; + private _icon_image: HTMLImageElement | undefined; + + on_icon_select?: (current_id: number) => Promise; + on_context_menu?: (x: number, y: number) => any; + on_grant_change?: () => any; + on_change?: () => any; + + constructor(permission: PermissionInfo) { + super(); + this._permission = permission; + } + + set_icon_id_image(image: HTMLImageElement | undefined) { + if (this._icon_image === image) + return; + this._icon_image = image; + if (image) { + image.height = 16; + image.width = 16; + } + } + + permission() { + return this._permission; + } + + draw(ctx: CanvasRenderingContext2D, full: boolean) { + if (!this.pop_full_draw() && !full) { /* Note: do not change this order! */ + /* test for update! */ + return; + } + if (this.hidden) { + this.handle_hide(); + return; + } + ctx.lineWidth = 1; + + /* debug box */ + if (false) { + ctx.fillStyle = "#FF0000"; + ctx.fillRect(0, 0, this.width(), PermissionEntry.HEIGHT); + ctx.fillStyle = "#000000"; + ctx.strokeRect(0, 0, this.width(), PermissionEntry.HEIGHT); + } + + if (!full) { + const off = this.selected || this._prev_selected ? ctx.getTransform().e : 0; + ctx.clearRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); + } + + if (this.selected) + ctx.fillStyle = this.colors.permission.background_selected; + else + ctx.fillStyle = this.colors.permission.background; + const off = this.selected ? ctx.getTransform().e : 0; + ctx.fillRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); + this._prev_selected = this.selected; + + /* permission name */ + { + ctx.fillStyle = typeof (this.value) !== "undefined" ? this.colors.permission.name : this.colors.permission.name_unset; + ctx.textBaseline = "middle"; + ctx.textAlign = "start"; + ctx.font = this.colors.permission.name_font; + + ctx.fillText(this._permission.name, 0, PermissionEntry.HALF_HEIGHT); + } + + const original_y = this.original_y(ctx, 0); + const original_x = this.original_x(ctx, 0); + const width = this.width(); + + /* draw granted */ + let w = width - PermissionEntry.COLUMN_GRANTED; + if (typeof (this.granted) === "number") { + this._listener_grant.region.x = original_x + w; + this._listener_grant.region.y = original_y; + + this._draw_number_field(ctx, this.colors.permission.granted, w, 0, PermissionEntry.COLUMN_VALUE, this.granted, this.flag_grant_hovered); + } else { + this._listener_grant.region.y = original_y; + this._listener_grant.region.x = + original_x + + width + - PermissionEntry.COLUMN_GRANTED; + } + + /* draw value and the skip stuff */ + if (typeof (this.value) === "number") { + w -= PermissionEntry.COLUMN_SKIP + PermissionEntry.COLUMN_PADDING; + { + const x = w + (PermissionEntry.COLUMN_SKIP - PermissionEntry.CHECKBOX_HEIGHT) / 2; + const y = 1; + + this._listener_checkbox_skip.region.x = original_x + x; + this._listener_checkbox_skip.region.y = original_y + y; + + this._draw_checkbox_field(ctx, this.colors.permission.skip, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_skip, this.flag_skip_hovered); + } + + w -= PermissionEntry.COLUMN_NEGATE + PermissionEntry.COLUMN_PADDING; + { + const x = w + (PermissionEntry.COLUMN_NEGATE - PermissionEntry.CHECKBOX_HEIGHT) / 2; + const y = 1; + + this._listener_checkbox_negate.region.x = original_x + x; + this._listener_checkbox_negate.region.y = original_y + y; + + this._draw_checkbox_field(ctx, this.colors.permission.negate, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_negate, this.flag_negate_hovered); + } + + w -= PermissionEntry.COLUMN_VALUE + PermissionEntry.COLUMN_PADDING; + if (this._permission.is_boolean()) { + const x = w + PermissionEntry.COLUMN_VALUE - PermissionEntry.CHECKBOX_HEIGHT; + const y = 1; + + this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT; + this._listener_value.region.x = original_x + x; + this._listener_value.region.y = original_y + y; + + this._draw_checkbox_field(ctx, this.colors.permission.value_b, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.value > 0, this.flag_value_hovered); + } else if (this._permission.name === "i_icon_id" && this._icon_image) { + this._listener_value.region.x = original_x + w; + this._listener_value.region.y = original_y; + this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT; + + this._draw_icon_field(ctx, this.colors.permission.value_b, w, 0, PermissionEntry.COLUMN_VALUE, this.flag_value_hovered, this._icon_image); + } else { + this._listener_value.region.width = PermissionEntry.COLUMN_VALUE; + this._listener_value.region.x = original_x + w; + this._listener_value.region.y = original_y; + + this._draw_number_field(ctx, this.colors.permission.value, w, 0, PermissionEntry.COLUMN_VALUE, this.value, this.flag_value_hovered); + } + this._listener_value.disabled = false; + } else { + this._listener_checkbox_skip.region.y = -1e8; + this._listener_checkbox_negate.region.y = -1e8; + + this._listener_value.region.y = original_y; + this._listener_value.region.x = + original_x + + width + - PermissionEntry.COLUMN_GRANTED + - PermissionEntry.COLUMN_NEGATE + - PermissionEntry.COLUMN_VALUE + - PermissionEntry.COLUMN_PADDING * 4; + this._listener_value.disabled = true; + } + + this._listener_general.region.y = original_y; + this._listener_general.region.x = original_x; + } + + handle_hide() { + /* so the listener wound get triggered */ + this._listener_value.region.x = -1e8; + this._listener_grant.region.x = -1e8; + this._listener_checkbox_negate.region.x = -1e8; + this._listener_checkbox_skip.region.x = -1e8; + this._listener_general.region.x = -1e8; + } + + private _draw_icon_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, width: number, hovered: boolean, image: HTMLImageElement) { + const line = ctx.lineWidth; + ctx.lineWidth = 2; + ctx.fillStyle = scheme.border; + ctx.strokeRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2); + ctx.lineWidth = line; + + ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; + ctx.fillRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2); + + const center_y = y + PermissionEntry.HEIGHT / 2; + const center_x = x + PermissionEntry.HEIGHT / 2; + ctx.drawImage(image, center_x - image.width / 2, center_y - image.height / 2); + } + + private _draw_number_field(ctx: CanvasRenderingContext2D, scheme: scheme.TextField, x: number, y: number, width: number, value: number, hovered: boolean) { + ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; + ctx.fillRect(x, y, width, PermissionEntry.HEIGHT); + + ctx.fillStyle = scheme.color; + ctx.font = scheme.font; //Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial"; + ctx.textAlign = "start"; + ctx.fillText(value + "", x, y + PermissionEntry.HALF_HEIGHT, width); + + ctx.strokeStyle = "#6e6e6e"; + const line = ctx.lineWidth; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(x, y + PermissionEntry.HEIGHT - 2); + ctx.lineTo(x + width, y + PermissionEntry.HEIGHT - 2); + ctx.stroke(); + ctx.lineWidth = line; + } + + private _draw_checkbox_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, height: number, checked: boolean, hovered: boolean) { + ctx.fillStyle = scheme.border; + ctx.strokeRect(x, y, height, height); + + + ctx.fillStyle = checked ? + (hovered ? scheme.background_checked_hovered : scheme.background_checked) : + (hovered ? scheme.background_hovered : scheme.background); + ctx.fillRect(x + 1, y + 1, height - 2, height - 2); + + if (checked) { + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = scheme.checkmark; + ctx.font = scheme.checkmark_font; //Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial"; + ctx.fillText("✓", x + height / 2, y + height / 2); + } + } + + height() { + return this.hidden ? 0 : PermissionEntry.HEIGHT; + } + + set_width(value: number) { + super.set_width(value); + this._listener_general.region.width = value; + } + + initialize() { + this._listener_checkbox_skip = { + region: { + x: -1e8, + y: -1e8, + height: PermissionEntry.CHECKBOX_HEIGHT, + width: PermissionEntry.CHECKBOX_HEIGHT + }, + region_weight: 10, + on_mouse_enter: () => { + this.flag_skip_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.flag_skip_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_click: () => { + this.flag_skip = !this.flag_skip; + if (this.on_change) + this.on_change(); + return RepaintMode.REPAINT_OBJECT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + this._listener_checkbox_negate = { + region: { + x: -1e8, + y: -1e8, + height: PermissionEntry.CHECKBOX_HEIGHT, + width: PermissionEntry.CHECKBOX_HEIGHT + }, + region_weight: 10, + on_mouse_enter: () => { + this.flag_negate_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.flag_negate_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_click: () => { + this.flag_negate = !this.flag_negate; + if (this.on_change) + this.on_change(); + return RepaintMode.REPAINT_OBJECT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + this._listener_value = { + region: { + x: -1e8, + y: -1e8, + height: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.HEIGHT, + width: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.COLUMN_VALUE + }, + region_weight: 10, + on_mouse_enter: () => { + this.flag_value_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.flag_value_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_click: () => { + if (this._permission.is_boolean()) { + this.value = this.value > 0 ? 0 : 1; + if (this.on_change) + this.on_change(); + return RepaintMode.REPAINT_OBJECT_FULL; + } else if (this._permission.name === "i_icon_id") { + this.on_icon_select(this.value).then(value => { + this.value = value; + if (this.on_change) + this.on_change(); + }).catch(error => { + console.warn(tr("Failed to select icon: %o"), error); + }) + } else { + this._spawn_number_edit( + this._listener_value.region.x, + this._listener_value.region.y, + this._listener_value.region.width, + this._listener_value.region.height, + this.colors.permission.value, + this.value || 0, + value => { + if (typeof (value) === "number") { + this.value = value; + this.request_full_draw(); + this.manager.request_draw(false); + if (this.on_change) + this.on_change(); + } + } + ) + } + return RepaintMode.REPAINT_OBJECT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + this._listener_grant = { + region: { + x: -1e8, + y: -1e8, + height: PermissionEntry.HEIGHT, + width: PermissionEntry.COLUMN_VALUE + }, + region_weight: 10, + on_mouse_enter: () => { + this.flag_grant_hovered = true; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + this.flag_grant_hovered = false; + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_click: () => { + this._spawn_number_edit( + this._listener_grant.region.x, + this._listener_grant.region.y, + this._listener_grant.region.width, + this._listener_grant.region.height, + this.colors.permission.granted, + this.granted || 0, //TODO use max assignable value? + value => { + if (typeof (value) === "number") { + this.granted = value; + this.request_full_draw(); + this.manager.request_draw(false); + + if (this.on_grant_change) + this.on_grant_change(); + } + } + ); + return RepaintMode.REPAINT_OBJECT_FULL; + }, + set_full_draw: () => this.request_full_draw(), + mouse_cursor: "pointer" + }; + + this._listener_general = { + region: { + x: -1e8, + y: -1e8, + height: PermissionEntry.HEIGHT, + width: 0 + }, + region_weight: 0, + /* + on_mouse_enter: () => { + return RepaintMode.REPAINT_OBJECT_FULL; + }, + on_mouse_leave: () => { + return RepaintMode.REPAINT_OBJECT_FULL; + }, + */ + on_click: (event: InteractionClickEvent) => { + this.manager.set_selected_entry(this); + + if (event.type == ClickEventType.DOUBLE && typeof (this.value) === "undefined") + return this._listener_value.on_click(event); + else if (event.type == ClickEventType.CONTEXT_MENU) { + const mouse = this.manager.mouse; + if (this.on_context_menu) { + this.on_context_menu(mouse.x, mouse.y); + event.consumed = true; + } + } + return RepaintMode.NONE; + }, + set_full_draw: () => this.request_full_draw(), + }; + + this.manager.intercept_manager().register_listener(this._listener_checkbox_negate); + this.manager.intercept_manager().register_listener(this._listener_checkbox_skip); + this.manager.intercept_manager().register_listener(this._listener_value); + this.manager.intercept_manager().register_listener(this._listener_grant); + this.manager.intercept_manager().register_listener(this._listener_general); + } + + finalize() { + } + + private _spawn_number_edit(x: number, y: number, width: number, height: number, color: scheme.TextField, value: number, callback: (new_value?: number) => any) { + const element = $.spawn("div"); + element.prop("contentEditable", true); + element + .css("pointer-events", "none") + .css("background", color.background) + .css("display", "block") + .css("position", "absolute") + .css("top", y) + .css("left", x) + .css("width", width) + .css("height", height) + .css("z-index", 1e6); + element.text(value); + element.appendTo(this.manager.canvas_container); + element.focus(); + + element.on('focusout', event => { + console.log("permission changed to " + element.text()); + if (!isNaN(parseInt(element.text()))) { + callback(parseInt(element.text())); + } else { + callback(undefined); + } + element.remove(); + }); + + element.on('keypress', event => { + if (event.which == KeyCode.KEY_RETURN) + element.trigger('focusout'); + + const text = String.fromCharCode(event.which); + if (isNaN(parseInt(text)) && text != "-") + event.preventDefault(); + + if (element.text().length > 7) + event.preventDefault(); + }); + + if (window.getSelection) { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element[0]); + selection.removeAllRanges(); + selection.addRange(range); + } + } + + trigger_value_assign() { + this._listener_value.on_click(undefined); + } + + trigger_grant_assign() { + this._listener_grant.on_click(undefined); + } + } + + export class InteractionManager { + private _listeners: InteractionListener[] = []; + private _entered_listeners: InteractionListener[] = []; + + register_listener(listener: InteractionListener) { + this._listeners.push(listener); + } + + remove_listener(listener: InteractionListener) { + this._listeners.remove(listener); + } + + process_mouse_move(new_x: number, new_y: number): { repaint: RepaintMode, cursor: string } { + let _entered_listeners: InteractionListener[] = []; + for (const listener of this._listeners) { + const aabb = listener.region; + + if (listener.disabled) + continue; + + if (new_x < aabb.x || new_x > aabb.x + aabb.width) + continue; + + if (new_y < aabb.y || new_y > aabb.y + aabb.height) + continue; + + _entered_listeners.push(listener); + } + + let repaint: RepaintMode = RepaintMode.NONE; + _entered_listeners.sort((a, b) => (a.region_weight || 0) - (b.region_weight || 0)); + for (const listener of this._entered_listeners) { + if (listener.on_mouse_leave && _entered_listeners.indexOf(listener) == -1) { + let mode = listener.on_mouse_leave(); + if (mode == RepaintMode.REPAINT_OBJECT_FULL) { + mode = RepaintMode.REPAINT; + if (listener.set_full_draw) + listener.set_full_draw(); + } + if (mode > repaint) + repaint = mode; + } + } + for (const listener of _entered_listeners) { + if (listener.on_mouse_enter && this._entered_listeners.indexOf(listener) == -1) { + let mode = listener.on_mouse_enter(); + if (mode == RepaintMode.REPAINT_OBJECT_FULL) { + mode = RepaintMode.REPAINT; + if (listener.set_full_draw) + listener.set_full_draw(); + } + if (mode > repaint) + repaint = mode; + } + } + this._entered_listeners = _entered_listeners; + + let cursor; + for (const listener of _entered_listeners) + if (typeof (listener.mouse_cursor) === "string") { + cursor = listener.mouse_cursor; + } + return { + repaint: repaint, + cursor: cursor + }; + } + + private process_click_event(x: number, y: number, event: InteractionClickEvent): RepaintMode { + const move_result = this.process_mouse_move(x, y); + + let repaint: RepaintMode = move_result.repaint; + for (const listener of this._entered_listeners) + if (listener.on_click) { + let mode = listener.on_click(event); + if (mode == RepaintMode.REPAINT_OBJECT_FULL) { + mode = RepaintMode.REPAINT; + if (listener.set_full_draw) + listener.set_full_draw(); + } + if (mode > repaint) + repaint = mode; + } + + return repaint; + } + + process_click(x: number, y: number): RepaintMode { + const event: InteractionClickEvent = { + consumed: false, + type: ClickEventType.SIGNLE, + offset_x: x, + offset_y: y + }; + + return this.process_click_event(x, y, event); + } + + process_dblclick(x: number, y: number): RepaintMode { + const event: InteractionClickEvent = { + consumed: false, + type: ClickEventType.DOUBLE, + offset_x: x, + offset_y: y + }; + + return this.process_click_event(x, y, event); + } + + process_context_menu(js_event: MouseEvent, x: number, y: number): RepaintMode { + const event: InteractionClickEvent = { + consumed: js_event.defaultPrevented, + type: ClickEventType.CONTEXT_MENU, + offset_x: x, + offset_y: y + }; + + const result = this.process_click_event(x, y, event); + if (event.consumed) + js_event.preventDefault(); + return result; + } + } + + export class PermissionEditor { + private static readonly PERMISSION_HEIGHT = PermissionEntry.HEIGHT; + private static readonly PERMISSION_GROUP_HEIGHT = PermissionGroup.HEIGHT; + + readonly grouped_permissions: GroupedPermissions[]; + readonly canvas: HTMLCanvasElement; + readonly canvas_container: HTMLDivElement; + private _max_height: number = 0; + + private _permission_count: number = 0; + private _permission_group_count: number = 0; + private _canvas_context: CanvasRenderingContext2D; + + private _selected_entry: PermissionEntry; + + private _draw_requested: boolean = false; + private _draw_requested_full: boolean = false; + + private _elements: PermissionGroup[] = []; + private _intersect_manager: InteractionManager; + + private _permission_entry_map: { [key: number]: PermissionEntry } = {}; + + mouse: { + x: number, + y: number + } = { + x: 0, + y: 0 + }; + + constructor(permissions: GroupedPermissions[]) { + this.grouped_permissions = permissions; + + this.canvas_container = $.spawn("div") + .addClass("window-resize-listener") /* we want to handle resized */ + .css("min-width", "750px") + .css("position", "relative") + .css("user-select", "none") + [0]; + this.canvas = $.spawn("canvas")[0]; + + this.canvas_container.appendChild(this.canvas); + + this._intersect_manager = new InteractionManager(); + this.canvas_container.onmousemove = event => { + this.mouse.x = event.pageX; + this.mouse.y = event.pageY; + + const draw = this._intersect_manager.process_mouse_move(event.offsetX, event.offsetY); + this.canvas_container.style.cursor = draw.cursor || ""; + this._handle_repaint(draw.repaint); + }; + this.canvas_container.onclick = event => { + this._handle_repaint(this._intersect_manager.process_click(event.offsetX, event.offsetY)); + }; + this.canvas_container.ondblclick = event => { + this._handle_repaint(this._intersect_manager.process_dblclick(event.offsetX, event.offsetY)); + }; + this.canvas_container.oncontextmenu = (event: MouseEvent) => { + this._handle_repaint(this._intersect_manager.process_context_menu(event, event.offsetX, event.offsetY)); + }; + this.canvas_container.onresize = () => this.request_draw(true); + + + this.initialize(); + } + + private _handle_repaint(mode: RepaintMode) { + if (mode == RepaintMode.REPAINT || mode == RepaintMode.REPAINT_FULL) + this.request_draw(mode == RepaintMode.REPAINT_FULL); + } + + request_draw(full?: boolean) { + this._draw_requested_full = this._draw_requested_full || full; + if (this._draw_requested) + return; + this._draw_requested = true; + requestAnimationFrame(() => { + this.draw(this._draw_requested_full); + }); + } + + draw(full?: boolean) { + this._draw_requested = false; + this._draw_requested_full = false; + + /* clear max height */ + this.canvas_container.style.overflowY = "shown"; + this.canvas_container.style.height = undefined; + + const max_height = this._max_height; + const max_width = this.canvas_container.clientWidth; + const update_width = this.canvas.width != max_width; + const full_draw = typeof (full) !== "boolean" || full || update_width; + + if (update_width) { + this.canvas.width = max_width; + for (const element of this._elements) + element.set_width(max_width); + } + + console.log("Drawing%s on %dx%d", full_draw ? " full" : "", max_width, max_height); + if (full_draw) + this.canvas.height = max_height; + const ctx = this._canvas_context; + ctx.resetTransform(); + if (full_draw) + ctx.clearRect(0, 0, max_width, max_height); + + let sum_height = 0; + for (const element of this._elements) { + element.draw(ctx, full_draw); + const height = element.height(); + sum_height += height; + ctx.translate(0, height); + } + + this.canvas_container.style.overflowY = "hidden"; + this.canvas_container.style.height = sum_height + "px"; + } + + private initialize() { + /* setup the canvas */ + { + const apply_group = (group: GroupedPermissions) => { + for (const g of group.children || []) + apply_group(g); + this._permission_group_count++; + this._permission_count += group.permissions.length; + }; + for (const group of this.grouped_permissions) + apply_group(group); + + this._max_height = this._permission_count * PermissionEditor.PERMISSION_HEIGHT + this._permission_group_count * PermissionEditor.PERMISSION_GROUP_HEIGHT; + console.log("%d permissions and %d groups required %d height", this._permission_count, this._permission_group_count, this._max_height); + + this.canvas.style.width = "100%"; + + this.canvas.style.flexShrink = "0"; + this.canvas_container.style.flexShrink = "0"; + + this._canvas_context = this.canvas.getContext("2d"); + } + + const font = Math.floor(2 / 3 * PermissionEntry.HEIGHT) + "px Arial"; + const font_checkmark = Math.floor((5 / 4) * PermissionEntry.HEIGHT) + "px Arial"; + const checkbox = { + background: "#303036", + background_hovered: "#CCCCCC", + + background_checked: "#0000AA", + background_checked_hovered: "#0000AA77", + + border: "#000000", + checkmark: "#303036", + checkmark_font: font_checkmark + }; + const input: scheme.TextField = { + color: "#000000", + font: font, + + background_hovered: "#CCCCCCCC", + background: "#30303600" + }; + + const color_scheme: scheme.ColorScheme = { + group: { + name: "#808080", + name_font: font + }, + //#28282c + permission: { + name: "#808080", + name_unset: "#1a1a1a", + name_font: font, + + background: "#303036", + background_selected: "#00007788", + + value: input, + value_b: checkbox, + granted: input, + negate: checkbox, + skip: checkbox + } + }; + (window as any).scheme = color_scheme; + /* setup elements to draw */ + { + const process_group = (group: PermissionGroup) => { + for (const permission of group._element_permissions.permissions) + this._permission_entry_map[permission.permission().id] = permission; + for (const g of group._sub_elements) + process_group(g); + }; + + for (const group of this.grouped_permissions) { + const element = new PermissionGroup(group); + element.set_color_scheme(color_scheme); + element.set_manager(this); + process_group(element); + this._elements.push(element); + } + for (const element of this._elements) { + element.initialize(); + } + } + } + + intercept_manager() { + return this._intersect_manager; + } + + set_selected_entry(entry?: PermissionEntry) { + if (this._selected_entry === entry) + return; + + if (this._selected_entry) { + this._selected_entry.selected = false; + this._selected_entry.request_full_draw(); + } + this._selected_entry = entry; + if (this._selected_entry) { + this._selected_entry.selected = true; + this._selected_entry.request_full_draw(); + } + this.request_draw(false); + } + + permission_entries(): PermissionEntry[] { + return Object.keys(this._permission_entry_map).map(e => this._permission_entry_map[e]); + } + + collapse_all() { + for (const group of this._elements) + group.collapse_group(); + this.request_draw(true); + } + + expend_all() { + for (const group of this._elements) + group.expend_group(); + this.request_draw(true); + } + } + } + + export class CanvasPermissionEditor extends Modals.AbstractPermissionEditor { + private container: JQuery; + + private mode_container_permissions: JQuery; + private mode_container_error_permission: JQuery; + private mode_container_unset: JQuery; + + /* references within the container tag */ + private permission_value_map: {[key:number]:PermissionValue} = {}; + + private entry_editor: ui.PermissionEditor; + + icon_resolver: (id: number) => Promise; + icon_selector: (current_id: number) => Promise; + + constructor() { + super(); + } + + initialize(permissions: GroupedPermissions[]) { + this._permissions = permissions; + this.entry_editor = new ui.PermissionEditor(permissions); + this.build_tag(); + } + + html_tag() { return this.container; } + + private build_tag() { + this.container = $("#tmpl_permission_editor_canvas").renderTag(); + /* search for that as long we've not that much nodes */ + this.mode_container_permissions = this.container.find(".container-mode-permissions"); + this.mode_container_error_permission = this.container.find(".container-mode-no-permissions"); + this.mode_container_unset = this.container.find(".container-mode-unset"); + this.set_mode(Modals.PermissionEditorMode.UNSET); + + /* the filter */ + { + const tag_filter_input = this.container.find(".filter-input"); + const tag_filter_granted = this.container.find(".filter-granted"); + + tag_filter_granted.on('change', event => tag_filter_input.trigger('change')); + tag_filter_input.on('keyup change', event => { + let filter_mask = tag_filter_input.val() as string; + let req_granted = tag_filter_granted.prop("checked"); + + + for(const entry of this.entry_editor.permission_entries()) { + const permission = entry.permission(); + + let shown = filter_mask.length == 0 || permission.name.indexOf(filter_mask) != -1; + if(shown && req_granted) { + const value: PermissionValue = this.permission_value_map[permission.id]; + shown = value && (value.hasValue() || value.hasGrant()); + } + + entry.hidden = !shown; + } + this.entry_editor.request_draw(true); + }); + } + + /* update button */ + { + this.container.find(".button-update").on('click', this.trigger_update.bind(this)); + } + + /* global context menu listener */ + { + this.container.on('contextmenu', event => { + if(event.isDefaultPrevented()) return; + event.preventDefault(); + + /* TODO allow collapse and expend all */ + }); + } + + { + const tag_container = this.container.find(".entry-editor-container"); + tag_container.append(this.entry_editor.canvas_container); + + tag_container.parent().on('contextmenu', event => { + if(event.isDefaultPrevented()) return; + event.preventDefault(); + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.entry_editor.expend_all() + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.entry_editor.collapse_all() + }); + }); + } + + /* setup the permissions */ + for(const entry of this.entry_editor.permission_entries()) { + const permission = entry.permission(); + entry.on_change = () => { + const flag_remove = typeof(entry.value) !== "number"; + this._listener_change(permission, { + remove: flag_remove, + flag_negate: entry.flag_negate, + flag_skip: entry.flag_skip, + value: flag_remove ? -2 : entry.value + }).then(() => { + if(flag_remove) { + const element = this.permission_value_map[permission.id]; + if(!element) return; /* This should never happen, if so how are we displaying this permission?! */ + + element.value = undefined; + element.flag_negate = false; + element.flag_skip = false; + } else { + const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); + + element.value = entry.value; + element.flag_skip = entry.flag_skip; + element.flag_negate = entry.flag_negate; + } + + if(permission.name === "i_icon_id") { + this.icon_resolver(entry.value).then(e => { + entry.set_icon_id_image(e); + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }).catch(error => { + console.warn(tr("Failed to load icon for permission editor: %o"), error); + }); + } + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }).catch(() => { + const element = this.permission_value_map[permission.id]; + + entry.value = element && element.hasValue() ? element.value : undefined; + entry.flag_skip = element && element.flag_skip; + entry.flag_negate = element && element.flag_negate; + + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }); + }; + + entry.on_grant_change = () => { + const flag_remove = typeof(entry.granted) !== "number"; + + this._listener_change(permission, { + remove: flag_remove, + granted: flag_remove ? -2 : entry.granted, + }).then(() => { + if(flag_remove) { + const element = this.permission_value_map[permission.id]; + if (!element) return; /* This should never happen, if so how are we displaying this permission?! */ + + element.granted_value = undefined; + } else { + const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); + element.granted_value = entry.granted; + } + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }).catch(() => { + const element = this.permission_value_map[permission.id]; + + entry.granted = element && element.hasGrant() ? element.granted_value : undefined; + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }); + }; + + entry.on_context_menu = (x, y) => { + let entries: contextmenu.MenuEntry[] = []; + if(typeof(entry.value) === "undefined") { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add permission"), + callback: () => entry.trigger_value_assign() + }); + } else { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove permission"), + callback: () => { + entry.value = undefined; + entry.on_change(); + } + }); + } + + if(typeof(entry.granted) === "undefined") { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add grant permission"), + callback: () => entry.trigger_grant_assign() + }); + } else { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove grant permission"), + callback: () => { + entry.granted = undefined; + entry.on_grant_change(); + } + }); + } + entries.push(contextmenu.Entry.HR()); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.entry_editor.expend_all() + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.entry_editor.collapse_all() + }); + entries.push(contextmenu.Entry.HR()); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Show permission description"), + callback: () => { + createInfoModal( + tr("Permission description"), + tr("Permission description for permission ") + permission.name + ":
" + permission.description + ).open(); + } + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Copy permission name"), + callback: () => { + copy_to_clipboard(permission.name); + } + }); + + contextmenu.spawn_context_menu(x, y, ...entries); + } + } + } + + set_permissions(permissions?: PermissionValue[]) { + permissions = permissions || []; + this.permission_value_map = {}; + + for(const permission of permissions) + this.permission_value_map[permission.type.id] = permission; + + for(const entry of this.entry_editor.permission_entries()) { + const permission = entry.permission(); + const value: PermissionValue = this.permission_value_map[permission.id]; + + if(permission.name === "i_icon_id") { + entry.set_icon_id_image(undefined); + entry.on_icon_select = this.icon_selector; + } + + if(value && value.hasValue()) { + entry.value = value.value; + entry.flag_skip = value.flag_skip; + entry.flag_negate = value.flag_negate; + if(permission.name === "i_icon_id") { + this.icon_resolver(value.value).then(e => { + entry.set_icon_id_image(e); + entry.request_full_draw(); + this.entry_editor.request_draw(false); + }).catch(error => { + console.warn(tr("Failed to load icon for permission editor: %o"), error); + }); + } + } else { + entry.value = undefined; + entry.flag_skip = false; + entry.flag_negate = false; + } + + if(value && value.hasGrant()) { + entry.granted = value.granted_value; + } else { + entry.granted = undefined; + } + } + this.entry_editor.request_draw(true); + } + + set_mode(mode: Modals.PermissionEditorMode) { + this.mode_container_permissions.css('display', mode == Modals.PermissionEditorMode.VISIBLE ? 'flex' : 'none'); + this.mode_container_error_permission.css('display', mode == Modals.PermissionEditorMode.NO_PERMISSION ? 'flex' : 'none'); + this.mode_container_unset.css('display', mode == Modals.PermissionEditorMode.UNSET ? 'block' : 'none'); + if(mode == Modals.PermissionEditorMode.VISIBLE) + this.entry_editor.draw(true); + } + + update_ui() { + this.entry_editor.draw(true); + } + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts index 308a3812..eb10460d 100644 --- a/shared/js/ui/modal/permission/HTMLPermissionEditor.ts +++ b/shared/js/ui/modal/permission/HTMLPermissionEditor.ts @@ -1,512 +1,819 @@ -/** - * RIP old HTML based editor (too many nodes, made the browser laggy) - */ -namespace unused { - namespace PermissionEditor { - export interface PermissionEntry { - tag: JQuery; - tag_value: JQuery; - tag_grant: JQuery; - tag_flag_negate: JQuery; - tag_flag_skip: JQuery; +namespace pe { + import PermissionEditorMode = Modals.PermissionEditorMode; + import KeyDownEvent = JQuery.KeyDownEvent; - id: number; - filter: string; - is_bool: boolean; + class HTMLPermission { + readonly handle: HTMLPermissionEditor; + readonly group: HTMLPermissionGroup; + readonly permission: PermissionInfo; + readonly index: number; + tag: JQuery; + tag_name: JQuery; + tag_container_value: JQuery; + tag_container_granted: JQuery; + tag_container_skip: JQuery; + tag_container_negate: JQuery; + + hidden: boolean; + + /* the "actual" values */ + private _mask = 0; /* fourth bit: hidden by filer | third bit: value type | second bit: grant shown | first bit: value shown */ + + private _tag_value: JQuery; + private _tag_value_input: JQuery; + + private _tag_granted: JQuery; + private _tag_granted_input: JQuery; + + private _tag_skip: JQuery; + private _tag_skip_input: JQuery; + + private _tag_negate: JQuery; + private _tag_negate_input: JQuery; + + private _value: number | undefined; + private _grant: number | undefined; + private flags: number; /* 0x01 := Skip | 0x02 := Negate */ + + constructor(handle: HTMLPermissionEditor, group: HTMLPermissionGroup, permission: PermissionInfo, index: number) { + this.handle = handle; + this.permission = permission; + this.index = index; + this.group = group; + + this.build_tag(); } - export interface PermissionValue { - remove: boolean; /* if set remove the set permission (value or granted) */ - - granted?: number; - value?: number; - - flag_skip?: boolean; - flag_negate?: boolean; + private static build_checkbox() : {tag: JQuery, input: JQuery} { + let tag, input; + tag = $.spawn("label").addClass("switch").append([ + input = $.spawn("input").attr("type", "checkbox"), + $.spawn("span").addClass("slider").append( + $.spawn("div").addClass("dot") + ) + ]); + return {tag: tag, input: input}; } - export type change_listener_t = (permission: PermissionInfo, value?: PermissionEditor.PermissionValue) => Promise; + private static number_filter_re = /^[-+]?([0-9]{0,9})$/; + private static number_filter = (event: KeyDownEvent) => { + if(event.ctrlKey) + return; + + const target = event.target; + if(event.key === "Enter") { + target.blur(); + return; + } + + if('keyCode' in event) { + /* everything under 46 is a control key except 32 its space */ + if(event.keyCode < 46 && event.keyCode != 32) + return; + + if(!HTMLPermission.number_filter_re.test(target.value + String.fromCharCode(event.keyCode))) { + event.preventDefault(); + return; + } + } else { + const e = event; /* for some reason typescript deducts the event type to "never" */ + if(!HTMLPermission.number_filter_re.test(e.key)) { + e.preventDefault(); + return; + } + } + }; + + private build_tag() { + this.tag = $.spawn("div").addClass("entry permission").css('padding-left', this.index + "em").append([ + this.tag_name = $.spawn("div").addClass("column-name").text(this.permission.name), + this.tag_container_value = $.spawn("div").addClass("column-value"), + this.tag_container_skip = $.spawn("div").addClass("column-skip"), + this.tag_container_negate = $.spawn("div").addClass("column-negate"), + this.tag_container_granted = $.spawn("div").addClass("column-granted") + ]); + + if(this.permission.is_boolean()) { + let value = HTMLPermission.build_checkbox(); + this._tag_value = value.tag; + this._tag_value_input = value.input; + + this._tag_value_input.on('change', event => { + const value = this._tag_value_input.prop('checked') ? 1 : 0; + + this.handle.trigger_change(this.permission, { + remove: false, + + value: value, + flag_skip: (this.flags & 0x01) > 0, + flag_negate: (this.flags & 0x02) > 0 + }).then(() => { + this._value = value; + }).catch(error => { + this._reset_value(); + }); + }); + + this._mask |= 0x04; + } else { + this._tag_value = $.spawn("input").addClass("number"); + this._tag_value_input = this._tag_value; + + this._tag_value_input.on('keydown', HTMLPermission.number_filter); + this._tag_value_input.on('change', event => { + const str_value = this._tag_value_input.val() as string; + const value = parseInt(str_value); + if(!HTMLPermission.number_filter_re.test(str_value) || value == NaN) { + console.warn(tr("Failed to parse given permission value string: %s"), this._tag_value_input.val()); + this._reset_value(); + return; + } + + this.handle.trigger_change(this.permission, { + remove: false, + + value: value, + flag_skip: (this.flags & 0x01) > 0, + flag_negate: (this.flags & 0x02) > 0 + }).then(() => { + this._value = value; + this._update_active_class(); + }).catch(error => { + this._reset_value(); + }); + }); + } + + { + let skip = HTMLPermission.build_checkbox(); + this._tag_skip = skip.tag; + this._tag_skip_input = skip.input; + + this._tag_skip_input.on('change', event => { + const value = this._tag_skip_input.prop('checked'); + + this.handle.trigger_change(this.permission, { + remove: false, + + value: this._value, + flag_skip: value, + flag_negate: (this.flags & 0x02) > 0 + }).then(() => { + if(value) + this.flags |= 0x01; + else + this.flags &= ~0x1; + this._update_active_class(); + }).catch(error => { + this._reset_value(); + }); + }); + } + + { + let negate = HTMLPermission.build_checkbox(); + this._tag_negate = negate.tag; + this._tag_negate_input = negate.input; + + this._tag_negate_input.on('change', event => { + const value = this._tag_negate_input.prop('checked'); + + console.log("Negate value: %o", value); + this.handle.trigger_change(this.permission, { + remove: false, + + value: this._value, + flag_skip: (this.flags & 0x01) > 0, + flag_negate: value + }).then(() => { + if(value) + this.flags |= 0x02; + else + this.flags &= ~0x2; + this._update_active_class(); + }).catch(error => { + this._reset_value(); + }); + }); + } + + { + this._tag_granted = $.spawn("input").addClass("number"); + this._tag_granted_input = this._tag_granted; + + this._tag_granted_input.on('keydown', HTMLPermission.number_filter); + this._tag_granted_input.on('change', event => { + const str_value = this._tag_granted_input.val() as string; + const value = parseInt(str_value); + if(!HTMLPermission.number_filter_re.test(str_value) || value == NaN) { + console.warn(tr("Failed to parse given permission granted value string: %s"), this._tag_granted_input.val()); + this._reset_value(); + return; + } + + this.handle.trigger_change(this.permission, { + remove: false, + + granted: value + }).then(() => { + this._grant = value; + this._update_active_class(); + }).catch(error => { + this._reset_grant(); + }); + }); + } + + /* double click handler */ + { + this.tag.on('dblclick', event => this._trigger_value_assign()) + } + + /* context menu */ + { + this.tag.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + event.preventDefault(); + + let entries: contextmenu.MenuEntry[] = []; + if(typeof(this._value) === "undefined") { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add permission"), + callback: () => this._trigger_value_assign() + }); + } else { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove permission"), + callback: () => { + this.handle.trigger_change(this.permission, { + remove: true, + value: 0 + }).then(() => { + this.value(undefined); + }).catch(error => { + //We have to do nothing + }); + } + }); + } + + if(typeof(this._grant) === "undefined") { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add grant permission"), + callback: () => this._trigger_grant_assign() + }); + } else { + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Remove grant permission"), + callback: () => { + this.handle.trigger_change(this.permission, { + remove: true, + granted: 0 + }).then(() => { + this.granted(undefined); + }).catch(error => { + //We have to do nothing + }); + } + }); + } + entries.push(contextmenu.Entry.HR()); + if(this.group.collapsed) + entries.push({ /* This could never happen! */ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend group"), + callback: () => this.group.expend() + }); + else + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse group"), + callback: () => this.group.collapse() + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.handle.expend_all() + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.handle.collapse_all() + }); + entries.push(contextmenu.Entry.HR()); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Show permission description"), + callback: () => { + createInfoModal( + tr("Permission description"), + tr("Permission description for permission ") + this.permission.name + ":
" + this.permission.description + ).open(); + } + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Copy permission name"), + callback: () => { + copy_to_clipboard(this.permission.name); + } + }); + + contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); + }); + } + } + + private _trigger_value_assign() { + if(typeof(this._value) === "undefined") + this.value(this._grant || 1, false, false); //TODO: Use max granted value? + this._tag_value_input.focus(); + if(this.permission.is_boolean()) + this._tag_value_input.trigger('change'); + } + + private _trigger_grant_assign() { + this.granted(1); //TODO: Use max granted value? + this._tag_granted_input.focus(); + } + + hide() { + this._mask &= ~0x08; + for(const element of this.tag) + (element).style.display = 'none'; + } + + show() { + this._mask |= 0x08; + for(const element of this.tag) + (element).style.display = 'flex'; + } + + is_filtered() : boolean { + return (this._mask & 0x10) > 0; + } + + set_filtered(flag: boolean) { + if(flag) + this._mask |= 0x10; + else + this._mask &= ~0x10; + } + + is_set() : boolean { + return (this._mask & 0x03) > 0; + } + + value(value: number | undefined, skip?: boolean, negate?: boolean) { + if(typeof value === "undefined") { + this._tag_value.detach(); + this._tag_negate.detach(); + this._tag_skip.detach(); + + this._value = undefined; + this.flags = 0; + + this._update_active_class(); + this._mask &= ~0x1; + return; + } + + if((this._mask & 0x1) == 0) { + this._tag_value.appendTo(this.tag_container_value); + this._tag_negate.appendTo(this.tag_container_negate); + this._tag_skip.appendTo(this.tag_container_skip); + + this._update_active_class(); + this._mask |= 0x01; + } + + if((this._mask & 0x04) > 0) + this._tag_value_input.prop('checked', !!value); + else + this._tag_value_input.val(value); + this._tag_skip_input.prop('checked', !!skip); + this._tag_negate_input.prop('checked', !!negate); + + this._value = value; + this.flags = (!!skip ? 0x01 : 0) | (!!negate ? 0x2 : 0); + } + + granted(value: number | undefined) { + if(typeof value === "undefined") { + this._tag_granted.detach(); + + this._update_active_class(); + this._grant = undefined; + this._mask &= ~0x2; + return; + } + + if((this._mask & 0x2) == 0) { + this._mask |= 0x02; + this._tag_granted.appendTo(this.tag_container_granted); + this._update_active_class(); + } + this._tag_granted_input.val(value); + this._grant = value; + } + + reset() { + this._mask &= ~0x03; + + this._tag_value.detach(); + this._tag_negate.detach(); + this._tag_skip.detach(); + + this._tag_granted.detach(); + + this._value = undefined; + this._grant = undefined; + this.flags = 0; + + const tag = this.tag[0] as HTMLDivElement; + tag.classList.remove("active"); + } + + private _reset_value() { + if(typeof(this._value) === "undefined") { + if((this._mask & 0x1) != 0) + this.value(undefined); + } else { + this.value(this._value, (this.flags & 0x1) > 1, (this.flags & 0x2) > 1); + } + } + + private _reset_grant() { + if(typeof(this._grant) === "undefined") { + if((this._mask & 0x2) != 0) + this.granted(undefined); + } else { + this.granted(this._grant); + } + } + + private _update_active_class() { + const value = typeof(this._value) !== "undefined" || typeof(this._grant) !== "undefined"; + const tag = this.tag[0] as HTMLDivElement; + if(value) + tag.classList.add("active"); + else + tag.classList.remove("active"); + } } - enum PermissionEditorMode { - VISIBLE, - NO_PERMISSION, - UNSET + class HTMLPermissionGroup { + readonly handle: HTMLPermissionEditor; + readonly group: PermissionGroup; + readonly index: number; + + private _tag_arrow: JQuery; + + permissions: HTMLPermission[] = []; + children: HTMLPermissionGroup[] = []; + + tag: JQuery; + visible: boolean; + + collapsed: boolean; + parent_collapsed: boolean; + + constructor(handle: HTMLPermissionEditor, group: PermissionGroup, index: number) { + this.handle = handle; + this.group = group; + this.index = index; + + this._build_tag(); + } + + private _build_tag() { + this.tag = $.spawn("div").addClass("entry group").css('padding-left', this.index + "em").append([ + $.spawn("div").addClass("column-name").append([ + this._tag_arrow = $.spawn("div").addClass("arrow down"), + $.spawn("div").addClass("group-name").text(this.group.name) + ]), + $.spawn("div").addClass("column-value"), + $.spawn("div").addClass("column-skip"), + $.spawn("div").addClass("column-negate"), + $.spawn("div").addClass("column-granted") + ]); + + this.tag.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + event.preventDefault(); + + const entries: contextmenu.MenuEntry[] = []; + if(this.collapsed) + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend group"), + callback: () => this.expend(), + }); + else + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse group"), + callback: () => this.collapse(), + }); + entries.push(contextmenu.Entry.HR()); + + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.handle.expend_all() + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.handle.collapse_all() + }); + + contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); + }); + + this._tag_arrow.on('click', event => { + if(this.collapsed) + this.expend(); + else + this.collapse(); + }) + } + + update_visibility() { + let flag = false; + if (!flag) { + for (const group of this.children) { + if (group.visible) { + flag = true; + break; + } + } + } + + if (!flag) { + for (const permission of this.permissions) { + if (!permission.is_filtered()) { + flag = true; + break; + } + } + } + + this.visible = flag; + + flag = flag && !this.parent_collapsed; + for(const element of this.tag) + (element).style.display = flag ? 'flex' : 'none'; + + const arrow_node = this._tag_arrow[0]; + arrow_node.classList.remove(this.collapsed ? "down" : "right"); + arrow_node.classList.add(!this.collapsed ? "down" : "right"); + } + + collapse() { + this.collapsed = true; + + const children = [...this.children]; + while (true) { + const child = children.pop(); + if(!child) break; + + child.parent_collapsed = true; + children.push(...child.children); + } + + this.handle.update_view(); + } + + expend() { + this.collapsed = false; + + if(this.parent_collapsed) + return; + + const children = [...this.children]; + while (true) { + const child = children.pop(); + if(!child) break; + + child.parent_collapsed = false; + if(!child.collapsed) + children.push(...child.children); + } + + this.handle.update_view(); + } } - class PermissionEditor { - readonly permissions: GroupedPermissions[]; - + export class HTMLPermissionEditor extends Modals.AbstractPermissionEditor { container: JQuery; private mode_container_permissions: JQuery; private mode_container_error_permission: JQuery; private mode_container_unset: JQuery; - /* references within the container tag */ - private permission_value_map: {[key:number]:PermissionValue} = {}; - private permission_map: {[key:number]: PermissionEditor.PermissionEntry}; - private listener_change: PermissionEditor.change_listener_t = () => Promise.resolve(); - private listener_update: () => any; + private filter_input: JQuery; + private filter_grant: JQuery; - constructor(permissions: GroupedPermissions[]) { - this.permissions = permissions; + private button_toggle: JQuery; + + private even_list: ({ visible() : boolean; set_even(flag: boolean); })[]; + private permission_map: Array; + private permission_groups: HTMLPermissionGroup[]; + + constructor() { + super(); } - build_tag() { - this.permission_map = {}; + initialize(permissions: GroupedPermissions[]) { + this._permissions = permissions; + this.build_tag(); + } + + private update_filter() { + const value = (this.filter_input.val() as string).toLowerCase(); + const grant = !!this.filter_grant.prop('checked'); + + const _filter = (permission: HTMLPermission) => { + if(value && permission.permission.name.indexOf(value) == -1) return false; + if(grant && !permission.is_set()) return false; + + return true; + }; + + for(let id = 1; id < this.permission_map.length; id++) { + const permission = this.permission_map[id]; + let flag = _filter(permission); + permission.set_filtered(!flag); + + + flag = flag && !permission.group.collapsed && !permission.group.parent_collapsed; /* hide when parent is filtered */ + if(flag) permission.show(); + else permission.hide(); + } + + /* run in both directions, to update the parent visibility and the actiual visibility */ + for(const group of this.permission_groups) + group.update_visibility(); + for(const group of this.permission_groups.slice().reverse()) + group.update_visibility(); + + + let index = 0; + for(const entry of this.even_list) { + if(!entry.visible()) continue; + entry.set_even((index++ & 0x1) == 0); + } + } + + private build_tag() { + this.container = $("#tmpl_permission_editor_html").renderTag(); + this.container.find("input").on('change', event => { + $(event.target).parents(".form-group").toggleClass('is-filled', !!(event.target as HTMLInputElement).value); + }); - this.container = $("#tmpl_permission_editor").renderTag(); /* search for that as long we've not that much nodes */ this.mode_container_permissions = this.container.find(".container-mode-permissions"); this.mode_container_error_permission = this.container.find(".container-mode-no-permissions"); this.mode_container_unset = this.container.find(".container-mode-unset"); - this.set_mode(PermissionEditorMode.UNSET); - /* the filter */ + this.filter_input = this.container.find(".filter-input"); + this.filter_input.on('change keyup', event => this.update_filter()); + + this.filter_grant = this.container.find(".filter-granted"); + this.filter_grant.on('change', event => this.update_filter()); + + this.button_toggle = this.container.find(".button-toggle-clients"); + this.button_toggle.on('click', () => { + if(this._toggle_callback) + this.button_toggle.text(this._toggle_callback()); + }); + + this.container.find(".button-update").on('click', event => this.trigger_update()); + + /* allocate array space */ { - const tag_filter_input = this.container.find(".filter-input"); - const tag_filter_granted = this.container.find(".filter-granted"); + let max_index = 0; + let tmp: GroupedPermissions[] = []; + while(true) { + const entry = tmp.pop(); + if(!entry) break; + for(const permission of entry.permissions) + if(permission.id > max_index) + max_index = permission.id; + tmp.push(...entry.children); + } + this.permission_map = new Array(max_index + 1); + } + this.permission_groups = []; + this.even_list = []; - tag_filter_granted.on('change', event => tag_filter_input.trigger('change')); - tag_filter_input.on('keyup change', event => { - let filter_mask = tag_filter_input.val() as string; - let req_granted = tag_filter_granted.prop("checked"); + { + const container_permission = this.mode_container_permissions.find(".container-permission-list .body"); - /* we've to disable this function because its sometimes laggy */ - const org_fn = $.fn.dropdown && $.fn.dropdown.Constructor ? $.fn.dropdown.Constructor._clearMenus : undefined; - if(org_fn) - $.fn.dropdown.Constructor._clearMenus = () => {}; - - /* update each permission */ - { - const start = Date.now(); - - for(const permission_id of Object.keys(this.permission_map)) { - const permission: PermissionEditor.PermissionEntry = this.permission_map[permission_id]; - let shown = filter_mask.length == 0 || permission.filter.indexOf(filter_mask) != -1; - if(shown && req_granted) { - const value: PermissionValue = this.permission_value_map[permission_id]; - shown = value && (value.hasValue() || value.hasGrant()); - } - - permission.tag.attr("match", shown ? 1 : 0); - /* this is faster then .hide() or .show() */ - if(shown) - permission.tag.css('display', 'flex'); + const build_group = (pgroup: HTMLPermissionGroup, group: GroupedPermissions, index: number) => { + const hgroup = new HTMLPermissionGroup(this, group.group, index); + hgroup.tag.appendTo(container_permission); + this.even_list.push({ + set_even(flag: boolean) { + if(flag) + hgroup.tag[0].classList.add('even'); else - permission.tag.css('display', 'none'); + hgroup.tag[0].classList.remove('even'); + }, + + visible(): boolean { + return !hgroup.parent_collapsed && hgroup.visible; } - - const end = Date.now(); - console.error("Filter update required %oms", end - start); - } - - /* update group visibility (hide empty groups) */ - { - const start = Date.now(); - - this.container.find(".group").each((idx, _entry) => { - let entry = $(_entry); - let target = entry.find(".entry:not(.group)[match=\"1\"]").length > 0; - /* this is faster then .hide() or .show() */ - if(target) - entry.css('display', 'flex'); - else - entry.css('display', 'none'); - }); - - const end = Date.now(); - console.error("Group update required %oms", end - start); - } - - if(org_fn) - $.fn.dropdown.Constructor._clearMenus = org_fn; - }); - } - - /* update button */ - { - this.container.find(".button-update").on('click', this.trigger_update.bind(this)); - } - - /* global context menu listener */ - { - this.container.on('contextmenu', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - /* TODO allow collapse and expend all */ - }); - } - - /* actual permissions */ - { - const tag_entries = this.container.find(".entries"); - - const template_entry = $("#tmpl_permission_entry"); - const build_group = (group: GroupedPermissions) : JQuery => { - const tag_group = template_entry.renderTag({ - type: "group", - name: group.group.name }); - const tag_group_entries = tag_group.find(".group-entries"); - const update_collapse_status = (status: boolean, recursive: boolean) => { - const tag = recursive ? this.container.find(".entry.group") : tag_group; + if(pgroup) + pgroup.children.push(hgroup); + this.permission_groups.push(hgroup); - /* this is faster then .hide() or .show() */ - if(status) { - tag.find("> .group-entries").css('display', 'block'); - } else { - tag.find("> .group-entries").css('display', 'none'); - } + index++; + for(const child of group.children) + build_group(hgroup, child, index); - tag.find("> .title .arrow").toggleClass("down", status).toggleClass("right", !status); - }; + for(const permission of group.permissions) { + const perm = new HTMLPermission(this, hgroup, permission, index); + this.permission_map[perm.permission.id] = perm; + perm.tag.appendTo(container_permission); + hgroup.permissions.push(perm); + this.even_list.push({ + set_even(flag: boolean) { + if(flag) + perm.tag[0].classList.add('even'); + else + perm.tag[0].classList.remove('even'); + }, - /* register collapse and context listener */ - { - const tag_arrow = tag_group.find(".arrow"); - tag_arrow.on('click', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - update_collapse_status(tag_arrow.hasClass("right"), false); - }); - - const tag_title = tag_group.find(".title"); - tag_title.on('contextmenu', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend group"), - callback: () => update_collapse_status(true, false) - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend all"), - callback: () => update_collapse_status(true, true) - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse group"), - callback: () => update_collapse_status(false, false) - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse all"), - callback: () => update_collapse_status(false, true) - }); + visible(): boolean { + return !perm.is_filtered() && !perm.group.collapsed && !perm.group.parent_collapsed; + } }); } - - /* build the permissions */ - { - for(const permission of group.permissions) { - const tag_permission = template_entry.renderTag({ - type: "permission", - permission_name: permission.name, - permission_id: permission.id, - permission_description: permission.description, - }); - - const tag_value = tag_permission.find(".column-value input"); - const tag_granted = tag_permission.find(".column-granted input"); - const tag_flag_skip = tag_permission.find(".column-skip input"); - const tag_flag_negate = tag_permission.find(".column-negate input"); - - /* double click listener */ - { - tag_permission.on('dblclick', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - if(tag_permission.hasClass("value-unset")) { - tag_flag_skip.prop("checked", false); - tag_flag_negate.prop("checked", false); - - tag_permission.removeClass("value-unset"); - if(permission.name.startsWith("b_")) { - tag_permission.find(".column-value input") - .prop("checked", true) - .trigger('change'); - } else { - /* TODO auto value */ - tag_value.val('').focus(); - } - } else if(!permission.name.startsWith("b_")) { - tag_value.focus(); - } - }); - - tag_permission.find(".column-granted").on('dblclick', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - if(tag_permission.hasClass("grant-unset")) { - tag_permission.removeClass("grant-unset"); - tag_granted.focus(); - } - }); - } - - /* focus out listener */ - { - tag_granted.on('focusout', event => { - try { - const value = tag_granted.val() as string; - if(isNaN(parseInt(value))) - throw ""; - } catch(_) { - tag_granted.val(""); - tag_permission.addClass("grant-unset"); - - const element = this.permission_value_map[permission.id]; - if(element && element.hasGrant()) { - this.listener_change(permission, { - remove: true, - granted: -2 - }).then(() => { - element.granted_value = undefined; - }).catch(() => { - tag_granted.val(element.granted_value); - }); - } - } - }); - - tag_value.on('focusout', event => { - try { - if(isNaN(parseInt(tag_value.val() as string))) - throw ""; - } catch(_) { - const element = this.permission_value_map[permission.id]; - if(element && element.hasValue()) { - tag_value.val(element.value); - } else { - tag_value.val(""); - tag_permission.addClass("value-unset"); - } - } - }) - } - - /* change listener */ - { - tag_flag_negate.on('change', () => tag_value.trigger('change')); - tag_flag_skip.on('change', () => tag_value.trigger('change')); - - tag_granted.on('change', event => { - const value = parseInt(tag_granted.val() as string); - if(isNaN(value)) return; - - this.listener_change(permission, { - remove: false, - granted: value, - }).then(() => { - const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); - element.granted_value = value; - }).catch(() => { - const element = this.permission_value_map[permission.id]; - tag_granted.val(element && element.hasGrant() ? element.granted_value : ""); - tag_permission.toggleClass("grant-unset", !element || !element.hasGrant()); - }); - }); - - tag_value.on('change', event => { - const value = permission.is_boolean() ? tag_value.prop("checked") ? 1 : 0 : parseInt(tag_value.val() as string); - if(isNaN(value)) return; - - const flag_negate = tag_flag_negate.prop("checked"); - const flag_skip = tag_flag_skip.prop("checked"); - - this.listener_change(permission, { - remove: false, - value: value, - flag_negate: flag_negate, - flag_skip: flag_skip - }).then(() => { - const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); - - element.value = value; - element.flag_skip = flag_skip; - element.flag_negate = flag_negate; - }).catch(error => { - const element = this.permission_value_map[permission.id]; - - /* reset or set the fields */ - if(permission.is_boolean()) - tag_value.prop('checked', element && element.hasValue() && element.value > 0); - else - tag_value.val(element && element.hasValue() ? element.value : ""); - tag_flag_negate.prop("checked", element && element.flag_negate); - tag_flag_skip.prop("checked", element && element.flag_skip); - tag_permission.toggleClass("value-unset", !element || !element.hasValue()); - }); - }); - } - - /* context menu */ - { - tag_permission.on('contextmenu', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - let entries: contextmenu.MenuEntry[] = []; - if(tag_permission.hasClass("value-unset")) { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add permission"), - callback: () => tag_permission.trigger('dblclick') - }); - } else { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove permission"), - callback: () => { - this.listener_change(permission, { - remove: true, - value: -2 - }).then(() => { - const element = this.permission_value_map[permission.id]; - if(!element) return; /* This should never happen, if so how are we displaying this permission?! */ - - element.value = undefined; - element.flag_negate = false; - element.flag_skip = false; - - tag_permission.toggleClass("value-unset", true); - }).catch(() => { - const element = this.permission_value_map[permission.id]; - - /* reset or set the fields */ - tag_value.val(element && element.hasValue() ? element.value : ""); - tag_flag_negate.prop("checked", element && element.flag_negate); - tag_flag_skip.prop("checked", element && element.flag_skip); - tag_permission.toggleClass("value-unset", !element || !element.hasValue()); - }); - } - }); - } - - if(tag_permission.hasClass("grant-unset")) { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add grant permission"), - callback: () => tag_permission.find(".column-granted").trigger('dblclick') - }); - } else { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove grant permission"), - callback: () => - tag_granted.val('').trigger('focusout') /* empty values are handled within focus out */ - }); - } - entries.push(contextmenu.Entry.HR()); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend all"), - callback: () => update_collapse_status(true, true) - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse all"), - callback: () => update_collapse_status(false, true) - }); - entries.push(contextmenu.Entry.HR()); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Show permission description"), - callback: () => { - createInfoModal( - tr("Permission description"), - tr("Permission description for permission ") + permission.name + ":
" + permission.description - ).open(); - } - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Copy permission name"), - callback: () => { - copy_to_clipboard(permission.name); - } - }); - - contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); - }); - } - - this.permission_map[permission.id] = { - tag: tag_permission, - id: permission.id, - filter: permission.name, - tag_flag_negate: tag_flag_negate, - tag_flag_skip: tag_flag_skip, - tag_grant: tag_granted, - tag_value: tag_value, - is_bool: permission.is_boolean() - }; - - tag_group_entries.append(tag_permission); - } - } - - /* append the subgroups */ - for(const child of group.children) { - tag_group_entries.append(build_group(child)); - } - - return tag_group; }; - /* build the groups */ - for(const group of this.permissions) - tag_entries.append(build_group(group)); + for(const group of this._permissions) + build_group(undefined, group, 0); } + + this.mode_container_permissions.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + event.preventDefault(); + + const entries: contextmenu.MenuEntry[] = []; + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Expend all"), + callback: () => this.expend_all() + }); + entries.push({ + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Collapse all"), + callback: () => this.collapse_all() + }); + + contextmenu.spawn_context_menu(event.pageX, event.pageY, ...entries); + }); + + this.set_mode(PermissionEditorMode.UNSET); } - set_permissions(permissions?: PermissionValue[]) { - permissions = permissions || []; - this.permission_value_map = {}; + html_tag(): JQuery { + return this.container; + } - for(const permission of permissions) - this.permission_value_map[permission.type.id] = permission; + set_permissions(u_permissions?: PermissionValue[]) { + const permissions = new Array(this.permission_map.length); - for(const permission_id of Object.keys(this.permission_map)) { - const permission: PermissionEditor.PermissionEntry = this.permission_map[permission_id]; - const value: PermissionValue = this.permission_value_map[permission_id]; + /* initialize update array, boundary checks are already made by js */ + for(const perm of u_permissions) + permissions[perm.type.id] = perm; - permission.tag - .toggleClass("value-unset", !value || !value.hasValue()) - .toggleClass("grant-unset", !value || !value.hasGrant()); - - if(value && value.hasValue()) { - if(value.type.is_boolean()) - permission.tag_value.prop("checked", value.value); - else - permission.tag_value.val(value.value); - permission.tag_flag_skip.prop("checked", value.flag_skip); - permission.tag_flag_negate.prop("checked", value.flag_negate); - } - if(value && value.hasGrant()) { - permission.tag_grant.val(value.granted_value); + /* there is no permission with id 0 */ + for(let id = 1; id < permissions.length; id++) { + const new_permission = permissions[id]; + const permission_handle = this.permission_map[id]; + if(!new_permission) { + permission_handle.reset(); + continue; } + + permission_handle.value(new_permission.value, new_permission.flag_skip, new_permission.flag_negate); + permission_handle.granted(new_permission.granted_value); } - } - set_listener(listener?: PermissionEditor.change_listener_t) { - this.listener_change = listener || (() => Promise.resolve()); - } - - set_listener_update(listener?: () => any) { - this.listener_update = listener; - } - - trigger_update() { - if(this.listener_update) - this.listener_update(); + this.update_filter(); } set_mode(mode: PermissionEditorMode) { @@ -514,5 +821,40 @@ namespace unused { this.mode_container_error_permission.css('display', mode == PermissionEditorMode.NO_PERMISSION ? 'flex' : 'none'); this.mode_container_unset.css('display', mode == PermissionEditorMode.UNSET ? 'block' : 'none'); } + + trigger_change(permission: PermissionInfo, value?: Modals.PermissionEditor.PermissionValue) : Promise { + if(this._listener_change) + return this._listener_change(permission, value); + return Promise.reject(); + } + + collapse_all() { + for(const group of this.permission_groups) { + group.collapsed = true; + for(const child of group.children) + child.parent_collapsed = true; + } + this.update_filter(); /* update display state of all entries */ + } + + expend_all() { + for(const group of this.permission_groups) { + group.collapsed = false; + group.parent_collapsed = false; + } + this.update_filter(); /* update display state of all entries */ + } + + update_view() { return this.update_filter(); } + + set_toggle_button(callback: () => string, initial: string) { + this._toggle_callback = callback; + if(this._toggle_callback) { + this.button_toggle.text(initial); + this.button_toggle.show(); + } else { + this.button_toggle.hide(); + } + } } } \ No newline at end of file diff --git a/shared/js/ui/modal/permission/ModalPermissionEdit.ts b/shared/js/ui/modal/permission/ModalPermissionEdit.ts index d9af8c3e..ff7f7791 100644 --- a/shared/js/ui/modal/permission/ModalPermissionEdit.ts +++ b/shared/js/ui/modal/permission/ModalPermissionEdit.ts @@ -17,7 +17,7 @@ interface JQuery { } namespace Modals { - namespace PermissionEditor { + export namespace PermissionEditor { export interface PermissionEntry { tag: JQuery; tag_value: JQuery; @@ -41,375 +41,150 @@ namespace Modals { flag_negate?: boolean; } - export type change_listener_t = (permission: PermissionInfo, value?: PermissionEditor.PermissionValue) => Promise; + export type change_listener_t = (permission: PermissionInfo, value?: PermissionEditor.PermissionValue) => Promise; } - enum PermissionEditorMode { + export enum PermissionEditorMode { VISIBLE, NO_PERMISSION, UNSET } - - class PermissionEditor { - readonly permissions: GroupedPermissions[]; - container: JQuery; - - private mode_container_permissions: JQuery; - private mode_container_error_permission: JQuery; - private mode_container_unset: JQuery; - - /* references within the container tag */ - private permission_value_map: {[key:number]:PermissionValue} = {}; - private listener_change: PermissionEditor.change_listener_t = () => Promise.resolve(); - private listener_update: () => any; - - private entry_editor: ui.PermissionEditor; + export abstract class AbstractPermissionEditor { + protected _permissions: GroupedPermissions[]; + protected _listener_update: () => any; + protected _listener_change: PermissionEditor.change_listener_t = () => Promise.resolve(); + protected _toggle_callback: () => string; icon_resolver: (id: number) => Promise; icon_selector: (current_id: number) => Promise; - constructor(permissions: GroupedPermissions[]) { - this.permissions = permissions; - this.entry_editor = new ui.PermissionEditor(permissions); - } + protected constructor() {} - build_tag() { - this.container = $("#tmpl_permission_editor").renderTag(); - /* search for that as long we've not that much nodes */ - this.mode_container_permissions = this.container.find(".container-mode-permissions"); - this.mode_container_error_permission = this.container.find(".container-mode-no-permissions"); - this.mode_container_unset = this.container.find(".container-mode-unset"); - this.set_mode(PermissionEditorMode.UNSET); + abstract set_mode(mode: PermissionEditorMode); - /* the filter */ - { - const tag_filter_input = this.container.find(".filter-input"); - const tag_filter_granted = this.container.find(".filter-granted"); - - tag_filter_granted.on('change', event => tag_filter_input.trigger('change')); - tag_filter_input.on('keyup change', event => { - let filter_mask = tag_filter_input.val() as string; - let req_granted = tag_filter_granted.prop("checked"); - - - for(const entry of this.entry_editor.permission_entries()) { - const permission = entry.permission(); - - let shown = filter_mask.length == 0 || permission.name.indexOf(filter_mask) != -1; - if(shown && req_granted) { - const value: PermissionValue = this.permission_value_map[permission.id]; - shown = value && (value.hasValue() || value.hasGrant()); - } - - entry.hidden = !shown; - } - this.entry_editor.request_draw(true); - }); - } - - /* update button */ - { - this.container.find(".button-update").on('click', this.trigger_update.bind(this)); - } - - /* global context menu listener */ - { - this.container.on('contextmenu', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - /* TODO allow collapse and expend all */ - }); - } - - { - const tag_container = this.container.find(".entry-editor-container"); - tag_container.append(this.entry_editor.canvas_container); - - tag_container.parent().on('contextmenu', event => { - if(event.isDefaultPrevented()) return; - event.preventDefault(); - - contextmenu.spawn_context_menu(event.pageX, event.pageY, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend all"), - callback: () => this.entry_editor.expend_all() - }, { - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse all"), - callback: () => this.entry_editor.collapse_all() - }); - }); - } - - /* setup the permissions */ - for(const entry of this.entry_editor.permission_entries()) { - const permission = entry.permission(); - entry.on_change = () => { - const flag_remove = typeof(entry.value) !== "number"; - this.listener_change(permission, { - remove: flag_remove, - flag_negate: entry.flag_negate, - flag_skip: entry.flag_skip, - value: flag_remove ? -2 : entry.value - }).then(() => { - if(flag_remove) { - const element = this.permission_value_map[permission.id]; - if(!element) return; /* This should never happen, if so how are we displaying this permission?! */ - - element.value = undefined; - element.flag_negate = false; - element.flag_skip = false; - } else { - const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); - - element.value = entry.value; - element.flag_skip = entry.flag_skip; - element.flag_negate = entry.flag_negate; - } - - if(permission.name === "i_icon_id") { - this.icon_resolver(entry.value).then(e => { - entry.set_icon_id_image(e); - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }).catch(error => { - console.warn(tr("Failed to load icon for permission editor: %o"), error); - }); - } - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }).catch(() => { - const element = this.permission_value_map[permission.id]; - - entry.value = element && element.hasValue() ? element.value : undefined; - entry.flag_skip = element && element.flag_skip; - entry.flag_negate = element && element.flag_negate; - - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }); - }; - - entry.on_grant_change = () => { - const flag_remove = typeof(entry.granted) !== "number"; - - this.listener_change(permission, { - remove: flag_remove, - granted: flag_remove ? -2 : entry.granted, - }).then(() => { - if(flag_remove) { - const element = this.permission_value_map[permission.id]; - if (!element) return; /* This should never happen, if so how are we displaying this permission?! */ - - element.granted_value = undefined; - } else { - const element = this.permission_value_map[permission.id] || (this.permission_value_map[permission.id] = new PermissionValue(permission)); - element.granted_value = entry.granted; - } - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }).catch(() => { - const element = this.permission_value_map[permission.id]; - - entry.granted = element && element.hasGrant() ? element.granted_value : undefined; - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }); - }; - - entry.on_context_menu = (x, y) => { - let entries: contextmenu.MenuEntry[] = []; - if(typeof(entry.value) === "undefined") { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add permission"), - callback: () => entry.trigger_value_assign() - }); - } else { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove permission"), - callback: () => { - entry.value = undefined; - entry.on_change(); - } - }); - } - - if(typeof(entry.granted) === "undefined") { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Add grant permission"), - callback: () => entry.trigger_grant_assign() - }); - } else { - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Remove grant permission"), - callback: () => { - entry.granted = undefined; - entry.on_grant_change(); - } - }); - } - entries.push(contextmenu.Entry.HR()); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Expend all"), - callback: () => this.entry_editor.expend_all() - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Collapse all"), - callback: () => this.entry_editor.collapse_all() - }); - entries.push(contextmenu.Entry.HR()); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Show permission description"), - callback: () => { - createInfoModal( - tr("Permission description"), - tr("Permission description for permission ") + permission.name + ":
" + permission.description - ).open(); - } - }); - entries.push({ - type: contextmenu.MenuEntryType.ENTRY, - name: tr("Copy permission name"), - callback: () => { - copy_to_clipboard(permission.name); - } - }); - - contextmenu.spawn_context_menu(x, y, ...entries); - } - } - } - - set_permissions(permissions?: PermissionValue[]) { - permissions = permissions || []; - this.permission_value_map = {}; - - for(const permission of permissions) - this.permission_value_map[permission.type.id] = permission; - - for(const entry of this.entry_editor.permission_entries()) { - const permission = entry.permission(); - const value: PermissionValue = this.permission_value_map[permission.id]; - - if(permission.name === "i_icon_id") { - entry.set_icon_id_image(undefined); - entry.on_icon_select = this.icon_selector; - } - - if(value && value.hasValue()) { - entry.value = value.value; - entry.flag_skip = value.flag_skip; - entry.flag_negate = value.flag_negate; - if(permission.name === "i_icon_id") { - this.icon_resolver(value.value).then(e => { - entry.set_icon_id_image(e); - entry.request_full_draw(); - this.entry_editor.request_draw(false); - }).catch(error => { - console.warn(tr("Failed to load icon for permission editor: %o"), error); - }); - } - } else { - entry.value = undefined; - entry.flag_skip = false; - entry.flag_negate = false; - } - - if(value && value.hasGrant()) { - entry.granted = value.granted_value; - } else { - entry.granted = undefined; - } - } - this.entry_editor.request_draw(true); - } + abstract initialize(permissions: GroupedPermissions[]); + abstract html_tag() : JQuery; + abstract set_permissions(permissions?: PermissionValue[]); set_listener(listener?: PermissionEditor.change_listener_t) { - this.listener_change = listener || (() => Promise.resolve()); + this._listener_change = listener || (() => Promise.resolve()); } - set_listener_update(listener?: () => any) { - this.listener_update = listener; - } + set_listener_update(listener?: () => any) { this._listener_update = listener; } + trigger_update() { if(this._listener_update) this._listener_update(); } - trigger_update() { - if(this.listener_update) - this.listener_update(); - } - - set_mode(mode: PermissionEditorMode) { - this.mode_container_permissions.css('display', mode == PermissionEditorMode.VISIBLE ? 'flex' : 'none'); - this.mode_container_error_permission.css('display', mode == PermissionEditorMode.NO_PERMISSION ? 'flex' : 'none'); - this.mode_container_unset.css('display', mode == PermissionEditorMode.UNSET ? 'block' : 'none'); - if(mode == PermissionEditorMode.VISIBLE) - this.entry_editor.draw(true); - } - - update_ui() { - this.entry_editor.draw(true); - } + abstract set_toggle_button(callback: () => string, initial: string); } - export function spawnPermissionEdit(connection: ConnectionHandler) : Modal { + export type OptionsServerGroup = {}; + export type OptionsChannelGroup = {}; + export type OptionsClientPermissions = { unique_id?: string }; + export type OptionsChannelPermissions = { channel_id?: number }; + export type OptionsClientChannelPermissions = OptionsClientPermissions & OptionsChannelPermissions; + export interface OptionMap { + "sg": OptionsServerGroup, + "cg": OptionsChannelGroup, + "clp": OptionsClientPermissions, + "chp": OptionsChannelPermissions, + "clchp": OptionsClientChannelPermissions + } + + export function _space() { + const now = Date.now(); + while(now + 100 > Date.now()); + } + + export function spawnPermissionEdit(connection: ConnectionHandler, selected_tab?: T, options?: OptionMap[T]) : Modal { + options = options || {}; + const modal = createModal({ header: function() { return tr("Server Permissions"); }, body: function () { let properties: any = {}; - let tag = $("#tmpl_server_permissions").renderTag(properties); - const pe = new PermissionEditor(connection.permissions.groupedPermissions()); - pe.build_tag(); - pe.icon_resolver = id => connection.fileManager.icons.resolve_icon(id).then(async icon => { - if(!icon) - return undefined; - const tag = document.createElement("img"); - await new Promise((resolve, reject) => { - tag.onerror = reject; - tag.onload = resolve; - tag.src = icon.url; + /* build the permission editor */ + const permission_editor: AbstractPermissionEditor = (() => { + const editor = new pe.HTMLPermissionEditor(); + editor.initialize(connection.permissions.groupedPermissions()); + editor.icon_resolver = id => connection.fileManager.icons.resolve_icon(id).then(async icon => { + if(!icon) + return undefined; + + const tag = document.createElement("img"); + await new Promise((resolve, reject) => { + tag.onerror = reject; + tag.onload = resolve; + tag.src = icon.url; + }); + return tag; + }); + editor.icon_selector = current_icon => new Promise(resolve => { + spawnIconSelect(connection, id => resolve(new Int32Array([id])[0]), current_icon); }); - return tag; - }); - pe.icon_selector = current_icon => new Promise(resolve => { - spawnIconSelect(connection, id => resolve(new Int32Array([id])[0]), current_icon); - }); - /* initialisation */ + if(editor instanceof pe.CanvasPermissionEditor) + setTimeout(() => editor.update_ui(), 500); + return editor; + })(); + + const container_tab_list = tag.find(".right > .header"); { - const pe_server = tag.find("permission-editor.group-server"); - pe_server.append(pe.container); /* fuck off workaround to initialize form listener */ - } - setTimeout(() => { - pe.update_ui(); - }, 500); + const label_current = tag.find(".left .container-selected"); + const create_tab = (tab_entry: JQuery, container_name: string) => { + const target_container = tag.find(".body .container." + container_name); - apply_server_groups(connection, pe, tag.find(".tab-group-server")); - apply_channel_groups(connection, pe, tag.find(".tab-group-channel")); - apply_channel_permission(connection, pe, tag.find(".tab-channel")); - apply_client_permission(connection, pe, tag.find(".tab-client")); - apply_client_channel_permission(connection, pe, tag.find(".tab-client-channel")); - return tag.tabify(false); + tab_entry.on('click', () => { + /* Using a timeout here prevents unnecessary style calculations required by other click event handlers */ + setTimeout(() => { + container_tab_list.find(".selected").removeClass("selected"); + tab_entry.addClass("selected"); + label_current.text(tab_entry.find("a").text()); + + /* dont use show() here because it causes a style recalculation */ + for(const element of tag.find(".body .container")) + (element).style.display = "none"; + + permission_editor.html_tag()[0].remove(); + target_container.find(".permission-editor").trigger('show'); + target_container.find(".permission-editor").append(permission_editor.html_tag()); + + for(const element of target_container) + (element).style.display = null; + }, 0); + }); + }; + + create_tab(container_tab_list.find(".sg"), "container-view-server-groups"); + create_tab(container_tab_list.find(".cg"), "container-view-channel-groups"); + create_tab(container_tab_list.find(".chp"), "container-view-channel-permissions"); + create_tab(container_tab_list.find(".clp"), "container-view-client-permissions"); + create_tab(container_tab_list.find(".clchp"), "container-view-client-channel-permissions"); + } + + apply_server_groups(connection, permission_editor, tag.find(".left .container-view-server-groups"), tag.find(".right .container-view-server-groups")); + apply_channel_groups(connection, permission_editor, tag.find(".left .container-view-channel-groups"), tag.find(".right .container-view-channel-groups")); + apply_channel_permission(connection, permission_editor, tag.find(".left .container-view-channel-permissions"), tag.find(".right .container-view-channel-permissions")); + apply_client_permission(connection, permission_editor, tag.find(".left .container-view-client-permissions"), tag.find(".right .container-view-client-permissions"), selected_tab == "clp" ? options : {}); + apply_client_channel_permission(connection, permission_editor, tag.find(".left .container-view-client-channel-permissions"), tag.find(".right .container-view-client-channel-permissions"), selected_tab == "clchp" ? options : {}); + + setTimeout(() => container_tab_list.find("." + (selected_tab || "sg")).trigger('click'), 0); + return tag.dividerfy(); }, footer: undefined, - width: "90%", + min_width: "30em", height: "80%", trigger_tab: false, full_size: true }); const tag = modal.htmlTag; + tag.find(".modal-body").addClass("modal-permission-editor"); + if(selected_tab) + setTimeout(() => tag.find(".tab-header .entry[x-id=" + selected_tab + "]").first().trigger("click"), 1); tag.find(".btn_close").on('click', () => { modal.close(); }); @@ -417,20 +192,18 @@ namespace Modals { return modal; } - function build_channel_tree(connection: ConnectionHandler, channel_list: JQuery, select_callback: (channel: ChannelEntry, icon_update: (id: number) => any) => any) { + function build_channel_tree(connection: ConnectionHandler, channel_list: JQuery, selected_channel: number, select_callback: (channel: ChannelEntry, icon_update: (id: number) => any) => any) { const root = connection.channelTree.get_first_channel(); if(!root) return; - const build_channel = (channel: ChannelEntry) => { - let tag = $.spawn("div").addClass("channel").attr("channel-id", channel.channelId); + const build_channel = (channel: ChannelEntry, level: number) => { + let tag = $.spawn("div").addClass("channel").css("padding-left", "calc(0.25em + " + (level * 16) + "px)").attr("channel-id", channel.channelId); let icon_tag = connection.fileManager.icons.generateTag(channel.properties.channel_icon_id); icon_tag.appendTo(tag); const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); { let name = $.spawn("a").text(channel.channelName() + " (" + channel.channelId + ")").addClass("name"); - //if(connection.channelTree.server.properties. == group.id) - // name.addClass("default"); name.appendTo(tag); } @@ -443,29 +216,33 @@ namespace Modals { return tag; }; - const build_channels = (root: ChannelEntry) => { - build_channel(root).appendTo(channel_list); - for(const child of root.children()) - build_channels(child); - while(root.channel_next) { - root = root.channel_next; - build_channel(root).appendTo(channel_list); - } + const build_channels = (root: ChannelEntry, level: number) => { + build_channel(root, level).appendTo(channel_list); + const child_head = root.children(false).find(e => e.channel_previous === undefined); + if(child_head) + build_channels(child_head, level + 1); + if(root.channel_next) + build_channels(root.channel_next, level) }; - build_channels(root); - setTimeout(() => channel_list.find('.channel').first().trigger('click'), 0); + build_channels(root, 0); + + let selected_channel_tag = channel_list.find(".channel[channel-id=" + selected_channel + "]"); + if(!selected_channel_tag || selected_channel_tag.length < 1) + selected_channel_tag = channel_list.find('.channel').first(); + setTimeout(() => selected_channel_tag.trigger('click'), 0); } - function apply_client_channel_permission(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) { + function apply_client_channel_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery, options: OptionsClientChannelPermissions) { let current_cldbid: number = 0; let current_channel: ChannelEntry; /* the editor */ { - const pe_client = tab_tag.find("permission-editor.client-channel"); - tab_tag.on('show', event => { - console.error("Channel tab show"); - pe_client.append(editor.container); + const pe_client = tab_right.find(".permission-editor"); + tab_right.on('show', event => { + console.error("client channel tab show"); + editor.set_toggle_button(undefined, undefined); + pe_client.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) { if(current_cldbid && current_channel) editor.set_mode(PermissionEditorMode.VISIBLE); @@ -488,11 +265,12 @@ namespace Modals { }); }); - editor.set_listener((permission, value) => { + /* TODO: Error handling? */ + editor.set_listener(async (permission, value) => { if (!current_cldbid) - return Promise.reject("unset client"); + throw "unset client"; if (!current_channel) - return Promise.reject("unset channel"); + throw "unset channel"; if (value.remove) { /* remove the permission */ @@ -502,7 +280,7 @@ namespace Modals { permission.id, ); - return connection.serverConnection.send_command("channelclientdelperm", { + await connection.serverConnection.send_command("channelclientdelperm", { cldbid: current_cldbid, cid: current_channel.channelId, permid: permission.id, @@ -514,7 +292,7 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("channelclientdelperm", { + await connection.serverConnection.send_command("channelclientdelperm", { cldbid: current_cldbid, cid: current_channel.channelId, permid: permission.id_grant(), @@ -531,13 +309,13 @@ namespace Modals { value.flag_negate ); - return connection.serverConnection.send_command("channelclientaddperm", { + await connection.serverConnection.send_command("channelclientaddperm", { cldbid: current_cldbid, cid: current_channel.channelId, permid: permission.id, permvalue: value.value, permskip: value.flag_skip, - permnegate: value.flag_negate + permnegated: value.flag_negate }); } else { log.info(LogCategory.PERMISSIONS, tr("Adding or updating client channel grant permission %s. permission.{id: %o, value: %o}"), @@ -546,13 +324,13 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("channelclientaddperm", { + await connection.serverConnection.send_command("channelclientaddperm", { cldbid: current_cldbid, cid: current_channel.channelId, permid: permission.id_grant(), permvalue: value.granted, permskip: false, - permnegate: false + permnegated: false }); } } @@ -563,7 +341,7 @@ namespace Modals { }); } - build_channel_tree(connection, tab_tag.find(".list-channel .entries"), channel => { + build_channel_tree(connection, tab_left.find(".list-channel .entries"), options.channel_id || 0, channel => { if(current_channel == channel) return; current_channel = channel; @@ -573,18 +351,21 @@ namespace Modals { }); { - const tag_select_uid = tab_tag.find(".client-select input"); - const tag_select_error = tab_tag.find(".client-select .invalid-feedback"); - const tag_client_name = tab_tag.find(".client-name"); - const tag_client_uid = tab_tag.find(".client-uid"); - const tag_client_dbid = tab_tag.find(".client-dbid"); + const tag_select = tab_left.find(".client-select"); + const tag_select_uid = tag_select.find("input"); + const tag_select_error = tag_select.find(".invalid-feedback"); + + const tag_client_name = tab_left.find(".client-name"); + const tag_client_uid = tab_left.find(".client-uid"); + const tag_client_dbid = tab_left.find(".client-dbid"); + const resolve_client = () => { let client_uid = tag_select_uid.val() as string; connection.serverConnection.command_helper.info_from_uid(client_uid).then(result => { if(!result || result.length == 0) return Promise.reject("invalid data"); - tag_select_uid.attr('pattern', null).removeClass('is-invalid'); + tag_select.removeClass('is-invalid'); tag_client_name.val(result[0].client_nickname ); tag_client_uid.val(result[0].client_unique_id); @@ -606,25 +387,30 @@ namespace Modals { tag_client_dbid.val(""); tag_select_error.text(error); - tag_select_uid.attr('pattern', '^[a]{1000}$').addClass('is-invalid'); + tag_select.addClass('is-invalid'); editor.set_mode(PermissionEditorMode.UNSET); }); }; - tab_tag.find(".client-select-uid").on('change', event => resolve_client()); + tag_select_uid.on('change', event => resolve_client()); + if(options.unique_id) { + tag_select_uid.val(options.unique_id); + setTimeout(() => resolve_client()); + } } } - function apply_client_permission(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) { + function apply_client_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery, options: OptionsClientPermissions) { let current_cldbid: number = 0; /* the editor */ { - const pe_client = tab_tag.find("permission-editor.client"); - tab_tag.on('show', event => { + const pe_client = tab_right.find("permission-editor.client"); + tab_right.on('show', event => { console.error("Channel tab show"); - pe_client.append(editor.container); + editor.set_toggle_button(undefined, undefined); + pe_client.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CLIENT_PERMISSION_LIST).granted(1)) { if(current_cldbid) editor.set_mode(PermissionEditorMode.VISIBLE); @@ -646,9 +432,10 @@ namespace Modals { }); }); - editor.set_listener((permission, value) => { + /* TODO: Error handling? */ + editor.set_listener(async (permission, value) => { if (!current_cldbid) - return Promise.reject("unset client"); + throw "unset client"; if (value.remove) { /* remove the permission */ @@ -658,7 +445,7 @@ namespace Modals { permission.id, ); - return connection.serverConnection.send_command("clientaddperm", { + await connection.serverConnection.send_command("clientdelperm", { cldbid: current_cldbid, permid: permission.id, }); @@ -669,7 +456,7 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("clientaddperm", { + await connection.serverConnection.send_command("clientdelperm", { cldbid: current_cldbid, permid: permission.id_grant(), }); @@ -685,12 +472,12 @@ namespace Modals { value.flag_negate ); - return connection.serverConnection.send_command("clientaddperm", { + await connection.serverConnection.send_command("clientaddperm", { cldbid: current_cldbid, permid: permission.id, permvalue: value.value, permskip: value.flag_skip, - permnegate: value.flag_negate + permnegated: value.flag_negate }); } else { log.info(LogCategory.PERMISSIONS, tr("Adding or updating client grant permission %s. permission.{id: %o, value: %o}"), @@ -699,12 +486,12 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("clientaddperm", { + await connection.serverConnection.send_command("clientaddperm", { cldbid: current_cldbid, permid: permission.id_grant(), permvalue: value.granted, permskip: false, - permnegate: false + permnegated: false }); } } @@ -716,18 +503,19 @@ namespace Modals { } - const tag_select_uid = tab_tag.find(".client-select input"); - const tag_select_error = tab_tag.find(".client-select .invalid-feedback"); + const tag_select = tab_left.find(".client-select"); + const tag_select_uid = tag_select.find("input"); + const tag_select_error = tag_select.find(".invalid-feedback"); - const tag_client_name = tab_tag.find(".client-name"); - const tag_client_uid = tab_tag.find(".client-uid"); - const tag_client_dbid = tab_tag.find(".client-dbid"); + const tag_client_name = tab_left.find(".client-name"); + const tag_client_uid = tab_left.find(".client-uid"); + const tag_client_dbid = tab_left.find(".client-dbid"); const resolve_client = () => { let client_uid = tag_select_uid.val() as string; connection.serverConnection.command_helper.info_from_uid(client_uid).then(result => { if(!result || result.length == 0) return Promise.reject("invalid data"); - tag_select_uid.attr('pattern', null).removeClass('is-invalid'); + tag_select.removeClass("is-invalid"); tag_client_name.val(result[0].client_nickname ); tag_client_uid.val(result[0].client_unique_id); @@ -749,24 +537,29 @@ namespace Modals { tag_client_dbid.val(""); tag_select_error.text(error); - tag_select_uid.attr('pattern', '^[a]{1000}$').addClass('is-invalid'); + tag_select.addClass("is-invalid"); editor.set_mode(PermissionEditorMode.UNSET); }); }; - tab_tag.find(".client-select-uid").on('change', event => resolve_client()); + tag_select_uid.on('change', event => resolve_client()); + if(options.unique_id) { + tag_select_uid.val(options.unique_id); + setTimeout(() => resolve_client()); + } } - function apply_channel_permission(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) { + function apply_channel_permission(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { let current_channel: ChannelEntry | undefined; let update_channel_icon: (id: number) => any; /* the editor */ { - const pe_channel = tab_tag.find("permission-editor.channel"); - tab_tag.on('show', event => { + const pe_channel = tab_right.find(".permission-editor"); + tab_right.on('show', event => { console.error("Channel tab show"); - pe_channel.append(editor.container); + editor.set_toggle_button(undefined, undefined); + pe_channel.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNEL_PERMISSION_LIST).granted(1)) editor.set_mode(PermissionEditorMode.VISIBLE); else { @@ -782,9 +575,9 @@ namespace Modals { }); }); - editor.set_listener((permission, value) => { + editor.set_listener(async (permission, value) => { if (!current_channel) - return Promise.reject("unset channel"); + throw "unset channel"; if (value.remove) { /* remove the permission */ @@ -794,7 +587,7 @@ namespace Modals { permission.id, ); - return connection.serverConnection.send_command("channeldelperm", { + await connection.serverConnection.send_command("channeldelperm", { cid: current_channel.channelId, permid: permission.id, }).then(e => { @@ -810,7 +603,7 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("channeldelperm", { + await connection.serverConnection.send_command("channeldelperm", { cid: current_channel.channelId, permid: permission.id_grant(), }); @@ -826,12 +619,12 @@ namespace Modals { value.flag_negate ); - return connection.serverConnection.send_command("channeladdperm", { + await connection.serverConnection.send_command("channeladdperm", { cid: current_channel.channelId, permid: permission.id, permvalue: value.value, permskip: value.flag_skip, - permnegate: value.flag_negate + permnegated: value.flag_negate }).then(e => { if(permission.name === "i_icon_id" && update_channel_icon) update_channel_icon(value.value); @@ -845,12 +638,12 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("channeladdperm", { + await connection.serverConnection.send_command("channeladdperm", { cid: current_channel.channelId, permid: permission.id_grant(), permvalue: value.granted, permskip: false, - permnegate: false + permnegated: false }); } } @@ -861,23 +654,27 @@ namespace Modals { }); } - let channel_list = tab_tag.find(".list-channel .entries"); - build_channel_tree(connection, channel_list, (channel, update) => { + let channel_list = tab_left.find(".list-channel .entries"); + build_channel_tree(connection, channel_list, 0, (channel, update) => { current_channel = channel; update_channel_icon = update; editor.trigger_update(); }); } - function apply_channel_groups(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) { + function apply_channel_groups(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { let current_group; let update_group_icon: (id: number) => any; + let update_groups: (selected_group: number) => any; + let update_buttons: () => any; + /* the editor */ { - const pe_server = tab_tag.find("permission-editor.group-channel"); - tab_tag.on('show', event => { + const pe_server = tab_right.find(".permission-editor"); + tab_right.on('show', event => { console.error("Channel group tab show"); - pe_server.append(editor.container); + editor.set_toggle_button(undefined, undefined); + pe_server.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_PERMISSION_LIST).granted(1)) editor.set_mode(PermissionEditorMode.VISIBLE); else { @@ -893,9 +690,9 @@ namespace Modals { }); }); - editor.set_listener((permission, value) => { + editor.set_listener(async (permission, value) => { if (!current_group) - return Promise.reject("unset channel group"); + throw "unset channel group"; if (value.remove) { /* remove the permission */ @@ -905,7 +702,7 @@ namespace Modals { permission.id, ); - return connection.serverConnection.send_command("channelgroupdelperm", { + await connection.serverConnection.send_command("channelgroupdelperm", { cgid: current_group.id, permid: permission.id, }).then(e => { @@ -920,7 +717,7 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("channelgroupdelperm", { + await connection.serverConnection.send_command("channelgroupdelperm", { cgid: current_group.id, permid: permission.id_grant(), }); @@ -936,12 +733,12 @@ namespace Modals { value.flag_negate ); - return connection.serverConnection.send_command("channelgroupaddperm", { + await connection.serverConnection.send_command("channelgroupaddperm", { cgid: current_group.id, permid: permission.id, permvalue: value.value, permskip: value.flag_skip, - permnegate: value.flag_negate + permnegated: value.flag_negate }).then(e => { if(permission.name === "i_icon_id" && update_group_icon) update_group_icon(value.value); @@ -954,12 +751,12 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("channelgroupaddperm", { + await connection.serverConnection.send_command("channelgroupaddperm", { cgid: current_group.id, permid: permission.id_grant(), permvalue: value.granted, permskip: false, - permnegate: false + permnegated: false }); } } @@ -970,119 +767,403 @@ namespace Modals { }); } - /* list all channel groups */ { - let group_list = tab_tag.find(".list-group-channel .entries"); + let group_list = tab_left.find(".list-groups .entries"); - const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1); - const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1); - for(let group of connection.groups.channelGroups.sort(GroupManager.sorter())) { - if(group.type == GroupType.QUERY) { - if(!allow_query_groups) - continue; - } else if(group.type == GroupType.TEMPLATE) { - if(!allow_template_groups) - continue; + update_groups = (selected_group: number) => { + group_list.children().remove(); + + const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1); + const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1); + for (let group of connection.groups.channelGroups.sort(GroupManager.sorter())) { + if (group.type == GroupType.QUERY) { + if (!allow_query_groups) + continue; + } else if (group.type == GroupType.TEMPLATE) { + if (!allow_template_groups) + continue; + } + + let tag = $.spawn("div").addClass("group").attr("group-id", group.id); + let icon_tag = connection.fileManager.icons.generateTag(group.properties.iconid); + icon_tag.appendTo(tag); + const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); + + { + let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name"); + if (group.properties.savedb) + name.addClass("savedb"); + if (connection.channelTree.server.properties.virtualserver_default_channel_group == group.id) + name.addClass("default"); + name.appendTo(tag); + } + tag.appendTo(group_list); + + tag.on('click', event => { + current_group = group; + update_group_icon = _update_icon; + group_list.find(".selected").removeClass("selected"); + tag.addClass("selected"); + + update_buttons(); + //TODO trigger only if the editor is in channel group mode! + editor.trigger_update(); + }); + tag.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Create a channel group"), + icon_class: 'client-add', + invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1), + callback: () => tab_left.find(".button-add").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Rename channel group"), + icon_class: 'client-edit', + invalidPermission: !connection.permissions.neededPermission(PermissionType.I_CHANNEL_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower), + callback: () => tab_left.find(".button-rename").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Duplicate channel group"), + icon_class: 'client-copy', + callback: () => tab_left.find(".button-duplicate").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Delete channel group"), + icon_class: 'client-delete', + invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_DELETE).granted(1), + callback: () => tab_left.find(".button-delete").trigger('click') + }); + event.preventDefault(); + }); + if(group.id === selected_group) { + setTimeout(() => tag.trigger('click'), 0); + selected_group = undefined; + } } - let tag = $.spawn("div").addClass("group").attr("group-id", group.id); - let icon_tag = connection.fileManager.icons.generateTag(group.properties.iconid); - icon_tag.appendTo(tag); - const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); - - { - let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name"); - if(group.properties.savedb) - name.addClass("savedb"); - if(connection.channelTree.server.properties.virtualserver_default_channel_group == group.id) - name.addClass("default"); - name.appendTo(tag); + /* because the server menu is the first which will be shown */ + if(typeof(selected_group) !== "undefined") { + setTimeout(() => group_list.find('.group').first().trigger('click'), 0); } - tag.appendTo(group_list); + }; - tag.on('click', event => { - current_group = group; - update_group_icon = _update_icon; - group_list.find(".selected").removeClass("selected"); - tag.addClass("selected"); + tab_left.find(".list-groups").on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; - //TODO trigger only if the editor is in channel group mode! - editor.trigger_update(); + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Create a channel group"), + icon_class: 'client-add', + callback: () => tab_left.find(".button-add").trigger('click') }); - } - - /* because the server menu is the first which will be shown */ - setTimeout(() => group_list.find('.group').first().trigger('click'), 0); + event.preventDefault(); + }); } - } - /* - b_virtualserver_servergroup_permission_list - b_virtualserver_channel_permission_list - b_virtualserver_client_permission_list - b_virtualserver_channelgroup_permission_list - b_virtualserver_channelclient_permission_list - b_virtualserver_playlist_permission_list - */ - function apply_server_groups(connection: ConnectionHandler, editor: PermissionEditor, tab_tag: JQuery) { - let current_group: Group; - let current_group_changed; - - /* list all groups */ - let update_icon: (icon_id: number) => any; { - let group_list = tab_tag.find(".list-group-server .entries"); + const container_buttons = tab_left.find(".container-buttons"); - const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1); - const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1); - for(const group of connection.groups.serverGroups.sort(GroupManager.sorter())) { - if(group.type == GroupType.QUERY) { - if(!allow_query_groups) - continue; - } else if(group.type == GroupType.TEMPLATE) { - if(!allow_template_groups) - continue; - } - let tag = $.spawn("div").addClass("group").attr("group-id", group.id); - let icon_tag = connection.fileManager.icons.generateTag(group.properties.iconid); - icon_tag.appendTo(tag); - const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); + const button_add = container_buttons.find(".button-add"); + const button_rename = container_buttons.find(".button-rename"); + const button_duplicate = container_buttons.find(".button-duplicate"); + const button_delete = container_buttons.find(".button-delete"); - { - let name = $.spawn("a").text(group.name + " (" + group.id + ")").addClass("name"); - if(group.properties.savedb) - name.addClass("savedb"); - if(connection.channelTree.server.properties.virtualserver_default_server_group == group.id) - name.addClass("default"); - name.appendTo(tag); - } - tag.appendTo(group_list); + button_add.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); + button_delete.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); + update_buttons = () => { + const permission_modify = current_group && connection.permissions.neededPermission(PermissionType.I_CHANNEL_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower); + button_rename.prop("disabled", !permission_modify); + button_duplicate.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_CHANNELGROUP_CREATE).granted(1)); + }; - tag.on('click', event => { - if(current_group === group) + button_add.on('click', () => { + spawnGroupAdd(false, connection.permissions, (name, type) => name.length > 0 && !connection.groups.channelGroups.find(e => e.target == GroupTarget.CHANNEL && e.name.toLowerCase() === name.toLowerCase() && e.type == type) , (name, type) => { + console.log("Creating channel group: %o, %o", name, type); + connection.serverConnection.send_command('channelgroupadd', { + name: name, + type: type + }).then(() => { + createInfoModal(tr("Group created"), tr("The channel group has been created.")).open(); + update_groups(0); //TODO: May get the created group? + }).catch(error => { + console.warn(tr("Failed to create channel group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to create group"), MessageHelper.formatMessage(tr("Failed to create group:{:br:}"), error)).open(); + }); + }); + }); + + button_rename.on('click', () => { + if(!current_group) + return; + + createInputModal(tr("Rename group"), tr("Enter the new group name"), name => name.length > 0 && !connection.groups.channelGroups.find(e => e.target == GroupTarget.CHANNEL && e.name.toLowerCase() === name.toLowerCase() && e.type == current_group.type), result => { + if(typeof(result) !== "string" || !result) + return; + connection.serverConnection.send_command('channelgrouprename', { + cgid: current_group.id, + name: result + }).then(() => { + createInfoModal(tr("Group renamed"), tr("The channel group has been renamed.")).open(); + update_groups(current_group.id); + }).catch(error => { + console.warn(tr("Failed to rename channel group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to rename group"), MessageHelper.formatMessage(tr("Failed to rename group:{:br:}"), error)).open(); + }); + }).open(); + }); + + button_duplicate.on('click', () => { + createErrorModal(tr("Not implemented yet"), tr("This function hasn't been implemented yet!")).open(); + }); + + button_delete.on('click', () => { + if(!current_group) + return; + + spawnYesNo(tr("Are you sure?"), MessageHelper.formatMessage(tr("Do you really want to delete the group {}?"), current_group.name), result => { + if(result !== true) return; - current_group = group; - update_icon = _update_icon; - current_group_changed(); - - group_list.find(".selected").removeClass("selected"); - tag.addClass("selected"); - editor.trigger_update(); + connection.serverConnection.send_command("channelgroupdel", { + cgid: current_group.id, + force: true + }).then(() => { + createInfoModal(tr("Group deleted"), tr("The channel group has been deleted.")).open(); + update_groups(0); + }).catch(error => { + console.warn(tr("Failed to delete channel group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to delete group"), MessageHelper.formatMessage(tr("Failed to delete group:{:br:}"), error)).open(); + }); }); - } - - /* because the server menu is the first which will be shown */ - setTimeout(() => group_list.find('.group').first().trigger('click'), 0); + }); } + update_groups(0); + } + + function apply_server_groups(connection: ConnectionHandler, editor: AbstractPermissionEditor, tab_left: JQuery, tab_right: JQuery) { + let current_group: Group; + let current_group_changed: (() => any)[] = []; + + let update_buttons: () => any; + /* list all groups */ + + let update_icon: ((icon_id: number) => any)[] = []; + + let update_groups: (selected_group: number) => any; + { + let group_list = tab_left.find(".container-group-list .list-groups .entries"); + let group_list_update_icon: (i: number) => any; + update_icon.push(i => group_list_update_icon(i)); + + update_groups = (selected_group: number) => { + group_list.children().remove(); + + const allow_query_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1); + const allow_template_groups = connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1); + for(const group of connection.groups.serverGroups.sort(GroupManager.sorter())) { + if(group.type == GroupType.QUERY) { + if(!allow_query_groups) + continue; + } else if(group.type == GroupType.TEMPLATE) { + if(!allow_template_groups) + continue; + } + let tag = $.spawn("div").addClass("group").attr("group-id", group.id); + let icon_tag = connection.fileManager.icons.generateTag(group.properties.iconid); + icon_tag.appendTo(tag); + const _update_icon = icon_id => icon_tag.replaceWith(icon_tag = connection.fileManager.icons.generateTag(icon_id)); + + { + let name = $.spawn("div").text(group.name + " (" + group.id + ")").addClass("name"); + if(group.properties.savedb) + name.addClass("savedb"); + if(connection.channelTree.server.properties.virtualserver_default_server_group == group.id) + name.addClass("default"); + name.appendTo(tag); + } + tag.appendTo(group_list); + + tag.on('click', event => { + if(current_group === group) + return; + + current_group = group; + group_list_update_icon = _update_icon; + if(update_buttons) + update_buttons(); + for(const entry of current_group_changed) + entry(); + + group_list.find(".selected").removeClass("selected"); + tag.addClass("selected"); + editor.trigger_update(); + }); + tag.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Create a server group"), + icon_class: 'client-add', + invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1), + callback: () => tab_left.find(".button-add").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Rename server group"), + icon_class: 'client-edit', + invalidPermission: !connection.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower), + callback: () => tab_left.find(".button-rename").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Duplicate server group"), + icon_class: 'client-copy', + callback: () => tab_left.find(".button-duplicate").trigger('click') + }, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Delete server group"), + icon_class: 'client-delete', + invalidPermission: !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_DELETE).granted(1), + callback: () => tab_left.find(".button-delete").trigger('click') + }); + event.preventDefault(); + }); + + if(group.id === selected_group) { + setTimeout(() => tag.trigger('click'), 0); + selected_group = undefined; + } + } + + /* because the server menu is the first which will be shown */ + if(typeof(selected_group) !== "undefined") { + setTimeout(() => group_list.find('.group').first().trigger('click'), 0); + } + }; + + + tab_left.find(".list-groups").on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Create a server group"), + icon_class: 'client-add', + callback: () => tab_left.find(".button-add").trigger('click') + }); + event.preventDefault(); + }); + } + { + const container_buttons = tab_left.find(".container-group-list .container-buttons"); + + const button_add = container_buttons.find(".button-add"); + const button_rename = container_buttons.find(".button-rename"); + const button_duplicate = container_buttons.find(".button-duplicate"); + const button_delete = container_buttons.find(".button-delete"); + + button_add.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1)); + button_delete.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_DELETE).granted(1)); + update_buttons = () => { + const permission_modify = current_group && connection.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MODIFY_POWER).granted(current_group.requiredModifyPower); + button_rename.prop("disabled", !permission_modify); + button_duplicate.prop("disabled", !connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_CREATE).granted(1)); + }; + + button_add.on('click', () => { + spawnGroupAdd(true, connection.permissions, (name, type) => name.length > 0 && !connection.groups.serverGroups.find(e => e.target == GroupTarget.SERVER && e.name.toLowerCase() === name.toLowerCase() && e.type == type) , (name, type) => { + console.log("Creating group: %o, %o", name, type); + connection.serverConnection.send_command('servergroupadd', { + name: name, + type: type + }).then(() => { + createInfoModal(tr("Group created"), tr("The server group has been created.")).open(); + update_groups(0); //TODO: May get the created group? + }).catch(error => { + console.warn(tr("Failed to create server group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to create group"), MessageHelper.formatMessage(tr("Failed to create group:{:br:}"), error)).open(); + }); + }); + }); + + button_rename.on('click', () => { + if(!current_group) + return; + + createInputModal(tr("Rename group"), tr("Enter the new group name"), name => name.length > 0 && !connection.groups.serverGroups.find(e => e.target == GroupTarget.SERVER && e.name.toLowerCase() === name.toLowerCase() && e.type == current_group.type), result => { + if(typeof(result) !== "string" || !result) + return; + connection.serverConnection.send_command('servergrouprename', { + sgid: current_group.id, + name: result + }).then(() => { + createInfoModal(tr("Group renamed"), tr("The server group has been renamed.")).open(); + update_groups(current_group.id); + }).catch(error => { + console.warn(tr("Failed to rename server group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to rename group"), MessageHelper.formatMessage(tr("Failed to rename group:{:br:}"), error)).open(); + }); + }).open(); + }); + + button_duplicate.on('click', () => { + createErrorModal(tr("Not implemented yet"), tr("This function hasn't been implemented yet!")).open(); + }); + + button_delete.on('click', () => { + if(!current_group) + return; + + spawnYesNo(tr("Are you sure?"), MessageHelper.formatMessage(tr("Do you really want to delete the group {}?"), current_group.name), result => { + if(result !== true) + return; + + connection.serverConnection.send_command("servergroupdel", { + sgid: current_group.id, + force: true + }).then(() => { + createInfoModal(tr("Group deleted"), tr("The server group has been deleted.")).open(); + update_groups(0); + }).catch(error => { + console.warn(tr("Failed to delete server group: %o"), error); + if(error instanceof CommandResult) { + error = error.extra_message || error.message; + } + createErrorModal(tr("Failed to delete group"), MessageHelper.formatMessage(tr("Failed to delete group:{:br:}"), error)).open(); + }); + }); + }); + } + update_groups(0); /* the editor */ { - const pe_server = tab_tag.find("permission-editor.group-server"); - tab_tag.on('show', event => { + const pe_server = tab_right.find(".permission-editor"); + tab_right.on('show', event => { console.error("Server tab show"); - pe_server.append(editor.container); + pe_server.append(editor.html_tag()); if(connection.permissions.neededPermission(PermissionType.B_VIRTUALSERVER_SERVERGROUP_PERMISSION_LIST).granted(1)) editor.set_mode(PermissionEditorMode.VISIBLE); else { @@ -1090,14 +1171,15 @@ namespace Modals { return; } editor.set_listener_update(() => { + console.log("Updating permissions"); connection.groups.request_permissions(current_group).then(result => editor.set_permissions(result)).catch(error => { console.log(error); //TODO handling? }); }); - editor.set_listener((permission, value) => { + editor.set_listener(async (permission, value) => { if (!current_group) - return Promise.reject("unset server group"); + throw "unset server group"; if (value.remove) { /* remove the permission */ @@ -1107,12 +1189,13 @@ namespace Modals { permission.id, ); - return connection.serverConnection.send_command("servergroupdelperm", { + await connection.serverConnection.send_command("servergroupdelperm", { sgid: current_group.id, permid: permission.id, }).then(e => { - if(permission.name === "i_icon_id" && update_icon) - update_icon(0); + if(permission.name === "i_icon_id") + for(const c of update_icon) + c(0); return e; }); } else { @@ -1122,7 +1205,7 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("servergroupdelperm", { + await connection.serverConnection.send_command("servergroupdelperm", { sgid: current_group.id, permid: permission.id_grant(), }); @@ -1138,15 +1221,16 @@ namespace Modals { value.flag_negate ); - return connection.serverConnection.send_command("servergroupaddperm", { + await connection.serverConnection.send_command("servergroupaddperm", { sgid: current_group.id, permid: permission.id, permvalue: value.value, permskip: value.flag_skip, - permnegate: value.flag_negate + permnegated: value.flag_negate }).then(e => { - if(permission.name === "i_icon_id" && update_icon) - update_icon(value.value); + if(permission.name === "i_icon_id") + for(const c of update_icon) + c(value.value); return e; }); } else { @@ -1156,12 +1240,12 @@ namespace Modals { value.granted, ); - return connection.serverConnection.send_command("servergroupaddperm", { + await connection.serverConnection.send_command("servergroupaddperm", { sgid: current_group.id, permid: permission.id_grant(), permvalue: value.granted, permskip: false, - permnegate: false + permnegated: false }); } } @@ -1173,17 +1257,30 @@ namespace Modals { /* client list */ { - //filter-client-list - const container_clients = tab_tag.find(".container-clients"); - const container_client_list = container_clients.find(".list-clients"); - const input_filter = container_clients.find(".filter-client-list"); + //container-client-list container-group-list + let clients_visible = false; + let selected_client: { + tag: JQuery, + dbid: number + }; + + const container_client_list = tab_left.find(".container-client-list").addClass("hidden"); + const container_group_list = tab_left.find(".container-group-list"); + + const container_selected_group = container_client_list.find(".container-current-group"); + const container_clients = container_client_list.find(".list-clients .entries"); + + const input_filter = container_client_list.find(".filter-client-list"); + + const button_add = container_client_list.find(".button-add"); + const button_delete = container_client_list.find(".button-delete"); const update_filter = () => { const filter_text = (input_filter.val() || "").toString().toLowerCase(); if(!filter_text) { - container_client_list.find(".entry").css('display', 'block'); + container_clients.find(".entry").css('display', 'block'); } else { - const entries = container_client_list.find(".entry"); + const entries = container_clients.find(".entry"); for(const _entry of entries) { const entry = $(_entry); if(entry.attr("search-string").toLowerCase().indexOf(filter_text) !== -1) @@ -1195,17 +1292,24 @@ namespace Modals { }; const update_client_list = () => { - container_client_list.empty(); + container_clients.empty(); + button_delete.prop('disabled', true); connection.serverConnection.command_helper.request_clients_by_server_group(current_group.id).then(clients => { for(const client of clients) { - const tag = $.spawn("div").addClass("entry").text(client.client_nickname); + const tag = $.spawn("div").addClass("client").text(client.client_nickname); tag.attr("search-string", client.client_nickname + "-" + client.client_unique_identifier + "-" + client.client_database_id); - container_client_list.append(tag); + container_clients.append(tag); tag.on('click contextmenu', event => { - container_client_list.find(".selected").removeClass("selected"); + container_clients.find(".selected").removeClass("selected"); tag.addClass("selected"); + + selected_client = { + tag: tag, + dbid: client.client_database_id + }; + button_delete.prop('disabled', false); }); tag.on('contextmenu', event => { @@ -1214,17 +1318,15 @@ namespace Modals { event.preventDefault(); contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add client"), + icon_class: 'client-add', + callback: () => button_add.trigger('click') + }, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Remove client"), icon_class: 'client-delete', - callback: () => { - connection.serverConnection.send_command("servergroupdelclient", { - sgid: current_group.id, - cldbid: client.client_database_id - }).then(() => { - tag.detach(); - }); - } + callback: () => button_delete.trigger('click') }, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Copy unique id"), @@ -1238,11 +1340,175 @@ namespace Modals { if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) return; console.warn(tr("Failed to receive server group clients for group %d: %o"), current_group.id, error); - }) + }); }; + current_group_changed.push(update_client_list); - input_filter.on('change keyup', event => update_filter()); - current_group_changed = update_client_list; + button_delete.on('click', event => { + const client = selected_client; + if(!client) return; + + connection.serverConnection.send_command("servergroupdelclient", { + sgid: current_group.id, + cldbid: client.dbid + }).then(() => { + selected_client.tag.detach(); + button_delete.prop('disabled', true); /* nothing is selected */ + }).catch(error => { + console.log(tr("Failed to delete client %o from server group %o: %o"), client.dbid, current_group.id, error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to remove client"), tr("Failed to remove client from server group")).open(); + }); + }); + + button_add.on('click', event => { + createInputModal(tr("Add client to server group"), tr("Enter the client unique id or database id"), text => { + if(!text) return false; + if(!!text.match(/^[0-9]+$/)) + return true; + try { + return atob(text).length >= 20; + } catch(error) { + return false; + } + }, async text => { + if(typeof(text) !== "string") + return; + + let dbid; + if(!!text.match(/^[0-9]+$/)) { + dbid = parseInt(text); + debugger; + } else { + try { + const data = await connection.serverConnection.command_helper.info_from_uid(text.trim()); + dbid = data[0].client_database_id; + } catch(error) { + console.log(tr("Failed to resolve client database id from unique id (%s): %o"), text, error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to add client"), MessageHelper.formatMessage(tr("Failed to add client to server group\nFailed to resolve database id: {}."), error)).open(); + return; + } + } + if(!dbid) { + console.log(tr("Failed to resolve client database id from unique id (%s): Client not found")); + createErrorModal(tr("Failed to add client"), tr("Failed to add client to server group\nClient database id not found")).open(); + return; + } + + + connection.serverConnection.send_command("servergroupaddclient", { + sgid: current_group.id, + cldbid: dbid + }).then(() => { + update_client_list(); + }).catch(error => { + console.log(tr("Failed to add client %o to server group %o: %o"), dbid, current_group.id, error); + if(error instanceof CommandResult) + error = error.extra_message || error.message; + createErrorModal(tr("Failed to add client"), tr("Failed to add client to server group\n" + error)).open(); + }); + }).open(); + }); + + container_client_list.on('contextmenu', event => { + if(event.isDefaultPrevented()) + return; + + event.preventDefault(); + contextmenu.spawn_context_menu(event.pageX, event.pageY, { + type: contextmenu.MenuEntryType.ENTRY, + name: tr("Add client"), + icon_class: 'client-add', + callback: () => button_add.trigger('click') + }) + }); + + /* icon handler and current group display */ + { + let update_icon_callback: (i: number) => any; + update_icon.push(i => update_icon_callback(i)); + + input_filter.on('change keyup', event => update_filter()); + current_group_changed.push(() => { + container_selected_group.empty(); + if(!current_group) return; + + let icon_container = $.spawn("div").addClass("icon-container").appendTo(container_selected_group); + + connection.fileManager.icons.generateTag(current_group.properties.iconid).appendTo(icon_container); + update_icon_callback = icon => { + icon_container.empty(); + connection.fileManager.icons.generateTag(icon).appendTo(icon_container); + }; + $.spawn("div").addClass("name").text(current_group.name + " (" + current_group.id + ")").appendTo(container_selected_group); + }); + } + + tab_right.on('show', event => { + editor.set_toggle_button(() => { + clients_visible = !clients_visible; + + container_client_list.toggleClass("hidden", !clients_visible); + container_group_list.toggleClass("hidden", clients_visible); + + return clients_visible ? tr("Hide clients in group") : tr("Show clients in group"); + }, clients_visible ? tr("Hide clients in group") : tr("Show clients in group")); + }); } } + + function spawnGroupAdd(server_group: boolean, permissions: PermissionManager, valid_name: (name: string, group_type: number) => boolean, callback: (group_name: string, group_type: number) => any) { + let modal: Modal; + modal = createModal({ + header: tr("Create a new group"), + body: () => { + let tag = $("#tmpl_group_add").renderTag({ + server_group: server_group + }); + + tag.find(".group-type-template").prop("disabled", !permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1)); + tag.find(".group-type-query").prop("disabled", !permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1)); + + const container_name = tag.find(".group-name"); + const button_create = tag.find(".button-create"); + + const group_type = () => (tag.find(".group-type")[0] as HTMLSelectElement).selectedIndex; + container_name.on('keyup change', (event: Event) => { + if(event.type === 'keyup') { + const kevent = event as KeyboardEvent; + if(!kevent.shiftKey && kevent.key == 'Enter') { + button_create.trigger('click'); + return; + } + } + const valid = valid_name(container_name.val() as string, group_type()); + button_create.prop("disabled", !valid); + container_name.parent().toggleClass("is-invalid", !valid); + }).trigger('change'); + tag.find(".group-type").on('change', () => container_name.trigger('change')); + + button_create.on('click', event => { + if(button_create.prop("disabled")) + return; + button_create.prop("disabled", true); /* disable double clicking */ + + modal.close(); + callback(container_name.val() as string, group_type()); + }); + return tag; + }, + footer: null, + + width: 600 + }); + modal.htmlTag.find(".modal-body").addClass("modal-group-add"); + modal.open_listener.push(() => { + modal.htmlTag.find(".group-name").focus(); + }); + + modal.open(); + } } \ No newline at end of file diff --git a/shared/js/ui/modal/permission/PermissionEditor.ts b/shared/js/ui/modal/permission/PermissionEditor.ts deleted file mode 100644 index 3a7e8b75..00000000 --- a/shared/js/ui/modal/permission/PermissionEditor.ts +++ /dev/null @@ -1,1297 +0,0 @@ -namespace ui { - export namespace scheme { - export interface CheckBox { - border: string; - checkmark: string; - checkmark_font: string; - - background_checked: string; - background_checked_hovered: string; - - background: string; - background_hovered: string; - } - - export interface TextField { - color: string; - font: string; - - background: string; - background_hovered: string; - } - - export interface ColorScheme { - permission: { - background: string; - background_selected: string; - - name: string; - name_unset: string; - name_font: string; - - value: TextField; - value_b: CheckBox; - granted: TextField; - negate: CheckBox; - skip: CheckBox; - } - - group: { - name: string; - name_font: string; - } - } - } - - export enum RepaintMode { - NONE, - REPAINT, - REPAINT_OBJECT_FULL, - REPAINT_FULL - } - - export interface AxisAlignedBoundingBox { - x: number; - y: number; - - width: number; - height: number; - } - - export enum ClickEventType { - SIGNLE, - DOUBLE, - CONTEXT_MENU - } - - export interface InteractionClickEvent { - type: ClickEventType; - consumed: boolean; - offset_x: number; - offset_y: number; - } - - export interface InteractionListener { - region: AxisAlignedBoundingBox; - region_weight: number; - - /** - * @return true if a redraw is required - */ - on_mouse_enter?: () => RepaintMode; - - /** - * @return true if a redraw is required - */ - on_mouse_leave?: () => RepaintMode; - - /** - * @return true if a redraw is required - */ - on_click?: (event: InteractionClickEvent) => RepaintMode; - - mouse_cursor?: string; - - set_full_draw?: () => any; - disabled?: boolean; - } - - abstract class DrawableObject { - abstract draw(context: CanvasRenderingContext2D, full: boolean); - - private _object_full_draw = false; - private _width: number = 0; - set_width(value: number) { - this._width = value; - } - request_full_draw() { - this._object_full_draw = true; - } - pop_full_draw() { - const result = this._object_full_draw; - this._object_full_draw = false; - return result; - } - - width() { return this._width; } - abstract height(); - - private _transforms: DOMMatrix[] = []; - protected push_transform(context: CanvasRenderingContext2D) { - this._transforms.push(context.getTransform()); - } - - protected pop_transform(context: CanvasRenderingContext2D) { - const transform = this._transforms.pop(); - context.setTransform( - transform.a, - transform.b, - transform.c, - transform.d, - transform.e, - transform.f - ); - } - - protected original_x(context: CanvasRenderingContext2D, x: number) { - return context.getTransform().e + x; - } - protected original_y(context: CanvasRenderingContext2D, y: number) { - return context.getTransform().f + y; - } - - protected colors: scheme.ColorScheme = {} as any; - set_color_scheme(scheme: scheme.ColorScheme) { - this.colors = scheme; - } - - protected manager: PermissionEditor; - - set_manager(manager: PermissionEditor) { - this.manager = manager; - } - abstract initialize(); - abstract finalize(); - } - - class PermissionGroup extends DrawableObject { - public static readonly HEIGHT = parseFloat(getComputedStyle(document.documentElement).fontSize) * (3/2); /* 24 */ - public static readonly ARROW_SIZE = 10; /* 12 */ - - group: GroupedPermissions; - _sub_elements: PermissionGroup[] = []; - _element_permissions: PermissionList; - - collapsed = false; - private _listener_colaps: InteractionListener; - - constructor(group: GroupedPermissions) { - super(); - - this.group = group; - - this._element_permissions = new PermissionList(this.group.permissions); - for(const sub of this.group.children) - this._sub_elements.push(new PermissionGroup(sub)); - } - - draw(context: CanvasRenderingContext2D, full: boolean) { - const _full = this.pop_full_draw() || full; - this.push_transform(context); - context.translate(PermissionGroup.ARROW_SIZE + 20, PermissionGroup.HEIGHT); - - let sum_height = 0; - /* let first draw the elements, because if the sum height is zero then we could hide ourselves */ - if(!this.collapsed) { /* draw the next groups */ - for(const group of this._sub_elements) { - group.draw(context, full); - - const height = group.height(); - sum_height += height; - context.translate(0, height); - } - - this._element_permissions.draw(context, full); - if(sum_height == 0) - sum_height += this._element_permissions.height(); - } else { - const process_group = (group: PermissionGroup) => { - for(const g of group._sub_elements) - process_group(g); - group._element_permissions.handle_hide(); - if(sum_height == 0 && group._element_permissions.height() > 0){ - sum_height = 1; - } - }; - process_group(this); - } - this.pop_transform(context); - - if(_full && sum_height > 0) { - const arrow_stretch = 2/3; - if(!full) { - context.clearRect(0, 0, this.width(), PermissionGroup.HEIGHT); - } - context.fillStyle = this.colors.group.name; - - /* arrow */ - { - const x1 = this.collapsed ? PermissionGroup.ARROW_SIZE * arrow_stretch / 2 : 0; - const y1 = (PermissionGroup.HEIGHT - PermissionGroup.ARROW_SIZE) / 2 + (this.collapsed ? 0 : PermissionGroup.ARROW_SIZE * arrow_stretch / 2); /* center arrow */ - - const x2 = this.collapsed ? x1 + PermissionGroup.ARROW_SIZE * arrow_stretch : x1 + PermissionGroup.ARROW_SIZE / 2; - const y2 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE / 2 : y1 + PermissionGroup.ARROW_SIZE * arrow_stretch; - - const x3 = this.collapsed ? x1 : x1 + PermissionGroup.ARROW_SIZE; - const y3 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE : y1; - - context.beginPath(); - context.moveTo(x1, y1); - - context.lineTo(x2, y2); - context.lineTo(x3, y3); - - context.moveTo(x2, y2); - context.lineTo(x3, y3); - context.fill(); - - this._listener_colaps.region.x = this.original_x(context, 0); - this._listener_colaps.region.y = this.original_y(context, y1); - } - /* text */ - { - context.font = this.colors.group.name_font; - context.textBaseline = "middle"; - context.textAlign = "start"; - - context.fillText(this.group.group.name, PermissionGroup.ARROW_SIZE + 5, PermissionGroup.HEIGHT / 2); - } - } - } - - set_width(value: number) { - super.set_width(value); - for(const element of this._sub_elements) - element.set_width(value - PermissionGroup.ARROW_SIZE - 20); - this._element_permissions.set_width(value - PermissionGroup.ARROW_SIZE - 20); - } - - set_color_scheme(scheme: scheme.ColorScheme) { - super.set_color_scheme(scheme); - for(const child of this._sub_elements) - child.set_color_scheme(scheme); - this._element_permissions.set_color_scheme(scheme); - } - - set_manager(manager: PermissionEditor) { - super.set_manager(manager); - for(const child of this._sub_elements) - child.set_manager(manager); - this._element_permissions.set_manager(manager); - } - - height() { - let result = 0; - - if(!this.collapsed) { - for(const element of this._sub_elements) - result += element.height(); - - result += this._element_permissions.height(); - } else { - //We've to figure out if we have permissions - const process_group = (group: PermissionGroup) => { - if(result == 0 && group._element_permissions.height() > 0){ - result = 1; - } else { - for(const g of group._sub_elements) - process_group(g); - } - }; - process_group(this); - - if(result > 0) - return PermissionGroup.HEIGHT; - - return 0; - } - if(result > 0) { - result += PermissionGroup.HEIGHT; - return result; - } else { - return 0; - } - } - - initialize() { - for(const child of this._sub_elements) - child.initialize(); - this._element_permissions.initialize(); - - - this._listener_colaps = { - region: { - x: 0, - y: 0, - height: PermissionGroup.ARROW_SIZE, - width: PermissionGroup.ARROW_SIZE - }, - region_weight: 10, - /* - on_mouse_enter: () => { - this.collapsed_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.collapsed_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - */ - on_click: () => { - this.collapsed = !this.collapsed; - return RepaintMode.REPAINT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - - this.manager.intercept_manager().register_listener(this._listener_colaps); - } - - finalize() { - for(const child of this._sub_elements) - child.finalize(); - this._element_permissions.finalize(); - } - - collapse_group() { - for(const child of this._sub_elements) - child.collapse_group(); - - this.collapsed = true; - } - - expend_group() { - for(const child of this._sub_elements) - child.expend_group(); - - this.collapsed = false; - } - } - - class PermissionList extends DrawableObject { - permissions: PermissionEntry[] = []; - - constructor(permissions: PermissionInfo[]) { - super(); - - for(const permission of permissions) - this.permissions.push(new PermissionEntry(permission)); - } - - set_width(value: number) { - super.set_width(value); - for(const entry of this.permissions) - entry.set_width(value); - } - - - draw(context: CanvasRenderingContext2D, full: boolean) { - this.push_transform(context); - - for(const permission of this.permissions) { - permission.draw(context, full); - context.translate(0, permission.height()); - } - - this.pop_transform(context); - } - - height() { - let height = 0; - for(const permission of this.permissions) - height += permission.height(); - return height; - } - - - set_color_scheme(scheme: scheme.ColorScheme) { - super.set_color_scheme(scheme); - for(const entry of this.permissions) - entry.set_color_scheme(scheme); - } - - set_manager(manager: PermissionEditor) { - super.set_manager(manager); - - for(const entry of this.permissions) - entry.set_manager(manager); - } - - initialize() { - for(const entry of this.permissions) - entry.initialize(); - } - - finalize() { - for(const entry of this.permissions) - entry.finalize(); - } - - handle_hide() { - for(const entry of this.permissions) - entry.handle_hide(); - } - } - - class PermissionEntry extends DrawableObject { - public static readonly HEIGHT = PermissionGroup.HEIGHT; /* 24 */ - public static readonly HALF_HEIGHT = PermissionEntry.HEIGHT / 2; - public static readonly CHECKBOX_HEIGHT = PermissionEntry.HEIGHT - 2; - - public static readonly COLUMN_PADDING = 2; - public static readonly COLUMN_VALUE = 75; - public static readonly COLUMN_GRANTED = 75; - //public static readonly COLUMN_NEGATE = 25; - //public static readonly COLUMN_SKIP = 25; - public static readonly COLUMN_NEGATE = 75; - public static readonly COLUMN_SKIP = 75; - - private _permission: PermissionInfo; - - hidden: boolean; - - granted: number = 22; - value: number; - flag_skip: boolean = true; - flag_negate: boolean; - - private _prev_selected = false; - selected: boolean; - - flag_skip_hovered = false; - flag_negate_hovered = false; - flag_value_hovered = false; - flag_grant_hovered = false; - - private _listener_checkbox_skip: InteractionListener; - private _listener_checkbox_negate: InteractionListener; - private _listener_value: InteractionListener; - private _listener_grant: InteractionListener; - private _listener_general: InteractionListener; - private _icon_image: HTMLImageElement | undefined; - - on_icon_select?: (current_id: number) => Promise; - on_context_menu?: (x: number, y: number) => any; - on_grant_change?: () => any; - on_change?: () => any; - - constructor(permission: PermissionInfo) { - super(); - this._permission = permission; - } - - set_icon_id_image(image: HTMLImageElement | undefined) { - if(this._icon_image === image) - return; - this._icon_image = image; - if(image) { - image.height = 16; - image.width = 16; - } - } - - permission() { return this._permission; } - - draw(ctx: CanvasRenderingContext2D, full: boolean) { - if(!this.pop_full_draw() && !full) { /* Note: do not change this order! */ - /* test for update! */ - return; - } - if(this.hidden) { - this.handle_hide(); - return; - } - ctx.lineWidth = 1; - - /* debug box */ - if(false) { - ctx.fillStyle = "#FF0000"; - ctx.fillRect(0, 0, this.width(), PermissionEntry.HEIGHT); - ctx.fillStyle = "#000000"; - ctx.strokeRect(0, 0, this.width(), PermissionEntry.HEIGHT); - } - - if(!full) { - const off = this.selected || this._prev_selected ? ctx.getTransform().e : 0; - ctx.clearRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); - } - - if(this.selected) - ctx.fillStyle = this.colors.permission.background_selected; - else - ctx.fillStyle = this.colors.permission.background; - const off = this.selected ? ctx.getTransform().e : 0; - ctx.fillRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); - this._prev_selected = this.selected; - - /* permission name */ - { - ctx.fillStyle = typeof(this.value) !== "undefined" ? this.colors.permission.name : this.colors.permission.name_unset; - ctx.textBaseline = "middle"; - ctx.textAlign = "start"; - ctx.font = this.colors.permission.name_font; - - ctx.fillText(this._permission.name, 0, PermissionEntry.HALF_HEIGHT); - } - - const original_y = this.original_y(ctx, 0); - const original_x = this.original_x(ctx, 0); - const width = this.width(); - - /* draw granted */ - let w = width - PermissionEntry.COLUMN_GRANTED; - if(typeof(this.granted) === "number") { - this._listener_grant.region.x = original_x + w; - this._listener_grant.region.y = original_y; - - this._draw_number_field(ctx, this.colors.permission.granted, w, 0, PermissionEntry.COLUMN_VALUE, this.granted, this.flag_grant_hovered); - } else { - this._listener_grant.region.y = original_y; - this._listener_grant.region.x = - original_x - + width - - PermissionEntry.COLUMN_GRANTED; - } - - /* draw value and the skip stuff */ - if(typeof(this.value) === "number") { - w -= PermissionEntry.COLUMN_SKIP + PermissionEntry.COLUMN_PADDING; - { - const x = w + (PermissionEntry.COLUMN_SKIP - PermissionEntry.CHECKBOX_HEIGHT) / 2; - const y = 1; - - this._listener_checkbox_skip.region.x = original_x + x; - this._listener_checkbox_skip.region.y = original_y + y; - - this._draw_checkbox_field(ctx, this.colors.permission.skip, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_skip, this.flag_skip_hovered); - } - - w -= PermissionEntry.COLUMN_NEGATE + PermissionEntry.COLUMN_PADDING; - { - const x = w + (PermissionEntry.COLUMN_NEGATE - PermissionEntry.CHECKBOX_HEIGHT) / 2; - const y = 1; - - this._listener_checkbox_negate.region.x = original_x + x; - this._listener_checkbox_negate.region.y = original_y + y; - - this._draw_checkbox_field(ctx, this.colors.permission.negate, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_negate, this.flag_negate_hovered); - } - - w -= PermissionEntry.COLUMN_VALUE + PermissionEntry.COLUMN_PADDING; - if(this._permission.is_boolean()) { - const x = w + PermissionEntry.COLUMN_VALUE - PermissionEntry.CHECKBOX_HEIGHT; - const y = 1; - - this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT; - this._listener_value.region.x = original_x + x; - this._listener_value.region.y = original_y + y; - - this._draw_checkbox_field(ctx, this.colors.permission.value_b, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.value > 0, this.flag_value_hovered); - } else if(this._permission.name === "i_icon_id" && this._icon_image) { - this._listener_value.region.x = original_x + w; - this._listener_value.region.y = original_y; - this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT; - - this._draw_icon_field(ctx, this.colors.permission.value_b, w, 0, PermissionEntry.COLUMN_VALUE, this.flag_value_hovered, this._icon_image); - } else { - this._listener_value.region.width = PermissionEntry.COLUMN_VALUE; - this._listener_value.region.x = original_x + w; - this._listener_value.region.y = original_y; - - this._draw_number_field(ctx, this.colors.permission.value, w, 0, PermissionEntry.COLUMN_VALUE, this.value, this.flag_value_hovered); - } - this._listener_value.disabled = false; - } else { - this._listener_checkbox_skip.region.y = -1e8; - this._listener_checkbox_negate.region.y = -1e8; - - this._listener_value.region.y = original_y; - this._listener_value.region.x = - original_x - + width - - PermissionEntry.COLUMN_GRANTED - - PermissionEntry.COLUMN_NEGATE - - PermissionEntry.COLUMN_VALUE - - PermissionEntry.COLUMN_PADDING * 4; - this._listener_value.disabled = true; - } - - this._listener_general.region.y = original_y; - this._listener_general.region.x = original_x; - } - - handle_hide() { - /* so the listener wound get triggered */ - this._listener_value.region.x = -1e8; - this._listener_grant.region.x = -1e8; - this._listener_checkbox_negate.region.x = -1e8; - this._listener_checkbox_skip.region.x = -1e8; - this._listener_general.region.x = -1e8; - } - - private _draw_icon_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, width: number, hovered: boolean, image: HTMLImageElement) { - const line = ctx.lineWidth; - ctx.lineWidth = 2; - ctx.fillStyle = scheme.border; - ctx.strokeRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2); - ctx.lineWidth = line; - - ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; - ctx.fillRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2); - - const center_y = y + PermissionEntry.HEIGHT / 2; - const center_x = x + PermissionEntry.HEIGHT / 2; - ctx.drawImage(image, center_x - image.width / 2, center_y - image.height / 2); - } - - private _draw_number_field(ctx: CanvasRenderingContext2D, scheme: scheme.TextField, x: number, y: number, width: number, value: number, hovered: boolean) { - ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; - ctx.fillRect(x, y, width, PermissionEntry.HEIGHT); - - ctx.fillStyle = scheme.color; - ctx.font = scheme.font; //Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial"; - ctx.textAlign = "start"; - ctx.fillText(value + "", x, y + PermissionEntry.HALF_HEIGHT, width); - - ctx.strokeStyle = "#6e6e6e"; - const line = ctx.lineWidth; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(x, y + PermissionEntry.HEIGHT - 2); - ctx.lineTo(x + width, y + PermissionEntry.HEIGHT - 2); - ctx.stroke(); - ctx.lineWidth = line; - } - - private _draw_checkbox_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, height: number, checked: boolean, hovered: boolean) { - ctx.fillStyle = scheme.border; - ctx.strokeRect(x, y, height, height); - - - ctx.fillStyle = checked ? - (hovered ? scheme.background_checked_hovered : scheme.background_checked) : - (hovered ? scheme.background_hovered : scheme.background); - ctx.fillRect(x + 1, y + 1, height - 2, height - 2); - - if(checked) { - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillStyle = scheme.checkmark; - ctx.font = scheme.checkmark_font; //Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial"; - ctx.fillText("✓", x + height / 2, y + height / 2); - } - } - - height() { - return this.hidden ? 0 : PermissionEntry.HEIGHT; - } - - set_width(value: number) { - super.set_width(value); - this._listener_general.region.width = value; - } - - initialize() { - this._listener_checkbox_skip = { - region: { - x: -1e8, - y: -1e8, - height: PermissionEntry.CHECKBOX_HEIGHT, - width: PermissionEntry.CHECKBOX_HEIGHT - }, - region_weight: 10, - on_mouse_enter: () => { - this.flag_skip_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.flag_skip_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_click: () => { - this.flag_skip = !this.flag_skip; - if(this.on_change) - this.on_change(); - return RepaintMode.REPAINT_OBJECT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - this._listener_checkbox_negate = { - region: { - x: -1e8, - y: -1e8, - height: PermissionEntry.CHECKBOX_HEIGHT, - width: PermissionEntry.CHECKBOX_HEIGHT - }, - region_weight: 10, - on_mouse_enter: () => { - this.flag_negate_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.flag_negate_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_click: () => { - this.flag_negate = !this.flag_negate; - if(this.on_change) - this.on_change(); - return RepaintMode.REPAINT_OBJECT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - this._listener_value = { - region: { - x: -1e8, - y: -1e8, - height: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.HEIGHT, - width: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.COLUMN_VALUE - }, - region_weight: 10, - on_mouse_enter: () => { - this.flag_value_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.flag_value_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_click: () => { - if(this._permission.is_boolean()) { - this.value = this.value > 0 ? 0 : 1; - if(this.on_change) - this.on_change(); - return RepaintMode.REPAINT_OBJECT_FULL; - } else if(this._permission.name === "i_icon_id") { - this.on_icon_select(this.value).then(value => { - this.value = value; - if(this.on_change) - this.on_change(); - }).catch(error => { - console.warn(tr("Failed to select icon: %o"), error); - }) - } else { - this._spawn_number_edit( - this._listener_value.region.x, - this._listener_value.region.y, - this._listener_value.region.width, - this._listener_value.region.height, - this.colors.permission.value, - this.value || 0, - value => { - if(typeof(value) === "number") { - this.value = value; - this.request_full_draw(); - this.manager.request_draw(false); - if(this.on_change) - this.on_change(); - } - } - ) - } - return RepaintMode.REPAINT_OBJECT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - this._listener_grant = { - region: { - x: -1e8, - y: -1e8, - height: PermissionEntry.HEIGHT, - width: PermissionEntry.COLUMN_VALUE - }, - region_weight: 10, - on_mouse_enter: () => { - this.flag_grant_hovered = true; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - this.flag_grant_hovered = false; - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_click: () => { - this._spawn_number_edit( - this._listener_grant.region.x, - this._listener_grant.region.y, - this._listener_grant.region.width, - this._listener_grant.region.height, - this.colors.permission.granted, - this.granted || 0, //TODO use max assignable value? - value => { - if(typeof(value) === "number") { - this.granted = value; - this.request_full_draw(); - this.manager.request_draw(false); - - if(this.on_grant_change) - this.on_grant_change(); - } - } - ); - return RepaintMode.REPAINT_OBJECT_FULL; - }, - set_full_draw: () => this.request_full_draw(), - mouse_cursor: "pointer" - }; - - this._listener_general = { - region: { - x: -1e8, - y: -1e8, - height: PermissionEntry.HEIGHT, - width: 0 - }, - region_weight: 0, - /* - on_mouse_enter: () => { - return RepaintMode.REPAINT_OBJECT_FULL; - }, - on_mouse_leave: () => { - return RepaintMode.REPAINT_OBJECT_FULL; - }, - */ - on_click: (event: InteractionClickEvent) => { - this.manager.set_selected_entry(this); - - if(event.type == ClickEventType.DOUBLE && typeof(this.value) === "undefined") - return this._listener_value.on_click(event); - else if(event.type == ClickEventType.CONTEXT_MENU) { - const mouse = this.manager.mouse; - if(this.on_context_menu) { - this.on_context_menu(mouse.x, mouse.y); - event.consumed = true; - } - } - return RepaintMode.NONE; - }, - set_full_draw: () => this.request_full_draw(), - }; - - this.manager.intercept_manager().register_listener(this._listener_checkbox_negate); - this.manager.intercept_manager().register_listener(this._listener_checkbox_skip); - this.manager.intercept_manager().register_listener(this._listener_value); - this.manager.intercept_manager().register_listener(this._listener_grant); - this.manager.intercept_manager().register_listener(this._listener_general); - } - - finalize() { } - - private _spawn_number_edit(x: number, y: number, width: number, height: number, color: scheme.TextField, value: number, callback: (new_value?: number) => any) { - const element = $.spawn("div"); - element.prop("contentEditable", true); - element - .css("pointer-events", "none") - .css("background", color.background) - .css("display", "block") - .css("position", "absolute") - .css("top", y) - .css("left", x) - .css("width", width) - .css("height", height) - .css("z-index", 1e6); - element.text(value); - element.appendTo(this.manager.canvas_container); - element.focus(); - - element.on('focusout', event => { - console.log("permission changed to " + element.text()); - if(!isNaN(parseInt(element.text()))) { - callback(parseInt(element.text())); - } else { - callback(undefined); - } - element.remove(); - }); - - element.on('keypress', event => { - if(event.which == KeyCode.KEY_RETURN) - element.trigger('focusout'); - - const text = String.fromCharCode(event.which); - if (isNaN(parseInt(text)) && text != "-") - event.preventDefault(); - - if(element.text().length > 7) - event.preventDefault(); - }); - - if (window.getSelection) { - const selection = window.getSelection(); - const range = document.createRange(); - range.selectNodeContents(element[0]); - selection.removeAllRanges(); - selection.addRange(range); - } - } - - trigger_value_assign() { - this._listener_value.on_click(undefined); - } - trigger_grant_assign() { - this._listener_grant.on_click(undefined); - } - } - - export class InteractionManager { - private _listeners: InteractionListener[] = []; - private _entered_listeners: InteractionListener[] = []; - - register_listener(listener: InteractionListener) { - this._listeners.push(listener); - } - - remove_listener(listener: InteractionListener) { - this._listeners.remove(listener); - } - - process_mouse_move(new_x: number, new_y: number) : { repaint: RepaintMode, cursor: string } { - let _entered_listeners: InteractionListener[] = []; - for(const listener of this._listeners) { - const aabb = listener.region; - - if(listener.disabled) - continue; - - if(new_x < aabb.x || new_x > aabb.x + aabb.width) - continue; - - if(new_y < aabb.y || new_y > aabb.y + aabb.height) - continue; - - _entered_listeners.push(listener); - } - - let repaint: RepaintMode = RepaintMode.NONE; - _entered_listeners.sort((a, b) => (a.region_weight || 0) - (b.region_weight || 0)); - for(const listener of this._entered_listeners) { - if(listener.on_mouse_leave && _entered_listeners.indexOf(listener) == -1) { - let mode = listener.on_mouse_leave(); - if(mode == RepaintMode.REPAINT_OBJECT_FULL) { - mode = RepaintMode.REPAINT; - if(listener.set_full_draw) - listener.set_full_draw(); - } - if(mode > repaint) - repaint = mode; - } - } - for(const listener of _entered_listeners) { - if(listener.on_mouse_enter && this._entered_listeners.indexOf(listener) == -1) { - let mode = listener.on_mouse_enter(); - if(mode == RepaintMode.REPAINT_OBJECT_FULL) { - mode = RepaintMode.REPAINT; - if(listener.set_full_draw) - listener.set_full_draw(); - } - if(mode > repaint) - repaint = mode; - } - } - this._entered_listeners = _entered_listeners; - - let cursor; - for(const listener of _entered_listeners) - if(typeof(listener.mouse_cursor) === "string") { - cursor = listener.mouse_cursor; - } - return { - repaint: repaint, - cursor: cursor - }; - } - - private process_click_event(x: number, y: number, event: InteractionClickEvent) : RepaintMode { - const move_result = this.process_mouse_move(x, y); - - let repaint: RepaintMode = move_result.repaint; - for(const listener of this._entered_listeners) - if(listener.on_click) { - let mode = listener.on_click(event); - if(mode == RepaintMode.REPAINT_OBJECT_FULL){ - mode = RepaintMode.REPAINT; - if(listener.set_full_draw) - listener.set_full_draw(); - } - if(mode > repaint) - repaint = mode; - } - - return repaint; - } - - process_click(x: number, y: number) : RepaintMode { - const event: InteractionClickEvent = { - consumed: false, - type: ClickEventType.SIGNLE, - offset_x: x, - offset_y: y - }; - - return this.process_click_event(x, y, event); - } - process_dblclick(x: number, y: number) : RepaintMode { - const event: InteractionClickEvent = { - consumed: false, - type: ClickEventType.DOUBLE, - offset_x: x, - offset_y: y - }; - - return this.process_click_event(x, y, event); - } - process_context_menu(js_event: MouseEvent, x: number, y: number) : RepaintMode { - const event: InteractionClickEvent = { - consumed: js_event.defaultPrevented, - type: ClickEventType.CONTEXT_MENU, - offset_x: x, - offset_y: y - }; - - const result = this.process_click_event(x, y, event); - if(event.consumed) - js_event.preventDefault(); - return result; - } - } - - export class PermissionEditor { - private static readonly PERMISSION_HEIGHT = PermissionEntry.HEIGHT; - private static readonly PERMISSION_GROUP_HEIGHT = PermissionGroup.HEIGHT; - - readonly grouped_permissions: GroupedPermissions[]; - readonly canvas: HTMLCanvasElement; - readonly canvas_container: HTMLDivElement; - private _max_height: number = 0; - - private _permission_count: number = 0; - private _permission_group_count: number = 0; - private _canvas_context: CanvasRenderingContext2D; - - private _selected_entry: PermissionEntry; - - private _draw_requested: boolean = false; - private _draw_requested_full: boolean = false; - - private _elements: PermissionGroup[] = []; - private _intersect_manager: InteractionManager; - - private _permission_entry_map: {[key: number]:PermissionEntry} = {}; - - mouse: { - x: number, - y: number - } = { - x: 0, - y: 0 - }; - - constructor(permissions: GroupedPermissions[]) { - this.grouped_permissions = permissions; - - this.canvas_container = $.spawn("div") - .addClass("window-resize-listener") /* we want to handle resized */ - .css("min-width", "750px") - .css("position", "relative") - .css("user-select", "none") - [0]; - this.canvas = $.spawn("canvas")[0]; - - this.canvas_container.appendChild(this.canvas); - - this._intersect_manager = new InteractionManager(); - this.canvas_container.onmousemove = event => { - this.mouse.x = event.pageX; - this.mouse.y = event.pageY; - - const draw = this._intersect_manager.process_mouse_move(event.offsetX, event.offsetY); - this.canvas_container.style.cursor = draw.cursor || ""; - this._handle_repaint(draw.repaint); - }; - this.canvas_container.onclick = event => { - this._handle_repaint(this._intersect_manager.process_click(event.offsetX, event.offsetY)); - }; - this.canvas_container.ondblclick = event => { - this._handle_repaint(this._intersect_manager.process_dblclick(event.offsetX, event.offsetY)); - }; - this.canvas_container.oncontextmenu = (event: MouseEvent) => { - this._handle_repaint(this._intersect_manager.process_context_menu(event, event.offsetX, event.offsetY)); - }; - this.canvas_container.onresize = () => this.request_draw(true); - - - this.initialize(); - } - - private _handle_repaint(mode: RepaintMode) { - if(mode == RepaintMode.REPAINT || mode == RepaintMode.REPAINT_FULL) - this.request_draw(mode == RepaintMode.REPAINT_FULL); - } - - request_draw(full?: boolean) { - this._draw_requested_full = this._draw_requested_full || full; - if(this._draw_requested) - return; - this._draw_requested = true; - requestAnimationFrame(() => { - this.draw(this._draw_requested_full); - }); - } - - draw(full?: boolean) { - this._draw_requested = false; - this._draw_requested_full = false; - - /* clear max height */ - this.canvas_container.style.overflowY = "shown"; - this.canvas_container.style.height = undefined; - - const max_height = this._max_height; - const max_width = this.canvas_container.clientWidth; - const update_width = this.canvas.width != max_width; - const full_draw = typeof(full) !== "boolean" || full || update_width; - - if(update_width) { - this.canvas.width = max_width; - for(const element of this._elements) - element.set_width(max_width); - } - - console.log("Drawing%s on %dx%d", full_draw ? " full" : "", max_width, max_height); - if(full_draw) - this.canvas.height = max_height; - const ctx = this._canvas_context; - ctx.resetTransform(); - if(full_draw) - ctx.clearRect(0, 0, max_width, max_height); - - let sum_height = 0; - for(const element of this._elements) { - element.draw(ctx, full_draw); - const height = element.height(); - sum_height += height; - ctx.translate(0, height); - } - - this.canvas_container.style.overflowY = "hidden"; - this.canvas_container.style.height = sum_height + "px"; - } - - private initialize() { - /* setup the canvas */ - { - const apply_group = (group: GroupedPermissions) => { - for(const g of group.children || []) - apply_group(g); - this._permission_group_count++; - this._permission_count += group.permissions.length; - }; - for(const group of this.grouped_permissions) - apply_group(group); - - this._max_height = this._permission_count * PermissionEditor.PERMISSION_HEIGHT + this._permission_group_count * PermissionEditor.PERMISSION_GROUP_HEIGHT; - console.log("%d permissions and %d groups required %d height", this._permission_count, this._permission_group_count, this._max_height); - - this.canvas.style.width = "100%"; - - this.canvas.style.flexShrink = "0"; - this.canvas_container.style.flexShrink = "0"; - - this._canvas_context = this.canvas.getContext("2d"); - } - - const font = Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial"; - const font_checkmark = Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial"; - const checkbox = { - background: "#FFFFFF", - background_hovered: "#CCCCCC", - - background_checked: "#0000AA", - background_checked_hovered: "#0000AA77", - - border: "#000000", - checkmark: "#FFFFFF", - checkmark_font: font_checkmark - }; - const input: scheme.TextField = { - color: "#000000", - font: font, - - background_hovered: "#CCCCCCCC", - background: "#FFFFFF00" - }; - - const color_scheme: scheme.ColorScheme = { - group: { - name: "black", - name_font: font - }, - - permission: { - name: "black", - name_unset: "#888888", - name_font: font, - - background: "#FFFFFF", - background_selected: "#00007788", - - value: input, - value_b: checkbox, - granted: input, - negate: checkbox, - skip: checkbox - } - }; - (window as any).scheme = color_scheme; - /* setup elements to draw */ - { - const process_group = (group: PermissionGroup) => { - for(const permission of group._element_permissions.permissions) - this._permission_entry_map[permission.permission().id] = permission; - for(const g of group._sub_elements) - process_group(g); - }; - - for(const group of this.grouped_permissions) { - const element = new PermissionGroup(group); - element.set_color_scheme(color_scheme); - element.set_manager(this); - process_group(element); - this._elements.push(element); - } - for(const element of this._elements) { - element.initialize(); - } - } - } - - intercept_manager() { - return this._intersect_manager; - } - - set_selected_entry(entry?: PermissionEntry) { - if(this._selected_entry === entry) - return; - - if(this._selected_entry) { - this._selected_entry.selected = false; - this._selected_entry.request_full_draw(); - } - this._selected_entry = entry; - if(this._selected_entry) { - this._selected_entry.selected = true; - this._selected_entry.request_full_draw(); - } - this.request_draw(false); - } - - permission_entries() : PermissionEntry[] { - return Object.keys(this._permission_entry_map).map(e => this._permission_entry_map[e]); - } - - collapse_all() { - for(const group of this._elements) - group.collapse_group(); - this.request_draw(true); - } - - expend_all() { - for(const group of this._elements) - group.expend_group(); - this.request_draw(true); - } - } -} \ No newline at end of file diff --git a/shared/js/ui/server.ts b/shared/js/ui/server.ts index 9ed774da..73af51c3 100644 --- a/shared/js/ui/server.ts +++ b/shared/js/ui/server.ts @@ -22,6 +22,8 @@ class ServerProperties { virtualserver_password: string = ""; virtualserver_flag_password: boolean = false; + virtualserver_ask_for_privilegekey: boolean = false; + virtualserver_welcomemessage: string = ""; virtualserver_hostmessage: string = ""; @@ -52,6 +54,8 @@ class ServerProperties { virtualserver_antiflood_points_needed_command_block: number = 0; virtualserver_antiflood_points_needed_ip_block: number = 0; + virtualserver_country_code: string = "XX"; + virtualserver_complain_autoban_count: number = 0; virtualserver_complain_autoban_time: number = 0; virtualserver_complain_remove_time: number = 0; @@ -59,11 +63,43 @@ class ServerProperties { virtualserver_needed_identity_security_level: number = 8; virtualserver_weblist_enabled: boolean = false; virtualserver_min_clients_in_channel_before_forced_silence: number = 0; + virtualserver_channel_temp_delete_delay_default: number = 60; + virtualserver_priority_speaker_dimm_modificator: number = -18; virtualserver_max_upload_total_bandwidth: number = 0; virtualserver_upload_quota: number = 0; virtualserver_max_download_total_bandwidth: number = 0; virtualserver_download_quota: number = 0; + + virtualserver_month_bytes_downloaded: number = 0; + virtualserver_month_bytes_uploaded: number = 0; + virtualserver_total_bytes_downloaded: number = 0; + virtualserver_total_bytes_uploaded: number = 0; +} + +interface ServerConnectionInfo { + connection_filetransfer_bandwidth_sent: number; + connection_filetransfer_bandwidth_received: number; + + connection_filetransfer_bytes_sent_total: number; + connection_filetransfer_bytes_received_total: number; + + connection_filetransfer_bytes_sent_month: number; + connection_filetransfer_bytes_received_month: number; + + connection_packets_sent_total: number; + connection_bytes_sent_total: number; + connection_packets_received_total: number; + connection_bytes_received_total: number; + + connection_bandwidth_sent_last_second_total: number; + connection_bandwidth_sent_last_minute_total: number; + connection_bandwidth_received_last_second_total: number; + connection_bandwidth_received_last_minute_total: number; + + connection_connected_time: number; + connection_packetloss_total: number; + connection_ping: number; } interface ServerAddress { @@ -80,18 +116,25 @@ class ServerEntry { private info_request_promise_resolve: any = undefined; private info_request_promise_reject: any = undefined; + private _info_connection_promise: Promise; + private _info_connection_promise_timestamp: number; + private _info_connection_promise_resolve: any; + private _info_connection_promise_reject: any; + lastInfoRequest: number = 0; nextInfoRequest: number = 0; private _htmlTag: JQuery; + private _destroyed = false; constructor(tree, name, address: ServerAddress) { this.properties = new ServerProperties(); this.channelTree = tree; - this.remote_address = address; + this.remote_address = Object.assign({}, address); /* close the address because it might get changed due to the DNS resolve */ this.properties.virtualserver_name = name; } get htmlTag() { + if(this._destroyed) throw "destoryed"; if(this._htmlTag) return this._htmlTag; let tag = $.spawn("div").addClass("tree-entry server"); @@ -115,6 +158,20 @@ class ServerEntry { return this._htmlTag = tag; } + destroy() { + this._destroyed = true; + if(this._htmlTag) { + this._htmlTag.remove(); + this._htmlTag = undefined; + } + this.info_request_promise = undefined; + this.info_request_promise_resolve = undefined; + this.info_request_promise_reject = undefined; + + this.channelTree = undefined; + this.remote_address = undefined; + } + initializeListener(){ this._htmlTag.click(() => { this.channelTree.onSelect(this); @@ -141,13 +198,24 @@ class ServerEntry { name: tr("Show server info"), callback: () => { trigger_close = false; - this.channelTree.client.select_info.open_popover() + + //TODO + alert("inplement me"); }, icon_class: "client-about", - visible: this.channelTree.client.select_info.is_popover() + visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) + }, { + type: contextmenu.MenuEntryType.ENTRY, + icon_class: "client-channel_switch", + name: tr("Join server text channel"), + callback: () => { + this.channelTree.client.side_bar.channel_conversations().set_current_channel(0); + this.channelTree.client.side_bar.show_channel_conversations(); + }, + visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) }, { type: contextmenu.MenuEntryType.HR, - visible: this.channelTree.client.select_info.is_popover(), + visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT), name: '' }, { type: contextmenu.MenuEntryType.ENTRY, @@ -157,10 +225,14 @@ class ServerEntry { Modals.createServerModal(this, properties => { log.info(LogCategory.SERVER, tr("Changing server properties %o"), properties); console.log(tr("Changed properties: %o"), properties); - if (properties) - this.channelTree.client.serverConnection.send_command("serveredit", properties).then(() => { - this.channelTree.client.sound.play(Sound.SERVER_EDITED_SELF); - }); + if (properties) { + if(Object.keys(properties)) { + return this.channelTree.client.serverConnection.send_command("serveredit", properties).then(() => { + this.channelTree.client.sound.play(Sound.SERVER_EDITED_SELF); + }); + } + } + return Promise.resolve(); }); } }, { @@ -197,7 +269,7 @@ class ServerEntry { log.table("Server update properties", entries); } - let update_bannner = false; + let update_bannner = false, update_button = false; for(let variable of variables) { JSON.map_field_to(this.properties, variable.value, variable.key); @@ -211,14 +283,31 @@ class ServerEntry { */ this.properties.virtualserver_icon_id = variable.value as any >>> 0; + const bmarks = bookmarks.bookmarks_flat() + .filter(e => e.server_properties.server_address === this.remote_address.host && e.server_properties.server_port == this.remote_address.port) + .filter(e => e.last_icon_id !== this.properties.virtualserver_icon_id); + if(bmarks.length > 0) { + bmarks.forEach(e => { + e.last_icon_id = this.properties.virtualserver_icon_id; + }); + bookmarks.save_bookmark(); + top_menu.rebuild_bookmarks(); + control_bar.update_bookmarks(); + } + if(this.channelTree.client.fileManager && this.channelTree.client.fileManager.icons) this.htmlTag.find(".icon_property").replaceWith(this.channelTree.client.fileManager.icons.generateTag(this.properties.virtualserver_icon_id).addClass("icon_property")); } else if(variable.key.indexOf('hostbanner') != -1) { update_bannner = true; + } else if(variable.key.indexOf('hostbutton') != -1) { + update_button = true; } } if(update_bannner) - this.channelTree.client.select_info.update_banner(); + this.channelTree.client.hostbanner.update(); + if(update_button) + if(control_bar.current_connection_handler() === this.channelTree.client) + control_bar.apply_server_hostbutton(); group.end(); if(is_self_notify && this.info_request_promise_resolve) { @@ -227,8 +316,23 @@ class ServerEntry { this.info_request_promise_reject = undefined; this.info_request_promise_resolve = undefined; } + + connection_log.update_address_info({ + hostname: this.remote_address.host, + port: this.remote_address.port + }, { + clients_online: this.properties.virtualserver_clientsonline, + clients_total: this.properties.virtualserver_maxclients, + country: this.properties.virtualserver_country_code, + flag_password: this.properties.virtualserver_flag_password, + name: this.properties.virtualserver_name, + icon_id: this.properties.virtualserver_icon_id, + + password_hash: undefined /* we've here no clue */ + }); } + /* this result !must! be cached for at least a second */ updateProperties() : Promise { if(this.info_request_promise && Date.now() - this.lastInfoRequest < 1000) return this.info_request_promise; this.lastInfoRequest = Date.now(); @@ -246,6 +350,34 @@ class ServerEntry { }); } + /* max 1s ago, so we could update every second */ + request_connection_info() : Promise { + if(Date.now() - 900 < this._info_connection_promise_timestamp && this._info_connection_promise) + return this._info_connection_promise; + + if(this._info_connection_promise_reject) + this._info_connection_promise_resolve("timeout"); + + let _local_reject; /* to ensure we're using the right resolve! */ + this._info_connection_promise = new Promise((resolve, reject) => { + this._info_connection_promise_resolve = resolve; + this._info_connection_promise_reject = reject; + _local_reject = reject; + }); + + this._info_connection_promise_timestamp = Date.now(); + this.channelTree.client.serverConnection.send_command("serverrequestconnectioninfo").catch(error => _local_reject(error)); + return this._info_connection_promise; + } + + set_connection_info(info: ServerConnectionInfo) { + if(!this._info_connection_promise_resolve) + return; + this._info_connection_promise_resolve(info); + this._info_connection_promise_resolve = undefined; + this._info_connection_promise_reject = undefined; + } + shouldUpdateProperties() : boolean { return this.nextInfoRequest < Date.now(); } diff --git a/shared/js/ui/view.ts b/shared/js/ui/view.ts index bdd8d528..ca927683 100644 --- a/shared/js/ui/view.ts +++ b/shared/js/ui/view.ts @@ -9,8 +9,8 @@ class ChannelTree { client: ConnectionHandler; server: ServerEntry; - channels: ChannelEntry[]; - clients: ClientEntry[]; + channels: ChannelEntry[] = []; + clients: ClientEntry[] = []; currently_selected: ClientEntry | ServerEntry | ChannelEntry | (ClientEntry | ServerEntry)[] = undefined; currently_selected_context_callback: (event) => any = undefined; @@ -24,11 +24,11 @@ class ChannelTree { private channel_last?: ChannelEntry; private channel_first?: ChannelEntry; - private selected_event?: Event; + private _focused = false; + private _listener_document_click; + private _listener_document_key; constructor(client) { - document.addEventListener("touchstart", function(){}, true); - this.client = client; this._tag_container = $.spawn("div").addClass("channel-tree-container"); @@ -56,22 +56,47 @@ class ChannelTree { } this._tag_container.on('resize', this.handle_resized.bind(this)); - - /* TODO release these events again when ChannelTree get deinitialized */ - $(document).on('click', event => { - if(this.selected_event != event.originalEvent) - this.selected_event = undefined; - }); - $(document).on('keydown', this.handle_key_press.bind(this)); - this._tag_container.on('click', event => {{ - this.selected_event = event.originalEvent; - }}); + this._listener_document_key = event => this.handle_key_press(event); + this._listener_document_click = event => { + this._focused = false; + let element = event.target as HTMLElement; + while(element) { + if(element === this._tag_container[0]) { + this._focused = true; + break; + } + element = element.parentNode as HTMLElement; + } + }; + document.addEventListener('click', this._listener_document_click); + document.addEventListener('keydown', this._listener_document_key); } tag_tree() : JQuery { return this._tag_container; } + destroy() { + this._listener_document_click && document.removeEventListener('click', this._listener_document_click); + this._listener_document_click = undefined; + + this._listener_document_key && document.removeEventListener('keydown', this._listener_document_key); + this._listener_document_key = undefined; + + if(this.server) { + this.server.destroy(); + this.server = undefined; + } + this.reset(); /* cleanup channel and clients */ + + this.channel_first = undefined; + this.channel_last = undefined; + + this._tag_container.remove(); + this.currently_selected = undefined; + this.currently_selected_context_callback = undefined; + } + hide_channel_tree() { this._tag_entries.detach(); this._tree_detached = true; @@ -103,6 +128,10 @@ class ChannelTree { } initialiseHead(serverName: string, address: ServerAddress) { + if(this.server) { + this.server.destroy(); + this.server = undefined; + } this.server = new ServerEntry(this, serverName, address); this.server.htmlTag.appendTo(this._tag_entries); this.server.initializeListener(); @@ -112,6 +141,7 @@ class ChannelTree { let tag = element instanceof ChannelEntry ? element.rootTag() : element.tag; tag.fadeOut("slow", () => { tag.detach(); + element.destroy(); }); } @@ -311,7 +341,7 @@ class ChannelTree { voice_connection.unregister_client(client.get_audio_handle()); } } - client.set_audio_handle(undefined); /* just to be sure */ + client.set_audio_handle(undefined); } registerClient(client: ClientEntry) { @@ -323,6 +353,12 @@ class ChannelTree { client.set_audio_handle(voice_connection.register_client(client.clientId())); } + unregisterClient(client: ClientEntry) { + if(!this.clients.remove(client)) + return; + client.tree_unregistered(); + } + insertClient(client: ClientEntry, channel: ChannelEntry) : ClientEntry { let newClient = this.findClient(client.clientId()); if(newClient) @@ -400,7 +436,6 @@ class ChannelTree { } onSelect(entry?: ChannelEntry | ClientEntry | ServerEntry, enforce_single?: boolean, flag_shift?: boolean) { - console.log("Select: " + entry); if(this.currently_selected && (ppt.key_pressed(ppt.SpecialKey.SHIFT) || flag_shift) && entry instanceof ClientEntry) { //Currently we're only supporting client multiselects :D if(!entry) return; //Nowhere @@ -448,6 +483,17 @@ class ChannelTree { else if(entry instanceof ServerEntry) (entry as ServerEntry).htmlTag.addClass("selected"); + if(!$.isArray(this.currently_selected)) { + if(this.currently_selected instanceof ClientEntry && settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)) { + this.client.side_bar.show_client_info(this.currently_selected); + } else if(this.currently_selected instanceof ChannelEntry && settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) { + this.client.side_bar.channel_conversations().set_current_channel(this.currently_selected.channelId); + this.client.side_bar.show_channel_conversations(); + } else if(this.currently_selected instanceof ServerEntry && settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) { + this.client.side_bar.channel_conversations().set_current_channel(0); + this.client.side_bar.show_channel_conversations(); + } + } this.client.select_info.setCurrentSelected($.isArray(this.currently_selected) ? undefined : entry); } @@ -460,7 +506,6 @@ class ChannelTree { const music_only = clients.map(e => e instanceof MusicClientEntry ? 0 : 1).reduce((a, b) => a + b, 0) == 0; const music_entry = clients.map(e => e instanceof MusicClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0; const local_client = clients.map(e => e instanceof LocalClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0; - console.log(tr("Music only: %o | Container music: %o | Container local: %o"), music_entry, music_entry, local_client); let entries: contextmenu.MenuEntry[] = []; if (!music_entry && !local_client) { //Music bots or local client cant be poked entries.push({ @@ -604,10 +649,21 @@ class ChannelTree { } reset(){ - this.server = null; + const voice_connection = this.client.serverConnection ? this.client.serverConnection.voice_connection() : undefined; + for(const client of this.clients) { + if(client.get_audio_handle() && voice_connection) { + voice_connection.unregister_client(client.get_audio_handle()); + client.set_audio_handle(undefined); + } + client.destroy(); + } this.clients = []; + + for(const channel of this.channels) + channel.destroy(); this.channels = []; - this._tag_entries.children().detach(); //Do not remove the listener! + + this._tag_entries.children().detach(); //Dont remove listeners this.channel_first = undefined; this.channel_last = undefined; @@ -692,7 +748,8 @@ class ChannelTree { } handle_key_press(event: KeyboardEvent) { - if(!this.selected_event || !this.currently_selected || $.isArray(this.currently_selected)) return; + console.log("Keydown: %o | %o | %o", this._focused, this.currently_selected, Array.isArray(this.currently_selected)); + if(!this._focused || !this.currently_selected || Array.isArray(this.currently_selected)) return; if(event.keyCode == KeyCode.KEY_UP) { event.preventDefault(); diff --git a/shared/js/voice/RecorderBase.ts b/shared/js/voice/RecorderBase.ts index b5de2672..ba32fb38 100644 --- a/shared/js/voice/RecorderBase.ts +++ b/shared/js/voice/RecorderBase.ts @@ -2,6 +2,7 @@ namespace audio { export namespace recorder { export interface InputDevice { unique_id: string; + driver: string; name: string; default_input: boolean; @@ -80,13 +81,21 @@ namespace audio { DRY } + export enum InputStartResult { + EOK = "eok", + EUNKNOWN = "eunknown", + EBUSY = "ebusy", + ENOTALLOWED = "enotallowed", + ENOTSUPPORTED = "enotsupported" + } + export interface AbstractInput { callback_begin: () => any; callback_end: () => any; current_state() : InputState; - start() : Promise; + start() : Promise; stop() : Promise; current_device() : InputDevice | undefined; @@ -102,6 +111,16 @@ namespace audio { disable_filter(type: filter.Type); enable_filter(type: filter.Type); + get_volume() : number; + set_volume(volume: number); + } + + export interface LevelMeter { + device() : InputDevice; + + set_observer(callback: (value: number) => any); + + destory(); } } } \ No newline at end of file diff --git a/shared/js/voice/RecorderProfile.ts b/shared/js/voice/RecorderProfile.ts index 0b67a9a0..44ffc806 100644 --- a/shared/js/voice/RecorderProfile.ts +++ b/shared/js/voice/RecorderProfile.ts @@ -7,10 +7,13 @@ interface RecorderProfileConfig { /* devices unique id */ device_id: string | undefined; + volume: number; + vad_type: VadType; vad_threshold: { threshold: number; } + vad_push_to_talk: { delay: number; key_code: string; @@ -59,7 +62,7 @@ class RecorderProfile { const filter = this.input.get_filter(audio.recorder.filter.Type.STATE) as audio.recorder.filter.StateFilter; if(filter) filter.set_state(true); - }, this.config.vad_push_to_talk.delay); + }, Math.min(this.config.vad_push_to_talk.delay, 0)); }, callback_press: () => { if(this._ppt_timeout) @@ -115,27 +118,30 @@ class RecorderProfile { } private async load() { - this.config = settings.static_global(Settings.FN_PROFILE_RECORD(this.name), {}) as RecorderProfileConfig; - if(typeof(this.config.version) === "undefined") { - /* default config */ - this.config = { - version: 1, - device_id: undefined, + const config = settings.static_global(Settings.FN_PROFILE_RECORD(this.name), {}) as RecorderProfileConfig; - vad_threshold: { - threshold: 50 - }, - vad_type: "threshold", - vad_push_to_talk: { - delay: 300, - key_alt: false, - key_ctrl: false, - key_shift: false, - key_windows: false, - key_code: 't' - } + /* default values */ + this.config = { + version: 1, + device_id: undefined, + volume: 100, + + vad_threshold: { + threshold: 50 + }, + vad_type: "threshold", + vad_push_to_talk: { + delay: 300, + key_alt: false, + key_ctrl: false, + key_shift: false, + key_windows: false, + key_code: 't' } - } + }; + + Object.assign(this.config, config || {}); + this.input.set_volume(this.config.volume / 100); { const all_devices = audio.recorder.devices(); @@ -216,7 +222,7 @@ class RecorderProfile { this.save(); } - get_vad_threshold() { return this.config.vad_threshold.threshold; } + get_vad_threshold() { return parseInt(this.config.vad_threshold.threshold as any); } /* for some reason it might be a string... */ set_vad_threshold(value: number) { if(this.config.vad_threshold.threshold === value) return; @@ -252,4 +258,14 @@ class RecorderProfile { this.save(); return this.input.set_device(device); } + + get_volume() : number { return this.input ? (this.input.get_volume() * 100) : this.config.volume; } + set_volume(volume: number) { + if(this.config.volume === volume) + return; + + this.config.volume = volume; + this.input && this.input.set_volume(volume / 100); + this.save(); + } } \ No newline at end of file diff --git a/todo.txt b/todo.txt new file mode 100644 index 00000000..772c4225 --- /dev/null +++ b/todo.txt @@ -0,0 +1,74 @@ +- Modals + - Settings (X) + - Banlist + - Add/Edit + - Query + - List + - Create + - Manage favorites + - Create favorite + - Entity info (Popup) + - Server + - Client + - Channel + - Icon Select + - Icon uploiad + - Avatar list + - Server group assignments checkbox + - Identity improve + - Identity import + + + +- Application Options + - Crash + - Focus crash window on crash + - Add a notification (Like the browser notifications) + + + +- Client info popup + - Basic Info + - Name/Unique ID (Database ID vil auch? Oder als hover irgendwo) + - TeaForo connected? Ist premium ja nein? + - Country + - Avatar + - IP (Wenn permission) + - Status + Away (+ Away message?) + Microphone disabled + Speakers/Headphones disabled + Speakers/Headphones Muted + Microphone Muted + + - Description + - Server groups + - Channel group + - Connect count (Wie oft der client schon connectet ist) + - Online seit | Idle time + - Ping + + Nur TeaClient; Nicht für WebClient clients + - Bandwidth + - Packets send/received | Packet Loss + - Up/Download Quota + + + +- Server "short" info? +- Server info popup + - Name + - Version + - Clients (TS3 | TeaSpeak | Query | Web) /Max Client/Reserved + - Uptime + - License? + - Address + - Average ping + Packet loss + - Bandwidth last minutes (In & Out) + - Bandwidth last second (In & Out) + - Packets + Bytes transfered (In & Out) + - File Transfer Bandwidth (Up + Download) + - File Transfer bytes transferred, month & global (Up + Download) + +TODO: +Fix these icons: https://img.did.science/Screenshot_20-11-06.png \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1835eec2..07b098f0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "target": "es6", "module": "commonjs", "sourceMap": true, - "lib": ["es6", "dom", "dom.iterable"] + "lib": ["es6", "dom", "dom.iterable"], + "removeComments": true /* we dont really need them within the target files */ }, "exclude": [ "node_modules", diff --git a/vendor/DOMPurify/purify.min.js b/vendor/DOMPurify/purify.min.js new file mode 100644 index 00000000..618e7b60 --- /dev/null +++ b/vendor/DOMPurify/purify.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.DOMPurify=t()}(this,function(){"use strict";function e(e,t){y&&y(e,null);for(var n=t.length;n--;){var r=t[n];if("string"==typeof r){var o=r.toLowerCase();o!==r&&(Object.isFrozen(t)||(t[n]=o),r=o)}e[r]=!0}return e}function t(e){var t={},n=void 0;for(n in e)g(h,e,[n])&&(t[n]=e[n]);return t}function n(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:_(),u=function(e){return r(e)};if(u.version="1.0.11",u.removed=[],!o||!o.document||9!==o.document.nodeType)return u.isSupported=!1,u;var h=o.document,y=!1,g=!1,v=o.document,D=o.DocumentFragment,R=o.HTMLTemplateElement,C=o.Node,H=o.NodeFilter,F=o.NamedNodeMap,z=void 0===F?o.NamedNodeMap||o.MozNamedAttrMap:F,I=o.Text,j=o.Comment,P=o.DOMParser,U=o.TrustedTypes;if("function"==typeof R){var W=v.createElement("template");W.content&&W.content.ownerDocument&&(v=W.content.ownerDocument)}var B=N(U,h),G=B?B.createHTML(""):"",q=v,V=q.implementation,Y=q.createNodeIterator,K=q.getElementsByTagName,X=q.createDocumentFragment,$=h.importNode,J={};u.isSupported=V&&void 0!==V.createHTMLDocument&&9!==v.documentMode;var Q=b,Z=T,ee=A,te=x,ne=S,re=M,oe=L,ie=null,ae=e({},[].concat(n(i),n(a),n(l),n(c),n(s))),le=null,ce=e({},[].concat(n(d),n(f),n(p),n(m))),se=null,ue=null,de=!0,fe=!0,pe=!1,me=!1,he=!1,ye=!1,ge=!1,ve=!1,be=!1,Te=!1,Ae=!1,xe=!0,Le=!0,Se=!1,Me={},ke=e({},["audio","head","math","script","style","template","svg","video"]),we=e({},["audio","video","img","source","image"]),Ee=null,Oe=e({},["alt","class","for","id","label","name","pattern","placeholder","summary","title","value","style","xmlns"]),_e=null,Ne=v.createElement("form"),De=function(r){_e&&_e===r||(r&&"object"===(void 0===r?"undefined":k(r))||(r={}),ie="ALLOWED_TAGS"in r?e({},r.ALLOWED_TAGS):ae,le="ALLOWED_ATTR"in r?e({},r.ALLOWED_ATTR):ce,Ee="ADD_URI_SAFE_ATTR"in r?e({},r.ADD_URI_SAFE_ATTR):Oe,se="FORBID_TAGS"in r?e({},r.FORBID_TAGS):{},ue="FORBID_ATTR"in r?e({},r.FORBID_ATTR):{},Me="USE_PROFILES"in r&&r.USE_PROFILES,de=!1!==r.ALLOW_ARIA_ATTR,fe=!1!==r.ALLOW_DATA_ATTR,pe=r.ALLOW_UNKNOWN_PROTOCOLS||!1,me=r.SAFE_FOR_JQUERY||!1,he=r.SAFE_FOR_TEMPLATES||!1,ye=r.WHOLE_DOCUMENT||!1,be=r.RETURN_DOM||!1,Te=r.RETURN_DOM_FRAGMENT||!1,Ae=r.RETURN_DOM_IMPORT||!1,ve=r.FORCE_BODY||!1,xe=!1!==r.SANITIZE_DOM,Le=!1!==r.KEEP_CONTENT,Se=r.IN_PLACE||!1,oe=r.ALLOWED_URI_REGEXP||oe,he&&(fe=!1),Te&&(be=!0),Me&&(ie=e({},[].concat(n(s))),le=[],!0===Me.html&&(e(ie,i),e(le,d)),!0===Me.svg&&(e(ie,a),e(le,f),e(le,m)),!0===Me.svgFilters&&(e(ie,l),e(le,f),e(le,m)),!0===Me.mathMl&&(e(ie,c),e(le,p),e(le,m))),r.ADD_TAGS&&(ie===ae&&(ie=t(ie)),e(ie,r.ADD_TAGS)),r.ADD_ATTR&&(le===ce&&(le=t(le)),e(le,r.ADD_ATTR)),r.ADD_URI_SAFE_ATTR&&e(Ee,r.ADD_URI_SAFE_ATTR),Le&&(ie["#text"]=!0),ye&&e(ie,["html","head","body"]),ie.table&&e(ie,["tbody"]),O&&O(r),_e=r)},Re=function(e){u.removed.push({element:e});try{e.parentNode.removeChild(e)}catch(t){e.outerHTML=G}},Ce=function(e,t){try{u.removed.push({attribute:t.getAttributeNode(e),from:t})}catch(e){u.removed.push({attribute:null,from:t})}t.removeAttribute(e)},He=function(t){var n=void 0,r=void 0;if(ve)t=""+t;else{var o=t.match(/^[\s]+/);(r=o&&o[0])&&(t=t.slice(r.length))}if(y)try{n=(new P).parseFromString(t,"text/html")}catch(e){}if(g&&e(se,["title"]),!n||!n.documentElement){var i=(n=V.createHTMLDocument("")).body;i.parentNode.removeChild(i.parentNode.firstElementChild),i.outerHTML=B?B.createHTML(t):t}return r&&n.body.insertBefore(v.createTextNode(r),n.body.childNodes[0]||null),K.call(n,ye?"html":"body")[0]};u.isSupported&&(function(){try{He('

').querySelector("svg img")&&(y=!0)}catch(e){}}(),function(){try{He("</title><img>").querySelector("title").innerHTML.match(/<\/title/)&&(g=!0)}catch(e){}}());var Fe=function(e){return Y.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT,function(){return H.FILTER_ACCEPT},!1)},ze=function(e){return!(e instanceof I||e instanceof j)&&!("string"==typeof e.nodeName&&"string"==typeof e.textContent&&"function"==typeof e.removeChild&&e.attributes instanceof z&&"function"==typeof e.removeAttribute&&"function"==typeof e.setAttribute)},Ie=function(e){return"object"===(void 0===C?"undefined":k(C))?e instanceof C:e&&"object"===(void 0===e?"undefined":k(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},je=function(e,t,n){J[e]&&J[e].forEach(function(e){e.call(u,t,n,_e)})},Pe=function(e){var t=void 0;if(je("beforeSanitizeElements",e,null),ze(e))return Re(e),!0;var n=e.nodeName.toLowerCase();if(je("uponSanitizeElement",e,{tagName:n,allowedTags:ie}),!ie[n]||se[n]){if(Le&&!ke[n]&&"function"==typeof e.insertAdjacentHTML)try{var r=e.innerHTML;e.insertAdjacentHTML("AfterEnd",B?B.createHTML(r):r)}catch(e){}return Re(e),!0}return"noscript"===n&&e.innerHTML.match(/<\/noscript/i)?(Re(e),!0):"noembed"===n&&e.innerHTML.match(/<\/noembed/i)?(Re(e),!0):(!me||e.firstElementChild||e.content&&e.content.firstElementChild||!/</g.test(e.textContent)||(u.removed.push({element:e.cloneNode()}),e.innerHTML?e.innerHTML=e.innerHTML.replace(/</g,"<"):e.innerHTML=e.textContent.replace(/</g,"<")),he&&3===e.nodeType&&(t=(t=(t=e.textContent).replace(Q," ")).replace(Z," "),e.textContent!==t&&(u.removed.push({element:e.cloneNode()}),e.textContent=t)),je("afterSanitizeElements",e,null),!1)},Ue=function(e,t,n){if(xe&&("id"===t||"name"===t)&&(n in v||n in Ne))return!1;if(fe&&ee.test(t));else if(de&&te.test(t));else{if(!le[t]||ue[t])return!1;if(Ee[t]);else if(oe.test(n.replace(re,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==n.indexOf("data:")||!we[e]){if(pe&&!ne.test(n.replace(re,"")));else if(n)return!1}else;}return!0},We=function(e){var t=void 0,n=void 0,r=void 0,o=void 0,i=void 0;je("beforeSanitizeAttributes",e,null);var a=e.attributes;if(a){var l={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:le};for(i=a.length;i--;){var c=t=a[i],s=c.name,d=c.namespaceURI;if(n=t.value.trim(),r=s.toLowerCase(),l.attrName=r,l.attrValue=n,l.keepAttr=!0,je("uponSanitizeAttribute",e,l),n=l.attrValue,"name"===r&&"IMG"===e.nodeName&&a.id)o=a.id,a=w(E,a,[]),Ce("id",e),Ce(s,e),a.indexOf(o)>i&&e.setAttribute("id",o.value);else{if("INPUT"===e.nodeName&&"type"===r&&"file"===n&&l.keepAttr&&(le[r]||!ue[r]))continue;"id"===s&&e.setAttribute(s,""),Ce(s,e)}if(l.keepAttr){he&&(n=(n=n.replace(Q," ")).replace(Z," "));var f=e.nodeName.toLowerCase();if(Ue(f,r,n))try{d?e.setAttributeNS(d,s,n):e.setAttribute(s,n),u.removed.pop()}catch(e){}}}je("afterSanitizeAttributes",e,null)}},Be=function e(t){var n=void 0,r=Fe(t);for(je("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)je("uponSanitizeShadowNode",n,null),Pe(n)||(n.content instanceof D&&e(n.content),We(n));je("afterSanitizeShadowDOM",t,null)};return u.sanitize=function(e,t){var n=void 0,r=void 0,i=void 0,a=void 0,l=void 0;if(e||(e="\x3c!--\x3e"),"string"!=typeof e&&!Ie(e)){if("function"!=typeof e.toString)throw new TypeError("toString is not a function");if("string"!=typeof(e=e.toString()))throw new TypeError("dirty is not a string, aborting")}if(!u.isSupported){if("object"===k(o.toStaticHTML)||"function"==typeof o.toStaticHTML){if("string"==typeof e)return o.toStaticHTML(e);if(Ie(e))return o.toStaticHTML(e.outerHTML)}return e}if(ge||De(t),u.removed=[],Se);else if(e instanceof C)1===(r=(n=He("\x3c!--\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===r.nodeName?n=r:"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!be&&!he&&!ye&&-1===e.indexOf("<"))return B?B.createHTML(e):e;if(!(n=He(e)))return be?null:G}n&&ve&&Re(n.firstChild);for(var c=Fe(Se?e:n);i=c.nextNode();)3===i.nodeType&&i===a||Pe(i)||(i.content instanceof D&&Be(i.content),We(i),a=i);if(a=null,Se)return e;if(be){if(Te)for(l=X.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return Ae&&(l=$.call(h,l,!0)),l}var s=ye?n.outerHTML:n.innerHTML;return he&&(s=(s=s.replace(Q," ")).replace(Z," ")),B?B.createHTML(s):s},u.setConfig=function(e){De(e),ge=!0},u.clearConfig=function(){_e=null,ge=!1},u.isValidAttribute=function(e,t,n){_e||De({});var r=e.toLowerCase(),o=t.toLowerCase();return Ue(r,o,n)},u.addHook=function(e,t){"function"==typeof t&&(J[e]=J[e]||[],J[e].push(t))},u.removeHook=function(e){J[e]&&J[e].pop()},u.removeHooks=function(e){J[e]&&(J[e]=[])},u.removeAllHooks=function(){J={}},u}var o=Object.freeze||function(e){return e},i=o(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),a=o(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","audio","canvas","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","video","view","vkern"]),l=o(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),c=o(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover"]),s=o(["#text"]),u=Object.freeze||function(e){return e},d=u(["accept","action","align","alt","autocomplete","background","bgcolor","border","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","coords","crossorigin","datetime","default","dir","disabled","download","enctype","face","for","headers","height","hidden","high","href","hreflang","id","integrity","ismap","label","lang","list","loop","low","max","maxlength","media","method","min","multiple","name","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","type","usemap","valign","value","width","xmlns"]),f=u(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","tabindex","targetx","targety","transform","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),p=u(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),m=u(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),h=Object.hasOwnProperty,y=Object.setPrototypeOf,g=("undefined"!=typeof Reflect&&Reflect).apply;g||(g=function(e,t,n){return e.apply(t,n)});var v=Object.seal||function(e){return e},b=v(/\{\{[\s\S]*|[\s\S]*\}\}/gm),T=v(/<%[\s\S]*|[\s\S]*%>/gm),A=v(/^data-[\-\w.\u00B7-\uFFFF]/),x=v(/^aria-[\-\w]+$/),L=v(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),S=v(/^(?:\w+script|data):/i),M=v(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g),k="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},w=("undefined"!=typeof Reflect&&Reflect).apply,E=Array.prototype.slice,O=Object.freeze,_=function(){return"undefined"==typeof window?null:window};w||(w=function(e,t,n){return e.apply(t,n)});var N=function(e,t){if("object"!==(void 0===e?"undefined":k(e))||"function"!=typeof e.createPolicy)return null;var n=null;t.currentScript&&t.currentScript.hasAttribute("data-tt-policy-suffix")&&(n=t.currentScript.getAttribute("data-tt-policy-suffix"));var r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};return r()}); +//# sourceMappingURL=purify.min.js.map \ No newline at end of file diff --git a/vendor/bootstrap-material/bootstrap-material-design.js b/vendor/bootstrap-material/bootstrap-material-design.js deleted file mode 100644 index 3e396bdc..00000000 --- a/vendor/bootstrap-material/bootstrap-material-design.js +++ /dev/null @@ -1,6537 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery'), require('popper.js')) : - typeof define === 'function' && define.amd ? define(['jquery', 'popper.js'], factory) : - (factory(global.jQuery,global.Popper)); -}(this, (function ($,Popper$1) { 'use strict'; - - $ = $ && $.hasOwnProperty('default') ? $['default'] : $; - Popper$1 = Popper$1 && Popper$1.hasOwnProperty('default') ? Popper$1['default'] : Popper$1; - - function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - return Constructor; - } - - function _extends() { - _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; - }; - - return _extends.apply(this, arguments); - } - - function _inheritsLoose(subClass, superClass) { - subClass.prototype = Object.create(superClass.prototype); - subClass.prototype.constructor = subClass; - subClass.__proto__ = superClass; - } - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): util.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Util = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Private TransitionEnd Helpers - * ------------------------------------------------------------------------ - */ - var transition = false; - var MAX_UID = 1000000; // Shoutout AngusCroll (https://goo.gl/pxwQGp) - - function toType(obj) { - return {}.toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); - } - - function getSpecialTransitionEndEvent() { - return { - bindType: transition.end, - delegateType: transition.end, - handle: function handle(event) { - if ($$$1(event.target).is(this)) { - return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params - } - - return undefined; // eslint-disable-line no-undefined - } - }; - } - - function transitionEndTest() { - if (typeof window !== 'undefined' && window.QUnit) { - return false; - } - - return { - end: 'transitionend' - }; - } - - function transitionEndEmulator(duration) { - var _this = this; - - var called = false; - $$$1(this).one(Util.TRANSITION_END, function () { - called = true; - }); - setTimeout(function () { - if (!called) { - Util.triggerTransitionEnd(_this); - } - }, duration); - return this; - } - - function setTransitionEndSupport() { - transition = transitionEndTest(); - $$$1.fn.emulateTransitionEnd = transitionEndEmulator; - - if (Util.supportsTransitionEnd()) { - $$$1.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent(); - } - } - - function escapeId(selector) { - // We escape IDs in case of special selectors (selector = '#myId:something') - // $.escapeSelector does not exist in jQuery < 3 - selector = typeof $$$1.escapeSelector === 'function' ? $$$1.escapeSelector(selector).substr(1) : selector.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1'); - return selector; - } - /** - * -------------------------------------------------------------------------- - * Public Util Api - * -------------------------------------------------------------------------- - */ - - - var Util = { - TRANSITION_END: 'bsTransitionEnd', - getUID: function getUID(prefix) { - do { - // eslint-disable-next-line no-bitwise - prefix += ~~(Math.random() * MAX_UID); // "~~" acts like a faster Math.floor() here - } while (document.getElementById(prefix)); - - return prefix; - }, - getSelectorFromElement: function getSelectorFromElement(element) { - var selector = element.getAttribute('data-target'); - - if (!selector || selector === '#') { - selector = element.getAttribute('href') || ''; - } // If it's an ID - - - if (selector.charAt(0) === '#') { - selector = escapeId(selector); - } - - try { - var $selector = $$$1(document).find(selector); - return $selector.length > 0 ? selector : null; - } catch (err) { - return null; - } - }, - reflow: function reflow(element) { - return element.offsetHeight; - }, - triggerTransitionEnd: function triggerTransitionEnd(element) { - $$$1(element).trigger(transition.end); - }, - supportsTransitionEnd: function supportsTransitionEnd() { - return Boolean(transition); - }, - isElement: function isElement(obj) { - return (obj[0] || obj).nodeType; - }, - typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) { - for (var property in configTypes) { - if (Object.prototype.hasOwnProperty.call(configTypes, property)) { - var expectedTypes = configTypes[property]; - var value = config[property]; - var valueType = value && Util.isElement(value) ? 'element' : toType(value); - - if (!new RegExp(expectedTypes).test(valueType)) { - throw new Error(componentName.toUpperCase() + ": " + ("Option \"" + property + "\" provided type \"" + valueType + "\" ") + ("but expected type \"" + expectedTypes + "\".")); - } - } - } - } - }; - setTransitionEndSupport(); - return Util; - }($); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): alert.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Alert = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'alert'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.alert'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var TRANSITION_DURATION = 150; - var Selector = { - DISMISS: '[data-dismiss="alert"]' - }; - var Event = { - CLOSE: "close" + EVENT_KEY, - CLOSED: "closed" + EVENT_KEY, - CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY - }; - var ClassName = { - ALERT: 'alert', - FADE: 'fade', - SHOW: 'show' - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Alert = - /*#__PURE__*/ - function () { - function Alert(element) { - this._element = element; - } // Getters - - - var _proto = Alert.prototype; - - // Public - _proto.close = function close(element) { - element = element || this._element; - - var rootElement = this._getRootElement(element); - - var customEvent = this._triggerCloseEvent(rootElement); - - if (customEvent.isDefaultPrevented()) { - return; - } - - this._removeElement(rootElement); - }; - - _proto.dispose = function dispose() { - $$$1.removeData(this._element, DATA_KEY); - this._element = null; - }; // Private - - - _proto._getRootElement = function _getRootElement(element) { - var selector = Util.getSelectorFromElement(element); - var parent = false; - - if (selector) { - parent = $$$1(selector)[0]; - } - - if (!parent) { - parent = $$$1(element).closest("." + ClassName.ALERT)[0]; - } - - return parent; - }; - - _proto._triggerCloseEvent = function _triggerCloseEvent(element) { - var closeEvent = $$$1.Event(Event.CLOSE); - $$$1(element).trigger(closeEvent); - return closeEvent; - }; - - _proto._removeElement = function _removeElement(element) { - var _this = this; - - $$$1(element).removeClass(ClassName.SHOW); - - if (!Util.supportsTransitionEnd() || !$$$1(element).hasClass(ClassName.FADE)) { - this._destroyElement(element); - - return; - } - - $$$1(element).one(Util.TRANSITION_END, function (event) { - return _this._destroyElement(element, event); - }).emulateTransitionEnd(TRANSITION_DURATION); - }; - - _proto._destroyElement = function _destroyElement(element) { - $$$1(element).detach().trigger(Event.CLOSED).remove(); - }; // Static - - - Alert._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Alert(this); - $element.data(DATA_KEY, data); - } - - if (config === 'close') { - data[config](this); - } - }); - }; - - Alert._handleDismiss = function _handleDismiss(alertInstance) { - return function (event) { - if (event) { - event.preventDefault(); - } - - alertInstance.close(this); - }; - }; - - _createClass(Alert, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }]); - return Alert; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $$$1(document).on(Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert())); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $$$1.fn[NAME] = Alert._jQueryInterface; - $$$1.fn[NAME].Constructor = Alert; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Alert._jQueryInterface; - }; - - return Alert; - }($); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): button.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Button = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'button'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.button'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var ClassName = { - ACTIVE: 'active', - BUTTON: 'btn', - FOCUS: 'focus' - }; - var Selector = { - DATA_TOGGLE_CARROT: '[data-toggle^="button"]', - DATA_TOGGLE: '[data-toggle="buttons"]', - INPUT: 'input', - ACTIVE: '.active', - BUTTON: '.btn' - }; - var Event = { - CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY, - FOCUS_BLUR_DATA_API: "focus" + EVENT_KEY + DATA_API_KEY + " " + ("blur" + EVENT_KEY + DATA_API_KEY) - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Button = - /*#__PURE__*/ - function () { - function Button(element) { - this._element = element; - } // Getters - - - var _proto = Button.prototype; - - // Public - _proto.toggle = function toggle() { - var triggerChangeEvent = true; - var addAriaPressed = true; - var rootElement = $$$1(this._element).closest(Selector.DATA_TOGGLE)[0]; - - if (rootElement) { - var input = $$$1(this._element).find(Selector.INPUT)[0]; - - if (input) { - if (input.type === 'radio') { - if (input.checked && $$$1(this._element).hasClass(ClassName.ACTIVE)) { - triggerChangeEvent = false; - } else { - var activeElement = $$$1(rootElement).find(Selector.ACTIVE)[0]; - - if (activeElement) { - $$$1(activeElement).removeClass(ClassName.ACTIVE); - } - } - } - - if (triggerChangeEvent) { - if (input.hasAttribute('disabled') || rootElement.hasAttribute('disabled') || input.classList.contains('disabled') || rootElement.classList.contains('disabled')) { - return; - } - - input.checked = !$$$1(this._element).hasClass(ClassName.ACTIVE); - $$$1(input).trigger('change'); - } - - input.focus(); - addAriaPressed = false; - } - } - - if (addAriaPressed) { - this._element.setAttribute('aria-pressed', !$$$1(this._element).hasClass(ClassName.ACTIVE)); - } - - if (triggerChangeEvent) { - $$$1(this._element).toggleClass(ClassName.ACTIVE); - } - }; - - _proto.dispose = function dispose() { - $$$1.removeData(this._element, DATA_KEY); - this._element = null; - }; // Static - - - Button._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $$$1(this).data(DATA_KEY); - - if (!data) { - data = new Button(this); - $$$1(this).data(DATA_KEY, data); - } - - if (config === 'toggle') { - data[config](); - } - }); - }; - - _createClass(Button, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }]); - return Button; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, function (event) { - event.preventDefault(); - var button = event.target; - - if (!$$$1(button).hasClass(ClassName.BUTTON)) { - button = $$$1(button).closest(Selector.BUTTON); - } - - Button._jQueryInterface.call($$$1(button), 'toggle'); - }).on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, function (event) { - var button = $$$1(event.target).closest(Selector.BUTTON)[0]; - $$$1(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type)); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $$$1.fn[NAME] = Button._jQueryInterface; - $$$1.fn[NAME].Constructor = Button; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Button._jQueryInterface; - }; - - return Button; - }($); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): carousel.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Carousel = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'carousel'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.carousel'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var TRANSITION_DURATION = 600; - var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key - - var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key - - var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch - - var Default = { - interval: 5000, - keyboard: true, - slide: false, - pause: 'hover', - wrap: true - }; - var DefaultType = { - interval: '(number|boolean)', - keyboard: 'boolean', - slide: '(boolean|string)', - pause: '(string|boolean)', - wrap: 'boolean' - }; - var Direction = { - NEXT: 'next', - PREV: 'prev', - LEFT: 'left', - RIGHT: 'right' - }; - var Event = { - SLIDE: "slide" + EVENT_KEY, - SLID: "slid" + EVENT_KEY, - KEYDOWN: "keydown" + EVENT_KEY, - MOUSEENTER: "mouseenter" + EVENT_KEY, - MOUSELEAVE: "mouseleave" + EVENT_KEY, - TOUCHEND: "touchend" + EVENT_KEY, - LOAD_DATA_API: "load" + EVENT_KEY + DATA_API_KEY, - CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY - }; - var ClassName = { - CAROUSEL: 'carousel', - ACTIVE: 'active', - SLIDE: 'slide', - RIGHT: 'carousel-item-right', - LEFT: 'carousel-item-left', - NEXT: 'carousel-item-next', - PREV: 'carousel-item-prev', - ITEM: 'carousel-item' - }; - var Selector = { - ACTIVE: '.active', - ACTIVE_ITEM: '.active.carousel-item', - ITEM: '.carousel-item', - NEXT_PREV: '.carousel-item-next, .carousel-item-prev', - INDICATORS: '.carousel-indicators', - DATA_SLIDE: '[data-slide], [data-slide-to]', - DATA_RIDE: '[data-ride="carousel"]' - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Carousel = - /*#__PURE__*/ - function () { - function Carousel(element, config) { - this._items = null; - this._interval = null; - this._activeElement = null; - this._isPaused = false; - this._isSliding = false; - this.touchTimeout = null; - this._config = this._getConfig(config); - this._element = $$$1(element)[0]; - this._indicatorsElement = $$$1(this._element).find(Selector.INDICATORS)[0]; - - this._addEventListeners(); - } // Getters - - - var _proto = Carousel.prototype; - - // Public - _proto.next = function next() { - if (!this._isSliding) { - this._slide(Direction.NEXT); - } - }; - - _proto.nextWhenVisible = function nextWhenVisible() { - // Don't call next when the page isn't visible - // or the carousel or its parent isn't visible - if (!document.hidden && $$$1(this._element).is(':visible') && $$$1(this._element).css('visibility') !== 'hidden') { - this.next(); - } - }; - - _proto.prev = function prev() { - if (!this._isSliding) { - this._slide(Direction.PREV); - } - }; - - _proto.pause = function pause(event) { - if (!event) { - this._isPaused = true; - } - - if ($$$1(this._element).find(Selector.NEXT_PREV)[0] && Util.supportsTransitionEnd()) { - Util.triggerTransitionEnd(this._element); - this.cycle(true); - } - - clearInterval(this._interval); - this._interval = null; - }; - - _proto.cycle = function cycle(event) { - if (!event) { - this._isPaused = false; - } - - if (this._interval) { - clearInterval(this._interval); - this._interval = null; - } - - if (this._config.interval && !this._isPaused) { - this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval); - } - }; - - _proto.to = function to(index) { - var _this = this; - - this._activeElement = $$$1(this._element).find(Selector.ACTIVE_ITEM)[0]; - - var activeIndex = this._getItemIndex(this._activeElement); - - if (index > this._items.length - 1 || index < 0) { - return; - } - - if (this._isSliding) { - $$$1(this._element).one(Event.SLID, function () { - return _this.to(index); - }); - return; - } - - if (activeIndex === index) { - this.pause(); - this.cycle(); - return; - } - - var direction = index > activeIndex ? Direction.NEXT : Direction.PREV; - - this._slide(direction, this._items[index]); - }; - - _proto.dispose = function dispose() { - $$$1(this._element).off(EVENT_KEY); - $$$1.removeData(this._element, DATA_KEY); - this._items = null; - this._config = null; - this._element = null; - this._interval = null; - this._isPaused = null; - this._isSliding = null; - this._activeElement = null; - this._indicatorsElement = null; - }; // Private - - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, Default, config); - Util.typeCheckConfig(NAME, config, DefaultType); - return config; - }; - - _proto._addEventListeners = function _addEventListeners() { - var _this2 = this; - - if (this._config.keyboard) { - $$$1(this._element).on(Event.KEYDOWN, function (event) { - return _this2._keydown(event); - }); - } - - if (this._config.pause === 'hover') { - $$$1(this._element).on(Event.MOUSEENTER, function (event) { - return _this2.pause(event); - }).on(Event.MOUSELEAVE, function (event) { - return _this2.cycle(event); - }); - - if ('ontouchstart' in document.documentElement) { - // If it's a touch-enabled device, mouseenter/leave are fired as - // part of the mouse compatibility events on first tap - the carousel - // would stop cycling until user tapped out of it; - // here, we listen for touchend, explicitly pause the carousel - // (as if it's the second time we tap on it, mouseenter compat event - // is NOT fired) and after a timeout (to allow for mouse compatibility - // events to fire) we explicitly restart cycling - $$$1(this._element).on(Event.TOUCHEND, function () { - _this2.pause(); - - if (_this2.touchTimeout) { - clearTimeout(_this2.touchTimeout); - } - - _this2.touchTimeout = setTimeout(function (event) { - return _this2.cycle(event); - }, TOUCHEVENT_COMPAT_WAIT + _this2._config.interval); - }); - } - } - }; - - _proto._keydown = function _keydown(event) { - if (/input|textarea/i.test(event.target.tagName)) { - return; - } - - switch (event.which) { - case ARROW_LEFT_KEYCODE: - event.preventDefault(); - this.prev(); - break; - - case ARROW_RIGHT_KEYCODE: - event.preventDefault(); - this.next(); - break; - - default: - } - }; - - _proto._getItemIndex = function _getItemIndex(element) { - this._items = $$$1.makeArray($$$1(element).parent().find(Selector.ITEM)); - return this._items.indexOf(element); - }; - - _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) { - var isNextDirection = direction === Direction.NEXT; - var isPrevDirection = direction === Direction.PREV; - - var activeIndex = this._getItemIndex(activeElement); - - var lastItemIndex = this._items.length - 1; - var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex; - - if (isGoingToWrap && !this._config.wrap) { - return activeElement; - } - - var delta = direction === Direction.PREV ? -1 : 1; - var itemIndex = (activeIndex + delta) % this._items.length; - return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex]; - }; - - _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) { - var targetIndex = this._getItemIndex(relatedTarget); - - var fromIndex = this._getItemIndex($$$1(this._element).find(Selector.ACTIVE_ITEM)[0]); - - var slideEvent = $$$1.Event(Event.SLIDE, { - relatedTarget: relatedTarget, - direction: eventDirectionName, - from: fromIndex, - to: targetIndex - }); - $$$1(this._element).trigger(slideEvent); - return slideEvent; - }; - - _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) { - if (this._indicatorsElement) { - $$$1(this._indicatorsElement).find(Selector.ACTIVE).removeClass(ClassName.ACTIVE); - - var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)]; - - if (nextIndicator) { - $$$1(nextIndicator).addClass(ClassName.ACTIVE); - } - } - }; - - _proto._slide = function _slide(direction, element) { - var _this3 = this; - - var activeElement = $$$1(this._element).find(Selector.ACTIVE_ITEM)[0]; - - var activeElementIndex = this._getItemIndex(activeElement); - - var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement); - - var nextElementIndex = this._getItemIndex(nextElement); - - var isCycling = Boolean(this._interval); - var directionalClassName; - var orderClassName; - var eventDirectionName; - - if (direction === Direction.NEXT) { - directionalClassName = ClassName.LEFT; - orderClassName = ClassName.NEXT; - eventDirectionName = Direction.LEFT; - } else { - directionalClassName = ClassName.RIGHT; - orderClassName = ClassName.PREV; - eventDirectionName = Direction.RIGHT; - } - - if (nextElement && $$$1(nextElement).hasClass(ClassName.ACTIVE)) { - this._isSliding = false; - return; - } - - var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName); - - if (slideEvent.isDefaultPrevented()) { - return; - } - - if (!activeElement || !nextElement) { - // Some weirdness is happening, so we bail - return; - } - - this._isSliding = true; - - if (isCycling) { - this.pause(); - } - - this._setActiveIndicatorElement(nextElement); - - var slidEvent = $$$1.Event(Event.SLID, { - relatedTarget: nextElement, - direction: eventDirectionName, - from: activeElementIndex, - to: nextElementIndex - }); - - if (Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.SLIDE)) { - $$$1(nextElement).addClass(orderClassName); - Util.reflow(nextElement); - $$$1(activeElement).addClass(directionalClassName); - $$$1(nextElement).addClass(directionalClassName); - $$$1(activeElement).one(Util.TRANSITION_END, function () { - $$$1(nextElement).removeClass(directionalClassName + " " + orderClassName).addClass(ClassName.ACTIVE); - $$$1(activeElement).removeClass(ClassName.ACTIVE + " " + orderClassName + " " + directionalClassName); - _this3._isSliding = false; - setTimeout(function () { - return $$$1(_this3._element).trigger(slidEvent); - }, 0); - }).emulateTransitionEnd(TRANSITION_DURATION); - } else { - $$$1(activeElement).removeClass(ClassName.ACTIVE); - $$$1(nextElement).addClass(ClassName.ACTIVE); - this._isSliding = false; - $$$1(this._element).trigger(slidEvent); - } - - if (isCycling) { - this.cycle(); - } - }; // Static - - - Carousel._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $$$1(this).data(DATA_KEY); - - var _config = _extends({}, Default, $$$1(this).data()); - - if (typeof config === 'object') { - _config = _extends({}, _config, config); - } - - var action = typeof config === 'string' ? config : _config.slide; - - if (!data) { - data = new Carousel(this, _config); - $$$1(this).data(DATA_KEY, data); - } - - if (typeof config === 'number') { - data.to(config); - } else if (typeof action === 'string') { - if (typeof data[action] === 'undefined') { - throw new TypeError("No method named \"" + action + "\""); - } - - data[action](); - } else if (_config.interval) { - data.pause(); - data.cycle(); - } - }); - }; - - Carousel._dataApiClickHandler = function _dataApiClickHandler(event) { - var selector = Util.getSelectorFromElement(this); - - if (!selector) { - return; - } - - var target = $$$1(selector)[0]; - - if (!target || !$$$1(target).hasClass(ClassName.CAROUSEL)) { - return; - } - - var config = _extends({}, $$$1(target).data(), $$$1(this).data()); - var slideIndex = this.getAttribute('data-slide-to'); - - if (slideIndex) { - config.interval = false; - } - - Carousel._jQueryInterface.call($$$1(target), config); - - if (slideIndex) { - $$$1(target).data(DATA_KEY).to(slideIndex); - } - - event.preventDefault(); - }; - - _createClass(Carousel, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }, { - key: "Default", - get: function get() { - return Default; - } - }]); - return Carousel; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler); - $$$1(window).on(Event.LOAD_DATA_API, function () { - $$$1(Selector.DATA_RIDE).each(function () { - var $carousel = $$$1(this); - - Carousel._jQueryInterface.call($carousel, $carousel.data()); - }); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $$$1.fn[NAME] = Carousel._jQueryInterface; - $$$1.fn[NAME].Constructor = Carousel; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Carousel._jQueryInterface; - }; - - return Carousel; - }($); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): collapse.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Collapse = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'collapse'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.collapse'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var TRANSITION_DURATION = 600; - var Default = { - toggle: true, - parent: '' - }; - var DefaultType = { - toggle: 'boolean', - parent: '(string|element)' - }; - var Event = { - SHOW: "show" + EVENT_KEY, - SHOWN: "shown" + EVENT_KEY, - HIDE: "hide" + EVENT_KEY, - HIDDEN: "hidden" + EVENT_KEY, - CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY - }; - var ClassName = { - SHOW: 'show', - COLLAPSE: 'collapse', - COLLAPSING: 'collapsing', - COLLAPSED: 'collapsed' - }; - var Dimension = { - WIDTH: 'width', - HEIGHT: 'height' - }; - var Selector = { - ACTIVES: '.show, .collapsing', - DATA_TOGGLE: '[data-toggle="collapse"]' - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Collapse = - /*#__PURE__*/ - function () { - function Collapse(element, config) { - this._isTransitioning = false; - this._element = element; - this._config = this._getConfig(config); - this._triggerArray = $$$1.makeArray($$$1("[data-toggle=\"collapse\"][href=\"#" + element.id + "\"]," + ("[data-toggle=\"collapse\"][data-target=\"#" + element.id + "\"]"))); - var tabToggles = $$$1(Selector.DATA_TOGGLE); - - for (var i = 0; i < tabToggles.length; i++) { - var elem = tabToggles[i]; - var selector = Util.getSelectorFromElement(elem); - - if (selector !== null && $$$1(selector).filter(element).length > 0) { - this._selector = selector; - - this._triggerArray.push(elem); - } - } - - this._parent = this._config.parent ? this._getParent() : null; - - if (!this._config.parent) { - this._addAriaAndCollapsedClass(this._element, this._triggerArray); - } - - if (this._config.toggle) { - this.toggle(); - } - } // Getters - - - var _proto = Collapse.prototype; - - // Public - _proto.toggle = function toggle() { - if ($$$1(this._element).hasClass(ClassName.SHOW)) { - this.hide(); - } else { - this.show(); - } - }; - - _proto.show = function show() { - var _this = this; - - if (this._isTransitioning || $$$1(this._element).hasClass(ClassName.SHOW)) { - return; - } - - var actives; - var activesData; - - if (this._parent) { - actives = $$$1.makeArray($$$1(this._parent).find(Selector.ACTIVES).filter("[data-parent=\"" + this._config.parent + "\"]")); - - if (actives.length === 0) { - actives = null; - } - } - - if (actives) { - activesData = $$$1(actives).not(this._selector).data(DATA_KEY); - - if (activesData && activesData._isTransitioning) { - return; - } - } - - var startEvent = $$$1.Event(Event.SHOW); - $$$1(this._element).trigger(startEvent); - - if (startEvent.isDefaultPrevented()) { - return; - } - - if (actives) { - Collapse._jQueryInterface.call($$$1(actives).not(this._selector), 'hide'); - - if (!activesData) { - $$$1(actives).data(DATA_KEY, null); - } - } - - var dimension = this._getDimension(); - - $$$1(this._element).removeClass(ClassName.COLLAPSE).addClass(ClassName.COLLAPSING); - this._element.style[dimension] = 0; - - if (this._triggerArray.length > 0) { - $$$1(this._triggerArray).removeClass(ClassName.COLLAPSED).attr('aria-expanded', true); - } - - this.setTransitioning(true); - - var complete = function complete() { - $$$1(_this._element).removeClass(ClassName.COLLAPSING).addClass(ClassName.COLLAPSE).addClass(ClassName.SHOW); - _this._element.style[dimension] = ''; - - _this.setTransitioning(false); - - $$$1(_this._element).trigger(Event.SHOWN); - }; - - if (!Util.supportsTransitionEnd()) { - complete(); - return; - } - - var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); - var scrollSize = "scroll" + capitalizedDimension; - $$$1(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(TRANSITION_DURATION); - this._element.style[dimension] = this._element[scrollSize] + "px"; - }; - - _proto.hide = function hide() { - var _this2 = this; - - if (this._isTransitioning || !$$$1(this._element).hasClass(ClassName.SHOW)) { - return; - } - - var startEvent = $$$1.Event(Event.HIDE); - $$$1(this._element).trigger(startEvent); - - if (startEvent.isDefaultPrevented()) { - return; - } - - var dimension = this._getDimension(); - - this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + "px"; - Util.reflow(this._element); - $$$1(this._element).addClass(ClassName.COLLAPSING).removeClass(ClassName.COLLAPSE).removeClass(ClassName.SHOW); - - if (this._triggerArray.length > 0) { - for (var i = 0; i < this._triggerArray.length; i++) { - var trigger = this._triggerArray[i]; - var selector = Util.getSelectorFromElement(trigger); - - if (selector !== null) { - var $elem = $$$1(selector); - - if (!$elem.hasClass(ClassName.SHOW)) { - $$$1(trigger).addClass(ClassName.COLLAPSED).attr('aria-expanded', false); - } - } - } - } - - this.setTransitioning(true); - - var complete = function complete() { - _this2.setTransitioning(false); - - $$$1(_this2._element).removeClass(ClassName.COLLAPSING).addClass(ClassName.COLLAPSE).trigger(Event.HIDDEN); - }; - - this._element.style[dimension] = ''; - - if (!Util.supportsTransitionEnd()) { - complete(); - return; - } - - $$$1(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(TRANSITION_DURATION); - }; - - _proto.setTransitioning = function setTransitioning(isTransitioning) { - this._isTransitioning = isTransitioning; - }; - - _proto.dispose = function dispose() { - $$$1.removeData(this._element, DATA_KEY); - this._config = null; - this._parent = null; - this._element = null; - this._triggerArray = null; - this._isTransitioning = null; - }; // Private - - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, Default, config); - config.toggle = Boolean(config.toggle); // Coerce string values - - Util.typeCheckConfig(NAME, config, DefaultType); - return config; - }; - - _proto._getDimension = function _getDimension() { - var hasWidth = $$$1(this._element).hasClass(Dimension.WIDTH); - return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT; - }; - - _proto._getParent = function _getParent() { - var _this3 = this; - - var parent = null; - - if (Util.isElement(this._config.parent)) { - parent = this._config.parent; // It's a jQuery object - - if (typeof this._config.parent.jquery !== 'undefined') { - parent = this._config.parent[0]; - } - } else { - parent = $$$1(this._config.parent)[0]; - } - - var selector = "[data-toggle=\"collapse\"][data-parent=\"" + this._config.parent + "\"]"; - $$$1(parent).find(selector).each(function (i, element) { - _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]); - }); - return parent; - }; - - _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) { - if (element) { - var isOpen = $$$1(element).hasClass(ClassName.SHOW); - - if (triggerArray.length > 0) { - $$$1(triggerArray).toggleClass(ClassName.COLLAPSED, !isOpen).attr('aria-expanded', isOpen); - } - } - }; // Static - - - Collapse._getTargetFromElement = function _getTargetFromElement(element) { - var selector = Util.getSelectorFromElement(element); - return selector ? $$$1(selector)[0] : null; - }; - - Collapse._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $this = $$$1(this); - var data = $this.data(DATA_KEY); - - var _config = _extends({}, Default, $this.data(), typeof config === 'object' && config); - - if (!data && _config.toggle && /show|hide/.test(config)) { - _config.toggle = false; - } - - if (!data) { - data = new Collapse(this, _config); - $this.data(DATA_KEY, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - _createClass(Collapse, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }, { - key: "Default", - get: function get() { - return Default; - } - }]); - return Collapse; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - // preventDefault only for <a> elements (which change the URL) not inside the collapsible element - if (event.currentTarget.tagName === 'A') { - event.preventDefault(); - } - - var $trigger = $$$1(this); - var selector = Util.getSelectorFromElement(this); - $$$1(selector).each(function () { - var $target = $$$1(this); - var data = $target.data(DATA_KEY); - var config = data ? 'toggle' : $trigger.data(); - - Collapse._jQueryInterface.call($target, config); - }); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $$$1.fn[NAME] = Collapse._jQueryInterface; - $$$1.fn[NAME].Constructor = Collapse; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Collapse._jQueryInterface; - }; - - return Collapse; - }($); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): modal.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Modal = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'modal'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.modal'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var TRANSITION_DURATION = 300; - var BACKDROP_TRANSITION_DURATION = 150; - var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key - - var Default = { - backdrop: true, - keyboard: true, - focus: true, - show: true - }; - var DefaultType = { - backdrop: '(boolean|string)', - keyboard: 'boolean', - focus: 'boolean', - show: 'boolean' - }; - var Event = { - HIDE: "hide" + EVENT_KEY, - HIDDEN: "hidden" + EVENT_KEY, - SHOW: "show" + EVENT_KEY, - SHOWN: "shown" + EVENT_KEY, - FOCUSIN: "focusin" + EVENT_KEY, - RESIZE: "resize" + EVENT_KEY, - CLICK_DISMISS: "click.dismiss" + EVENT_KEY, - KEYDOWN_DISMISS: "keydown.dismiss" + EVENT_KEY, - MOUSEUP_DISMISS: "mouseup.dismiss" + EVENT_KEY, - MOUSEDOWN_DISMISS: "mousedown.dismiss" + EVENT_KEY, - CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY - }; - var ClassName = { - SCROLLBAR_MEASURER: 'modal-scrollbar-measure', - BACKDROP: 'modal-backdrop', - OPEN: 'modal-open', - FADE: 'fade', - SHOW: 'show' - }; - var Selector = { - DIALOG: '.modal-dialog', - DATA_TOGGLE: '[data-toggle="modal"]', - DATA_DISMISS: '[data-dismiss="modal"]', - FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', - STICKY_CONTENT: '.sticky-top', - NAVBAR_TOGGLER: '.navbar-toggler' - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Modal = - /*#__PURE__*/ - function () { - function Modal(element, config) { - this._config = this._getConfig(config); - this._element = element; - this._dialog = $$$1(element).find(Selector.DIALOG)[0]; - this._backdrop = null; - this._isShown = false; - this._isBodyOverflowing = false; - this._ignoreBackdropClick = false; - this._originalBodyPadding = 0; - this._scrollbarWidth = 0; - } // Getters - - - var _proto = Modal.prototype; - - // Public - _proto.toggle = function toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); - }; - - _proto.show = function show(relatedTarget) { - var _this = this; - - if (this._isTransitioning || this._isShown) { - return; - } - - if (Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.FADE)) { - this._isTransitioning = true; - } - - var showEvent = $$$1.Event(Event.SHOW, { - relatedTarget: relatedTarget - }); - $$$1(this._element).trigger(showEvent); - - if (this._isShown || showEvent.isDefaultPrevented()) { - return; - } - - this._isShown = true; - - this._checkScrollbar(); - - this._setScrollbar(); - - this._adjustDialog(); - - $$$1(document.body).addClass(ClassName.OPEN); - - this._setEscapeEvent(); - - this._setResizeEvent(); - - $$$1(this._element).on(Event.CLICK_DISMISS, Selector.DATA_DISMISS, function (event) { - return _this.hide(event); - }); - $$$1(this._dialog).on(Event.MOUSEDOWN_DISMISS, function () { - $$$1(_this._element).one(Event.MOUSEUP_DISMISS, function (event) { - if ($$$1(event.target).is(_this._element)) { - _this._ignoreBackdropClick = true; - } - }); - }); - - this._showBackdrop(function () { - return _this._showElement(relatedTarget); - }); - }; - - _proto.hide = function hide(event) { - var _this2 = this; - - if (event) { - event.preventDefault(); - } - - if (this._isTransitioning || !this._isShown) { - return; - } - - var hideEvent = $$$1.Event(Event.HIDE); - $$$1(this._element).trigger(hideEvent); - - if (!this._isShown || hideEvent.isDefaultPrevented()) { - return; - } - - this._isShown = false; - var transition = Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.FADE); - - if (transition) { - this._isTransitioning = true; - } - - this._setEscapeEvent(); - - this._setResizeEvent(); - - $$$1(document).off(Event.FOCUSIN); - $$$1(this._element).removeClass(ClassName.SHOW); - $$$1(this._element).off(Event.CLICK_DISMISS); - $$$1(this._dialog).off(Event.MOUSEDOWN_DISMISS); - - if (transition) { - $$$1(this._element).one(Util.TRANSITION_END, function (event) { - return _this2._hideModal(event); - }).emulateTransitionEnd(TRANSITION_DURATION); - } else { - this._hideModal(); - } - }; - - _proto.dispose = function dispose() { - $$$1.removeData(this._element, DATA_KEY); - $$$1(window, document, this._element, this._backdrop).off(EVENT_KEY); - this._config = null; - this._element = null; - this._dialog = null; - this._backdrop = null; - this._isShown = null; - this._isBodyOverflowing = null; - this._ignoreBackdropClick = null; - this._scrollbarWidth = null; - }; - - _proto.handleUpdate = function handleUpdate() { - this._adjustDialog(); - }; // Private - - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, Default, config); - Util.typeCheckConfig(NAME, config, DefaultType); - return config; - }; - - _proto._showElement = function _showElement(relatedTarget) { - var _this3 = this; - - var transition = Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.FADE); - - if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { - // Don't move modal's DOM position - document.body.appendChild(this._element); - } - - this._element.style.display = 'block'; - - this._element.removeAttribute('aria-hidden'); - - this._element.scrollTop = 0; - - if (transition) { - Util.reflow(this._element); - } - - $$$1(this._element).addClass(ClassName.SHOW); - - if (this._config.focus) { - this._enforceFocus(); - } - - var shownEvent = $$$1.Event(Event.SHOWN, { - relatedTarget: relatedTarget - }); - - var transitionComplete = function transitionComplete() { - if (_this3._config.focus) { - _this3._element.focus(); - } - - _this3._isTransitioning = false; - $$$1(_this3._element).trigger(shownEvent); - }; - - if (transition) { - $$$1(this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(TRANSITION_DURATION); - } else { - transitionComplete(); - } - }; - - _proto._enforceFocus = function _enforceFocus() { - var _this4 = this; - - $$$1(document).off(Event.FOCUSIN) // Guard against infinite focus loop - .on(Event.FOCUSIN, function (event) { - if (document !== event.target && _this4._element !== event.target && $$$1(_this4._element).has(event.target).length === 0) { - _this4._element.focus(); - } - }); - }; - - _proto._setEscapeEvent = function _setEscapeEvent() { - var _this5 = this; - - if (this._isShown && this._config.keyboard) { - $$$1(this._element).on(Event.KEYDOWN_DISMISS, function (event) { - if (event.which === ESCAPE_KEYCODE) { - event.preventDefault(); - - _this5.hide(); - } - }); - } else if (!this._isShown) { - $$$1(this._element).off(Event.KEYDOWN_DISMISS); - } - }; - - _proto._setResizeEvent = function _setResizeEvent() { - var _this6 = this; - - if (this._isShown) { - $$$1(window).on(Event.RESIZE, function (event) { - return _this6.handleUpdate(event); - }); - } else { - $$$1(window).off(Event.RESIZE); - } - }; - - _proto._hideModal = function _hideModal() { - var _this7 = this; - - this._element.style.display = 'none'; - - this._element.setAttribute('aria-hidden', true); - - this._isTransitioning = false; - - this._showBackdrop(function () { - $$$1(document.body).removeClass(ClassName.OPEN); - - _this7._resetAdjustments(); - - _this7._resetScrollbar(); - - $$$1(_this7._element).trigger(Event.HIDDEN); - }); - }; - - _proto._removeBackdrop = function _removeBackdrop() { - if (this._backdrop) { - $$$1(this._backdrop).remove(); - this._backdrop = null; - } - }; - - _proto._showBackdrop = function _showBackdrop(callback) { - var _this8 = this; - - var animate = $$$1(this._element).hasClass(ClassName.FADE) ? ClassName.FADE : ''; - - if (this._isShown && this._config.backdrop) { - var doAnimate = Util.supportsTransitionEnd() && animate; - this._backdrop = document.createElement('div'); - this._backdrop.className = ClassName.BACKDROP; - - if (animate) { - $$$1(this._backdrop).addClass(animate); - } - - $$$1(this._backdrop).appendTo(document.body); - $$$1(this._element).on(Event.CLICK_DISMISS, function (event) { - if (_this8._ignoreBackdropClick) { - _this8._ignoreBackdropClick = false; - return; - } - - if (event.target !== event.currentTarget) { - return; - } - - if (_this8._config.backdrop === 'static') { - _this8._element.focus(); - } else { - _this8.hide(); - } - }); - - if (doAnimate) { - Util.reflow(this._backdrop); - } - - $$$1(this._backdrop).addClass(ClassName.SHOW); - - if (!callback) { - return; - } - - if (!doAnimate) { - callback(); - return; - } - - $$$1(this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(BACKDROP_TRANSITION_DURATION); - } else if (!this._isShown && this._backdrop) { - $$$1(this._backdrop).removeClass(ClassName.SHOW); - - var callbackRemove = function callbackRemove() { - _this8._removeBackdrop(); - - if (callback) { - callback(); - } - }; - - if (Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.FADE)) { - $$$1(this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(BACKDROP_TRANSITION_DURATION); - } else { - callbackRemove(); - } - } else if (callback) { - callback(); - } - }; // ---------------------------------------------------------------------- - // the following methods are used to handle overflowing modals - // todo (fat): these should probably be refactored out of modal.js - // ---------------------------------------------------------------------- - - - _proto._adjustDialog = function _adjustDialog() { - var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - - if (!this._isBodyOverflowing && isModalOverflowing) { - this._element.style.paddingLeft = this._scrollbarWidth + "px"; - } - - if (this._isBodyOverflowing && !isModalOverflowing) { - this._element.style.paddingRight = this._scrollbarWidth + "px"; - } - }; - - _proto._resetAdjustments = function _resetAdjustments() { - this._element.style.paddingLeft = ''; - this._element.style.paddingRight = ''; - }; - - _proto._checkScrollbar = function _checkScrollbar() { - var rect = document.body.getBoundingClientRect(); - this._isBodyOverflowing = rect.left + rect.right < window.innerWidth; - this._scrollbarWidth = this._getScrollbarWidth(); - }; - - _proto._setScrollbar = function _setScrollbar() { - var _this9 = this; - - if (this._isBodyOverflowing) { - // Note: DOMNode.style.paddingRight returns the actual value or '' if not set - // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set - // Adjust fixed content padding - $$$1(Selector.FIXED_CONTENT).each(function (index, element) { - var actualPadding = $$$1(element)[0].style.paddingRight; - var calculatedPadding = $$$1(element).css('padding-right'); - $$$1(element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this9._scrollbarWidth + "px"); - }); // Adjust sticky content margin - - $$$1(Selector.STICKY_CONTENT).each(function (index, element) { - var actualMargin = $$$1(element)[0].style.marginRight; - var calculatedMargin = $$$1(element).css('margin-right'); - $$$1(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this9._scrollbarWidth + "px"); - }); // Adjust navbar-toggler margin - - $$$1(Selector.NAVBAR_TOGGLER).each(function (index, element) { - var actualMargin = $$$1(element)[0].style.marginRight; - var calculatedMargin = $$$1(element).css('margin-right'); - $$$1(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) + _this9._scrollbarWidth + "px"); - }); // Adjust body padding - - var actualPadding = document.body.style.paddingRight; - var calculatedPadding = $$$1('body').css('padding-right'); - $$$1('body').data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + "px"); - } - }; - - _proto._resetScrollbar = function _resetScrollbar() { - // Restore fixed content padding - $$$1(Selector.FIXED_CONTENT).each(function (index, element) { - var padding = $$$1(element).data('padding-right'); - - if (typeof padding !== 'undefined') { - $$$1(element).css('padding-right', padding).removeData('padding-right'); - } - }); // Restore sticky content and navbar-toggler margin - - $$$1(Selector.STICKY_CONTENT + ", " + Selector.NAVBAR_TOGGLER).each(function (index, element) { - var margin = $$$1(element).data('margin-right'); - - if (typeof margin !== 'undefined') { - $$$1(element).css('margin-right', margin).removeData('margin-right'); - } - }); // Restore body padding - - var padding = $$$1('body').data('padding-right'); - - if (typeof padding !== 'undefined') { - $$$1('body').css('padding-right', padding).removeData('padding-right'); - } - }; - - _proto._getScrollbarWidth = function _getScrollbarWidth() { - // thx d.walsh - var scrollDiv = document.createElement('div'); - scrollDiv.className = ClassName.SCROLLBAR_MEASURER; - document.body.appendChild(scrollDiv); - var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth; - document.body.removeChild(scrollDiv); - return scrollbarWidth; - }; // Static - - - Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) { - return this.each(function () { - var data = $$$1(this).data(DATA_KEY); - - var _config = _extends({}, Modal.Default, $$$1(this).data(), typeof config === 'object' && config); - - if (!data) { - data = new Modal(this, _config); - $$$1(this).data(DATA_KEY, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](relatedTarget); - } else if (_config.show) { - data.show(relatedTarget); - } - }); - }; - - _createClass(Modal, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }, { - key: "Default", - get: function get() { - return Default; - } - }]); - return Modal; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - var _this10 = this; - - var target; - var selector = Util.getSelectorFromElement(this); - - if (selector) { - target = $$$1(selector)[0]; - } - - var config = $$$1(target).data(DATA_KEY) ? 'toggle' : _extends({}, $$$1(target).data(), $$$1(this).data()); - - if (this.tagName === 'A' || this.tagName === 'AREA') { - event.preventDefault(); - } - - var $target = $$$1(target).one(Event.SHOW, function (showEvent) { - if (showEvent.isDefaultPrevented()) { - // Only register focus restorer if modal will actually get shown - return; - } - - $target.one(Event.HIDDEN, function () { - if ($$$1(_this10).is(':visible')) { - _this10.focus(); - } - }); - }); - - Modal._jQueryInterface.call($$$1(target), config, this); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $$$1.fn[NAME] = Modal._jQueryInterface; - $$$1.fn[NAME].Constructor = Modal; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Modal._jQueryInterface; - }; - - return Modal; - }($); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): tooltip.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Tooltip = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'tooltip'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.tooltip'; - var EVENT_KEY = "." + DATA_KEY; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var TRANSITION_DURATION = 150; - var CLASS_PREFIX = 'bs-tooltip'; - var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g'); - var DefaultType = { - animation: 'boolean', - template: 'string', - title: '(string|element|function)', - trigger: 'string', - delay: '(number|object)', - html: 'boolean', - selector: '(string|boolean)', - placement: '(string|function)', - offset: '(number|string)', - container: '(string|element|boolean)', - fallbackPlacement: '(string|array)', - boundary: '(string|element)' - }; - var AttachmentMap = { - AUTO: 'auto', - TOP: 'top', - RIGHT: 'right', - BOTTOM: 'bottom', - LEFT: 'left' - }; - var Default = { - animation: true, - template: '<div class="tooltip" role="tooltip">' + '<div class="arrow"></div>' + '<div class="tooltip-inner"></div></div>', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - selector: false, - placement: 'top', - offset: 0, - container: false, - fallbackPlacement: 'flip', - boundary: 'scrollParent' - }; - var HoverState = { - SHOW: 'show', - OUT: 'out' - }; - var Event = { - HIDE: "hide" + EVENT_KEY, - HIDDEN: "hidden" + EVENT_KEY, - SHOW: "show" + EVENT_KEY, - SHOWN: "shown" + EVENT_KEY, - INSERTED: "inserted" + EVENT_KEY, - CLICK: "click" + EVENT_KEY, - FOCUSIN: "focusin" + EVENT_KEY, - FOCUSOUT: "focusout" + EVENT_KEY, - MOUSEENTER: "mouseenter" + EVENT_KEY, - MOUSELEAVE: "mouseleave" + EVENT_KEY - }; - var ClassName = { - FADE: 'fade', - SHOW: 'show' - }; - var Selector = { - TOOLTIP: '.tooltip', - TOOLTIP_INNER: '.tooltip-inner', - ARROW: '.arrow' - }; - var Trigger = { - HOVER: 'hover', - FOCUS: 'focus', - CLICK: 'click', - MANUAL: 'manual' - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Tooltip = - /*#__PURE__*/ - function () { - function Tooltip(element, config) { - /** - * Check for Popper dependency - * Popper - https://popper.js.org - */ - if (typeof Popper$1 === 'undefined') { - throw new TypeError('Bootstrap tooltips require Popper.js (https://popper.js.org)'); - } // private - - - this._isEnabled = true; - this._timeout = 0; - this._hoverState = ''; - this._activeTrigger = {}; - this._popper = null; // Protected - - this.element = element; - this.config = this._getConfig(config); - this.tip = null; - - this._setListeners(); - } // Getters - - - var _proto = Tooltip.prototype; - - // Public - _proto.enable = function enable() { - this._isEnabled = true; - }; - - _proto.disable = function disable() { - this._isEnabled = false; - }; - - _proto.toggleEnabled = function toggleEnabled() { - this._isEnabled = !this._isEnabled; - }; - - _proto.toggle = function toggle(event) { - if (!this._isEnabled) { - return; - } - - if (event) { - var dataKey = this.constructor.DATA_KEY; - var context = $$$1(event.currentTarget).data(dataKey); - - if (!context) { - context = new this.constructor(event.currentTarget, this._getDelegateConfig()); - $$$1(event.currentTarget).data(dataKey, context); - } - - context._activeTrigger.click = !context._activeTrigger.click; - - if (context._isWithActiveTrigger()) { - context._enter(null, context); - } else { - context._leave(null, context); - } - } else { - if ($$$1(this.getTipElement()).hasClass(ClassName.SHOW)) { - this._leave(null, this); - - return; - } - - this._enter(null, this); - } - }; - - _proto.dispose = function dispose() { - clearTimeout(this._timeout); - $$$1.removeData(this.element, this.constructor.DATA_KEY); - $$$1(this.element).off(this.constructor.EVENT_KEY); - $$$1(this.element).closest('.modal').off('hide.bs.modal'); - - if (this.tip) { - $$$1(this.tip).remove(); - } - - this._isEnabled = null; - this._timeout = null; - this._hoverState = null; - this._activeTrigger = null; - - if (this._popper !== null) { - this._popper.destroy(); - } - - this._popper = null; - this.element = null; - this.config = null; - this.tip = null; - }; - - _proto.show = function show() { - var _this = this; - - if ($$$1(this.element).css('display') === 'none') { - throw new Error('Please use show on visible elements'); - } - - var showEvent = $$$1.Event(this.constructor.Event.SHOW); - - if (this.isWithContent() && this._isEnabled) { - $$$1(this.element).trigger(showEvent); - var isInTheDom = $$$1.contains(this.element.ownerDocument.documentElement, this.element); - - if (showEvent.isDefaultPrevented() || !isInTheDom) { - return; - } - - var tip = this.getTipElement(); - var tipId = Util.getUID(this.constructor.NAME); - tip.setAttribute('id', tipId); - this.element.setAttribute('aria-describedby', tipId); - this.setContent(); - - if (this.config.animation) { - $$$1(tip).addClass(ClassName.FADE); - } - - var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement; - - var attachment = this._getAttachment(placement); - - this.addAttachmentClass(attachment); - var container = this.config.container === false ? document.body : $$$1(this.config.container); - $$$1(tip).data(this.constructor.DATA_KEY, this); - - if (!$$$1.contains(this.element.ownerDocument.documentElement, this.tip)) { - $$$1(tip).appendTo(container); - } - - $$$1(this.element).trigger(this.constructor.Event.INSERTED); - this._popper = new Popper$1(this.element, tip, { - placement: attachment, - modifiers: { - offset: { - offset: this.config.offset - }, - flip: { - behavior: this.config.fallbackPlacement - }, - arrow: { - element: Selector.ARROW - }, - preventOverflow: { - boundariesElement: this.config.boundary - } - }, - onCreate: function onCreate(data) { - if (data.originalPlacement !== data.placement) { - _this._handlePopperPlacementChange(data); - } - }, - onUpdate: function onUpdate(data) { - _this._handlePopperPlacementChange(data); - } - }); - $$$1(tip).addClass(ClassName.SHOW); // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - - if ('ontouchstart' in document.documentElement) { - $$$1('body').children().on('mouseover', null, $$$1.noop); - } - - var complete = function complete() { - if (_this.config.animation) { - _this._fixTransition(); - } - - var prevHoverState = _this._hoverState; - _this._hoverState = null; - $$$1(_this.element).trigger(_this.constructor.Event.SHOWN); - - if (prevHoverState === HoverState.OUT) { - _this._leave(null, _this); - } - }; - - if (Util.supportsTransitionEnd() && $$$1(this.tip).hasClass(ClassName.FADE)) { - $$$1(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(Tooltip._TRANSITION_DURATION); - } else { - complete(); - } - } - }; - - _proto.hide = function hide(callback) { - var _this2 = this; - - var tip = this.getTipElement(); - var hideEvent = $$$1.Event(this.constructor.Event.HIDE); - - var complete = function complete() { - if (_this2._hoverState !== HoverState.SHOW && tip.parentNode) { - tip.parentNode.removeChild(tip); - } - - _this2._cleanTipClass(); - - _this2.element.removeAttribute('aria-describedby'); - - $$$1(_this2.element).trigger(_this2.constructor.Event.HIDDEN); - - if (_this2._popper !== null) { - _this2._popper.destroy(); - } - - if (callback) { - callback(); - } - }; - - $$$1(this.element).trigger(hideEvent); - - if (hideEvent.isDefaultPrevented()) { - return; - } - - $$$1(tip).removeClass(ClassName.SHOW); // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - - if ('ontouchstart' in document.documentElement) { - $$$1('body').children().off('mouseover', null, $$$1.noop); - } - - this._activeTrigger[Trigger.CLICK] = false; - this._activeTrigger[Trigger.FOCUS] = false; - this._activeTrigger[Trigger.HOVER] = false; - - if (Util.supportsTransitionEnd() && $$$1(this.tip).hasClass(ClassName.FADE)) { - $$$1(tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(TRANSITION_DURATION); - } else { - complete(); - } - - this._hoverState = ''; - }; - - _proto.update = function update() { - if (this._popper !== null) { - this._popper.scheduleUpdate(); - } - }; // Protected - - - _proto.isWithContent = function isWithContent() { - return Boolean(this.getTitle()); - }; - - _proto.addAttachmentClass = function addAttachmentClass(attachment) { - $$$1(this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment); - }; - - _proto.getTipElement = function getTipElement() { - this.tip = this.tip || $$$1(this.config.template)[0]; - return this.tip; - }; - - _proto.setContent = function setContent() { - var $tip = $$$1(this.getTipElement()); - this.setElementContent($tip.find(Selector.TOOLTIP_INNER), this.getTitle()); - $tip.removeClass(ClassName.FADE + " " + ClassName.SHOW); - }; - - _proto.setElementContent = function setElementContent($element, content) { - var html = this.config.html; - - if (typeof content === 'object' && (content.nodeType || content.jquery)) { - // Content is a DOM node or a jQuery - if (html) { - if (!$$$1(content).parent().is($element)) { - $element.empty().append(content); - } - } else { - $element.text($$$1(content).text()); - } - } else { - $element[html ? 'html' : 'text'](content); - } - }; - - _proto.getTitle = function getTitle() { - var title = this.element.getAttribute('data-original-title'); - - if (!title) { - title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title; - } - - return title; - }; // Private - - - _proto._getAttachment = function _getAttachment(placement) { - return AttachmentMap[placement.toUpperCase()]; - }; - - _proto._setListeners = function _setListeners() { - var _this3 = this; - - var triggers = this.config.trigger.split(' '); - triggers.forEach(function (trigger) { - if (trigger === 'click') { - $$$1(_this3.element).on(_this3.constructor.Event.CLICK, _this3.config.selector, function (event) { - return _this3.toggle(event); - }); - } else if (trigger !== Trigger.MANUAL) { - var eventIn = trigger === Trigger.HOVER ? _this3.constructor.Event.MOUSEENTER : _this3.constructor.Event.FOCUSIN; - var eventOut = trigger === Trigger.HOVER ? _this3.constructor.Event.MOUSELEAVE : _this3.constructor.Event.FOCUSOUT; - $$$1(_this3.element).on(eventIn, _this3.config.selector, function (event) { - return _this3._enter(event); - }).on(eventOut, _this3.config.selector, function (event) { - return _this3._leave(event); - }); - } - - $$$1(_this3.element).closest('.modal').on('hide.bs.modal', function () { - return _this3.hide(); - }); - }); - - if (this.config.selector) { - this.config = _extends({}, this.config, { - trigger: 'manual', - selector: '' - }); - } else { - this._fixTitle(); - } - }; - - _proto._fixTitle = function _fixTitle() { - var titleType = typeof this.element.getAttribute('data-original-title'); - - if (this.element.getAttribute('title') || titleType !== 'string') { - this.element.setAttribute('data-original-title', this.element.getAttribute('title') || ''); - this.element.setAttribute('title', ''); - } - }; - - _proto._enter = function _enter(event, context) { - var dataKey = this.constructor.DATA_KEY; - context = context || $$$1(event.currentTarget).data(dataKey); - - if (!context) { - context = new this.constructor(event.currentTarget, this._getDelegateConfig()); - $$$1(event.currentTarget).data(dataKey, context); - } - - if (event) { - context._activeTrigger[event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER] = true; - } - - if ($$$1(context.getTipElement()).hasClass(ClassName.SHOW) || context._hoverState === HoverState.SHOW) { - context._hoverState = HoverState.SHOW; - return; - } - - clearTimeout(context._timeout); - context._hoverState = HoverState.SHOW; - - if (!context.config.delay || !context.config.delay.show) { - context.show(); - return; - } - - context._timeout = setTimeout(function () { - if (context._hoverState === HoverState.SHOW) { - context.show(); - } - }, context.config.delay.show); - }; - - _proto._leave = function _leave(event, context) { - var dataKey = this.constructor.DATA_KEY; - context = context || $$$1(event.currentTarget).data(dataKey); - - if (!context) { - context = new this.constructor(event.currentTarget, this._getDelegateConfig()); - $$$1(event.currentTarget).data(dataKey, context); - } - - if (event) { - context._activeTrigger[event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER] = false; - } - - if (context._isWithActiveTrigger()) { - return; - } - - clearTimeout(context._timeout); - context._hoverState = HoverState.OUT; - - if (!context.config.delay || !context.config.delay.hide) { - context.hide(); - return; - } - - context._timeout = setTimeout(function () { - if (context._hoverState === HoverState.OUT) { - context.hide(); - } - }, context.config.delay.hide); - }; - - _proto._isWithActiveTrigger = function _isWithActiveTrigger() { - for (var trigger in this._activeTrigger) { - if (this._activeTrigger[trigger]) { - return true; - } - } - - return false; - }; - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, this.constructor.Default, $$$1(this.element).data(), config); - - if (typeof config.delay === 'number') { - config.delay = { - show: config.delay, - hide: config.delay - }; - } - - if (typeof config.title === 'number') { - config.title = config.title.toString(); - } - - if (typeof config.content === 'number') { - config.content = config.content.toString(); - } - - Util.typeCheckConfig(NAME, config, this.constructor.DefaultType); - return config; - }; - - _proto._getDelegateConfig = function _getDelegateConfig() { - var config = {}; - - if (this.config) { - for (var key in this.config) { - if (this.constructor.Default[key] !== this.config[key]) { - config[key] = this.config[key]; - } - } - } - - return config; - }; - - _proto._cleanTipClass = function _cleanTipClass() { - var $tip = $$$1(this.getTipElement()); - var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); - - if (tabClass !== null && tabClass.length > 0) { - $tip.removeClass(tabClass.join('')); - } - }; - - _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(data) { - this._cleanTipClass(); - - this.addAttachmentClass(this._getAttachment(data.placement)); - }; - - _proto._fixTransition = function _fixTransition() { - var tip = this.getTipElement(); - var initConfigAnimation = this.config.animation; - - if (tip.getAttribute('x-placement') !== null) { - return; - } - - $$$1(tip).removeClass(ClassName.FADE); - this.config.animation = false; - this.hide(); - this.show(); - this.config.animation = initConfigAnimation; - }; // Static - - - Tooltip._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $$$1(this).data(DATA_KEY); - - var _config = typeof config === 'object' && config; - - if (!data && /dispose|hide/.test(config)) { - return; - } - - if (!data) { - data = new Tooltip(this, _config); - $$$1(this).data(DATA_KEY, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - _createClass(Tooltip, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }, { - key: "Default", - get: function get() { - return Default; - } - }, { - key: "NAME", - get: function get() { - return NAME; - } - }, { - key: "DATA_KEY", - get: function get() { - return DATA_KEY; - } - }, { - key: "Event", - get: function get() { - return Event; - } - }, { - key: "EVENT_KEY", - get: function get() { - return EVENT_KEY; - } - }, { - key: "DefaultType", - get: function get() { - return DefaultType; - } - }]); - return Tooltip; - }(); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[NAME] = Tooltip._jQueryInterface; - $$$1.fn[NAME].Constructor = Tooltip; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Tooltip._jQueryInterface; - }; - - return Tooltip; - }($, Popper$1); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): popover.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Popover = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'popover'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.popover'; - var EVENT_KEY = "." + DATA_KEY; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var CLASS_PREFIX = 'bs-popover'; - var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g'); - var Default = _extends({}, Tooltip.Default, { - placement: 'right', - trigger: 'click', - content: '', - template: '<div class="popover" role="tooltip">' + '<div class="arrow"></div>' + '<h3 class="popover-header"></h3>' + '<div class="popover-body"></div></div>' - }); - var DefaultType = _extends({}, Tooltip.DefaultType, { - content: '(string|element|function)' - }); - var ClassName = { - FADE: 'fade', - SHOW: 'show' - }; - var Selector = { - TITLE: '.popover-header', - CONTENT: '.popover-body' - }; - var Event = { - HIDE: "hide" + EVENT_KEY, - HIDDEN: "hidden" + EVENT_KEY, - SHOW: "show" + EVENT_KEY, - SHOWN: "shown" + EVENT_KEY, - INSERTED: "inserted" + EVENT_KEY, - CLICK: "click" + EVENT_KEY, - FOCUSIN: "focusin" + EVENT_KEY, - FOCUSOUT: "focusout" + EVENT_KEY, - MOUSEENTER: "mouseenter" + EVENT_KEY, - MOUSELEAVE: "mouseleave" + EVENT_KEY - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Popover = - /*#__PURE__*/ - function (_Tooltip) { - _inheritsLoose(Popover, _Tooltip); - - function Popover() { - return _Tooltip.apply(this, arguments) || this; - } - - var _proto = Popover.prototype; - - // Overrides - _proto.isWithContent = function isWithContent() { - return this.getTitle() || this._getContent(); - }; - - _proto.addAttachmentClass = function addAttachmentClass(attachment) { - $$$1(this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment); - }; - - _proto.getTipElement = function getTipElement() { - this.tip = this.tip || $$$1(this.config.template)[0]; - return this.tip; - }; - - _proto.setContent = function setContent() { - var $tip = $$$1(this.getTipElement()); // We use append for html objects to maintain js events - - this.setElementContent($tip.find(Selector.TITLE), this.getTitle()); - - var content = this._getContent(); - - if (typeof content === 'function') { - content = content.call(this.element); - } - - this.setElementContent($tip.find(Selector.CONTENT), content); - $tip.removeClass(ClassName.FADE + " " + ClassName.SHOW); - }; // Private - - - _proto._getContent = function _getContent() { - return this.element.getAttribute('data-content') || this.config.content; - }; - - _proto._cleanTipClass = function _cleanTipClass() { - var $tip = $$$1(this.getTipElement()); - var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); - - if (tabClass !== null && tabClass.length > 0) { - $tip.removeClass(tabClass.join('')); - } - }; // Static - - - Popover._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $$$1(this).data(DATA_KEY); - - var _config = typeof config === 'object' ? config : null; - - if (!data && /destroy|hide/.test(config)) { - return; - } - - if (!data) { - data = new Popover(this, _config); - $$$1(this).data(DATA_KEY, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - _createClass(Popover, null, [{ - key: "VERSION", - // Getters - get: function get() { - return VERSION; - } - }, { - key: "Default", - get: function get() { - return Default; - } - }, { - key: "NAME", - get: function get() { - return NAME; - } - }, { - key: "DATA_KEY", - get: function get() { - return DATA_KEY; - } - }, { - key: "Event", - get: function get() { - return Event; - } - }, { - key: "EVENT_KEY", - get: function get() { - return EVENT_KEY; - } - }, { - key: "DefaultType", - get: function get() { - return DefaultType; - } - }]); - return Popover; - }(Tooltip); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[NAME] = Popover._jQueryInterface; - $$$1.fn[NAME].Constructor = Popover; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Popover._jQueryInterface; - }; - - return Popover; - }($); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): scrollspy.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var ScrollSpy = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'scrollspy'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.scrollspy'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var Default = { - offset: 10, - method: 'auto', - target: '' - }; - var DefaultType = { - offset: 'number', - method: 'string', - target: '(string|element)' - }; - var Event = { - ACTIVATE: "activate" + EVENT_KEY, - SCROLL: "scroll" + EVENT_KEY, - LOAD_DATA_API: "load" + EVENT_KEY + DATA_API_KEY - }; - var ClassName = { - DROPDOWN_ITEM: 'dropdown-item', - DROPDOWN_MENU: 'dropdown-menu', - ACTIVE: 'active' - }; - var Selector = { - DATA_SPY: '[data-spy="scroll"]', - ACTIVE: '.active', - NAV_LIST_GROUP: '.nav, .list-group', - NAV_LINKS: '.nav-link', - NAV_ITEMS: '.nav-item', - LIST_ITEMS: '.list-group-item', - DROPDOWN: '.dropdown', - DROPDOWN_ITEMS: '.dropdown-item', - DROPDOWN_TOGGLE: '.dropdown-toggle' - }; - var OffsetMethod = { - OFFSET: 'offset', - POSITION: 'position' - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var ScrollSpy = - /*#__PURE__*/ - function () { - function ScrollSpy(element, config) { - var _this = this; - - this._element = element; - this._scrollElement = element.tagName === 'BODY' ? window : element; - this._config = this._getConfig(config); - this._selector = this._config.target + " " + Selector.NAV_LINKS + "," + (this._config.target + " " + Selector.LIST_ITEMS + ",") + (this._config.target + " " + Selector.DROPDOWN_ITEMS); - this._offsets = []; - this._targets = []; - this._activeTarget = null; - this._scrollHeight = 0; - $$$1(this._scrollElement).on(Event.SCROLL, function (event) { - return _this._process(event); - }); - this.refresh(); - - this._process(); - } // Getters - - - var _proto = ScrollSpy.prototype; - - // Public - _proto.refresh = function refresh() { - var _this2 = this; - - var autoMethod = this._scrollElement === this._scrollElement.window ? OffsetMethod.OFFSET : OffsetMethod.POSITION; - var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method; - var offsetBase = offsetMethod === OffsetMethod.POSITION ? this._getScrollTop() : 0; - this._offsets = []; - this._targets = []; - this._scrollHeight = this._getScrollHeight(); - var targets = $$$1.makeArray($$$1(this._selector)); - targets.map(function (element) { - var target; - var targetSelector = Util.getSelectorFromElement(element); - - if (targetSelector) { - target = $$$1(targetSelector)[0]; - } - - if (target) { - var targetBCR = target.getBoundingClientRect(); - - if (targetBCR.width || targetBCR.height) { - // TODO (fat): remove sketch reliance on jQuery position/offset - return [$$$1(target)[offsetMethod]().top + offsetBase, targetSelector]; - } - } - - return null; - }).filter(function (item) { - return item; - }).sort(function (a, b) { - return a[0] - b[0]; - }).forEach(function (item) { - _this2._offsets.push(item[0]); - - _this2._targets.push(item[1]); - }); - }; - - _proto.dispose = function dispose() { - $$$1.removeData(this._element, DATA_KEY); - $$$1(this._scrollElement).off(EVENT_KEY); - this._element = null; - this._scrollElement = null; - this._config = null; - this._selector = null; - this._offsets = null; - this._targets = null; - this._activeTarget = null; - this._scrollHeight = null; - }; // Private - - - _proto._getConfig = function _getConfig(config) { - config = _extends({}, Default, config); - - if (typeof config.target !== 'string') { - var id = $$$1(config.target).attr('id'); - - if (!id) { - id = Util.getUID(NAME); - $$$1(config.target).attr('id', id); - } - - config.target = "#" + id; - } - - Util.typeCheckConfig(NAME, config, DefaultType); - return config; - }; - - _proto._getScrollTop = function _getScrollTop() { - return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop; - }; - - _proto._getScrollHeight = function _getScrollHeight() { - return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); - }; - - _proto._getOffsetHeight = function _getOffsetHeight() { - return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height; - }; - - _proto._process = function _process() { - var scrollTop = this._getScrollTop() + this._config.offset; - - var scrollHeight = this._getScrollHeight(); - - var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight(); - - if (this._scrollHeight !== scrollHeight) { - this.refresh(); - } - - if (scrollTop >= maxScroll) { - var target = this._targets[this._targets.length - 1]; - - if (this._activeTarget !== target) { - this._activate(target); - } - - return; - } - - if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) { - this._activeTarget = null; - - this._clear(); - - return; - } - - for (var i = this._offsets.length; i--;) { - var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]); - - if (isActiveTarget) { - this._activate(this._targets[i]); - } - } - }; - - _proto._activate = function _activate(target) { - this._activeTarget = target; - - this._clear(); - - var queries = this._selector.split(','); // eslint-disable-next-line arrow-body-style - - - queries = queries.map(function (selector) { - return selector + "[data-target=\"" + target + "\"]," + (selector + "[href=\"" + target + "\"]"); - }); - var $link = $$$1(queries.join(',')); - - if ($link.hasClass(ClassName.DROPDOWN_ITEM)) { - $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE); - $link.addClass(ClassName.ACTIVE); - } else { - // Set triggered link as active - $link.addClass(ClassName.ACTIVE); // Set triggered links parents as active - // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor - - $link.parents(Selector.NAV_LIST_GROUP).prev(Selector.NAV_LINKS + ", " + Selector.LIST_ITEMS).addClass(ClassName.ACTIVE); // Handle special case when .nav-link is inside .nav-item - - $link.parents(Selector.NAV_LIST_GROUP).prev(Selector.NAV_ITEMS).children(Selector.NAV_LINKS).addClass(ClassName.ACTIVE); - } - - $$$1(this._scrollElement).trigger(Event.ACTIVATE, { - relatedTarget: target - }); - }; - - _proto._clear = function _clear() { - $$$1(this._selector).filter(Selector.ACTIVE).removeClass(ClassName.ACTIVE); - }; // Static - - - ScrollSpy._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $$$1(this).data(DATA_KEY); - - var _config = typeof config === 'object' && config; - - if (!data) { - data = new ScrollSpy(this, _config); - $$$1(this).data(DATA_KEY, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - _createClass(ScrollSpy, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }, { - key: "Default", - get: function get() { - return Default; - } - }]); - return ScrollSpy; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $$$1(window).on(Event.LOAD_DATA_API, function () { - var scrollSpys = $$$1.makeArray($$$1(Selector.DATA_SPY)); - - for (var i = scrollSpys.length; i--;) { - var $spy = $$$1(scrollSpys[i]); - - ScrollSpy._jQueryInterface.call($spy, $spy.data()); - } - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $$$1.fn[NAME] = ScrollSpy._jQueryInterface; - $$$1.fn[NAME].Constructor = ScrollSpy; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return ScrollSpy._jQueryInterface; - }; - - return ScrollSpy; - }($); - - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0): tab.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Tab = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = 'tab'; - var VERSION = '4.0.0'; - var DATA_KEY = 'bs.tab'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var TRANSITION_DURATION = 150; - var Event = { - HIDE: "hide" + EVENT_KEY, - HIDDEN: "hidden" + EVENT_KEY, - SHOW: "show" + EVENT_KEY, - SHOWN: "shown" + EVENT_KEY, - CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY - }; - var ClassName = { - DROPDOWN_MENU: 'dropdown-menu', - ACTIVE: 'active', - DISABLED: 'disabled', - FADE: 'fade', - SHOW: 'show' - }; - var Selector = { - DROPDOWN: '.dropdown', - NAV_LIST_GROUP: '.nav, .list-group', - ACTIVE: '.active', - ACTIVE_UL: '> li > .active', - DATA_TOGGLE: '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]', - DROPDOWN_TOGGLE: '.dropdown-toggle', - DROPDOWN_ACTIVE_CHILD: '> .dropdown-menu .active' - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Tab = - /*#__PURE__*/ - function () { - function Tab(element) { - this._element = element; - } // Getters - - - var _proto = Tab.prototype; - - // Public - _proto.show = function show() { - var _this = this; - - if (this._element.parentNode && this._element.parentNode.nodeType === Node.ELEMENT_NODE && $$$1(this._element).hasClass(ClassName.ACTIVE) || $$$1(this._element).hasClass(ClassName.DISABLED)) { - return; - } - - var target; - var previous; - var listElement = $$$1(this._element).closest(Selector.NAV_LIST_GROUP)[0]; - var selector = Util.getSelectorFromElement(this._element); - - if (listElement) { - var itemSelector = listElement.nodeName === 'UL' ? Selector.ACTIVE_UL : Selector.ACTIVE; - previous = $$$1.makeArray($$$1(listElement).find(itemSelector)); - previous = previous[previous.length - 1]; - } - - var hideEvent = $$$1.Event(Event.HIDE, { - relatedTarget: this._element - }); - var showEvent = $$$1.Event(Event.SHOW, { - relatedTarget: previous - }); - - if (previous) { - $$$1(previous).trigger(hideEvent); - } - - $$$1(this._element).trigger(showEvent); - - if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) { - return; - } - - if (selector) { - target = $$$1(selector)[0]; - } - - this._activate(this._element, listElement); - - var complete = function complete() { - var hiddenEvent = $$$1.Event(Event.HIDDEN, { - relatedTarget: _this._element - }); - var shownEvent = $$$1.Event(Event.SHOWN, { - relatedTarget: previous - }); - $$$1(previous).trigger(hiddenEvent); - $$$1(_this._element).trigger(shownEvent); - }; - - if (target) { - this._activate(target, target.parentNode, complete); - } else { - complete(); - } - }; - - _proto.dispose = function dispose() { - $$$1.removeData(this._element, DATA_KEY); - this._element = null; - }; // Private - - - _proto._activate = function _activate(element, container, callback) { - var _this2 = this; - - var activeElements; - - if (container.nodeName === 'UL') { - activeElements = $$$1(container).find(Selector.ACTIVE_UL); - } else { - activeElements = $$$1(container).children(Selector.ACTIVE); - } - - var active = activeElements[0]; - var isTransitioning = callback && Util.supportsTransitionEnd() && active && $$$1(active).hasClass(ClassName.FADE); - - var complete = function complete() { - return _this2._transitionComplete(element, active, callback); - }; - - if (active && isTransitioning) { - $$$1(active).one(Util.TRANSITION_END, complete).emulateTransitionEnd(TRANSITION_DURATION); - } else { - complete(); - } - }; - - _proto._transitionComplete = function _transitionComplete(element, active, callback) { - if (active) { - $$$1(active).removeClass(ClassName.SHOW + " " + ClassName.ACTIVE); - var dropdownChild = $$$1(active.parentNode).find(Selector.DROPDOWN_ACTIVE_CHILD)[0]; - - if (dropdownChild) { - $$$1(dropdownChild).removeClass(ClassName.ACTIVE); - } - - if (active.getAttribute('role') === 'tab') { - active.setAttribute('aria-selected', false); - } - } - - $$$1(element).addClass(ClassName.ACTIVE); - - if (element.getAttribute('role') === 'tab') { - element.setAttribute('aria-selected', true); - } - - Util.reflow(element); - $$$1(element).addClass(ClassName.SHOW); - - if (element.parentNode && $$$1(element.parentNode).hasClass(ClassName.DROPDOWN_MENU)) { - var dropdownElement = $$$1(element).closest(Selector.DROPDOWN)[0]; - - if (dropdownElement) { - $$$1(dropdownElement).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE); - } - - element.setAttribute('aria-expanded', true); - } - - if (callback) { - callback(); - } - }; // Static - - - Tab._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $this = $$$1(this); - var data = $this.data(DATA_KEY); - - if (!data) { - data = new Tab(this); - $this.data(DATA_KEY, data); - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - _createClass(Tab, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }]); - return Tab; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - event.preventDefault(); - - Tab._jQueryInterface.call($$$1(this), 'show'); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $$$1.fn[NAME] = Tab._jQueryInterface; - $$$1.fn[NAME].Constructor = Tab; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Tab._jQueryInterface; - }; - - return Tab; - }($); - - var Util$2 = function () { - /** - * ------------------------------------------------------------------------ - * Private TransitionEnd Helpers - * ------------------------------------------------------------------------ - */ - var transitionEnd = false; - var _transitionEndSelector = ""; - var TransitionEndEvent = { - WebkitTransition: "webkitTransitionEnd", - MozTransition: "transitionend", - OTransition: "oTransitionEnd otransitionend", - transition: "transitionend" - }; - - function transitionEndTest() { - if (window.QUnit) { - return false; - } - - var el = document.createElement("bmd"); - - for (var name in TransitionEndEvent) { - if (el.style[name] !== undefined) { - return TransitionEndEvent[name]; // { end: TransitionEndEvent[name] } - } - } - - return false; - } - - function setTransitionEndSupport() { - transitionEnd = transitionEndTest(); // generate a concatenated transition end event selector - - for (var name in TransitionEndEvent) { - _transitionEndSelector += " " + TransitionEndEvent[name]; - } - } - /** - * -------------------------------------------------------------------------- - * Public Util Api - * -------------------------------------------------------------------------- - */ - - - var Util = { - transitionEndSupported: function transitionEndSupported() { - return transitionEnd; - }, - transitionEndSelector: function transitionEndSelector() { - return _transitionEndSelector; - }, - isChar: function isChar(event) { - if (typeof event.which === "undefined") { - return true; - } else if (typeof event.which === "number" && event.which > 0) { - return !event.ctrlKey && !event.metaKey && !event.altKey && event.which !== 8 && // backspace - event.which !== 9 && // tab - event.which !== 13 && // enter - event.which !== 16 && // shift - event.which !== 17 && // ctrl - event.which !== 20 && // caps lock - event.which !== 27 // escape - ; - } - - return false; - }, - assert: function assert($element, invalidTest, message) { - if (invalidTest) { - if (!$element === undefined) { - $element.css("border", "1px solid red"); - } - - console.error(message, $element); // eslint-disable-line no-console - - throw message; - } - }, - describe: function describe($element) { - if ($element === undefined) { - return "undefined"; - } else if ($element.length === 0) { - return "(no matching elements)"; - } - - return $element[0].outerHTML.split(">")[0] + ">"; - } - }; - setTransitionEndSupport(); - return Util; - }(jQuery); - - var Base = function ($$$1) { - var ClassName = { - BMD_FORM_GROUP: "bmd-form-group", - IS_FILLED: "is-filled", - IS_FOCUSED: "is-focused" - }; - var Selector = { - BMD_FORM_GROUP: "." + ClassName.BMD_FORM_GROUP - }; - var Default = {}; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Base = - /*#__PURE__*/ - function () { - /** - * - * @param element - * @param config - * @param properties - anything that needs to be set as this[key] = value. Works around the need to call `super` before using `this` - */ - function Base($element, config, properties) { - if (properties === void 0) { - properties = {}; - } - - this.$element = $element; - this.config = $$$1.extend(true, {}, Default, config); // set properties for use in the constructor initialization - - for (var key in properties) { - this[key] = properties[key]; - } - } - - var _proto = Base.prototype; - - _proto.dispose = function dispose(dataKey) { - this.$element.data(dataKey, null); - this.$element = null; - this.config = null; - }; // ------------------------------------------------------------------------ - // protected - - - _proto.addFormGroupFocus = function addFormGroupFocus() { - if (!this.$element.prop("disabled")) { - this.$bmdFormGroup.addClass(ClassName.IS_FOCUSED); - } - }; - - _proto.removeFormGroupFocus = function removeFormGroupFocus() { - this.$bmdFormGroup.removeClass(ClassName.IS_FOCUSED); - }; - - _proto.removeIsFilled = function removeIsFilled() { - this.$bmdFormGroup.removeClass(ClassName.IS_FILLED); - }; - - _proto.addIsFilled = function addIsFilled() { - this.$bmdFormGroup.addClass(ClassName.IS_FILLED); - }; // Find bmd-form-group - - - _proto.findMdbFormGroup = function findMdbFormGroup(raiseError) { - if (raiseError === void 0) { - raiseError = true; - } - - var mfg = this.$element.closest(Selector.BMD_FORM_GROUP); - - if (mfg.length === 0 && raiseError) { - $$$1.error("Failed to find " + Selector.BMD_FORM_GROUP + " for " + Util$2.describe(this.$element)); - } - - return mfg; - }; // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - return Base; - }(); - - return Base; - }(jQuery); - - var BaseInput = function ($$$1) { - var ClassName = { - FORM_GROUP: "form-group", - BMD_FORM_GROUP: "bmd-form-group", - BMD_LABEL: "bmd-label", - BMD_LABEL_STATIC: "bmd-label-static", - BMD_LABEL_PLACEHOLDER: "bmd-label-placeholder", - BMD_LABEL_FLOATING: "bmd-label-floating", - HAS_DANGER: "has-danger", - IS_FILLED: "is-filled", - IS_FOCUSED: "is-focused", - INPUT_GROUP: "input-group" - }; - var Selector = { - FORM_GROUP: "." + ClassName.FORM_GROUP, - BMD_FORM_GROUP: "." + ClassName.BMD_FORM_GROUP, - BMD_LABEL_WILDCARD: "label[class^='" + ClassName.BMD_LABEL + "'], label[class*=' " + ClassName.BMD_LABEL + "']" // match any label variant if specified - - }; - var Default = { - validate: false, - formGroup: { - required: false - }, - bmdFormGroup: { - template: "<span class='" + ClassName.BMD_FORM_GROUP + "'></span>", - create: true, - // create a wrapper if form-group not found - required: true // not recommended to turn this off, only used for inline components - - }, - label: { - required: false, - // Prioritized find order for resolving the label to be used as an bmd-label if not specified in the markup - // - a function(thisComponent); or - // - a string selector used like $bmdFormGroup.find(selector) - // - // Note this only runs if $bmdFormGroup.find(Selector.BMD_LABEL_WILDCARD) fails to find a label (as authored in the markup) - // - selectors: [".form-control-label", // in the case of horizontal or inline forms, this will be marked - "> label" // usual case for text inputs, first child. Deeper would find toggle labels so don't do that. - ], - className: ClassName.BMD_LABEL_STATIC - }, - requiredClasses: [], - invalidComponentMatches: [], - convertInputSizeVariations: true - }; - var FormControlSizeMarkers = { - "form-control-lg": "bmd-form-group-lg", - "form-control-sm": "bmd-form-group-sm" - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var BaseInput = - /*#__PURE__*/ - function (_Base) { - _inheritsLoose(BaseInput, _Base); - - /** - * - * @param element - * @param config - * @param properties - anything that needs to be set as this[key] = value. Works around the need to call `super` before using `this` - */ - function BaseInput($element, config, properties) { - var _this; - - if (properties === void 0) { - properties = {}; - } - - _this = _Base.call(this, $element, $$$1.extend(true, {}, Default, config), properties) || this; // Enforce no overlap between components to prevent side effects - - _this._rejectInvalidComponentMatches(); // Enforce expected structure (if any) - - - _this.rejectWithoutRequiredStructure(); // Enforce required classes for a consistent rendering - - - _this._rejectWithoutRequiredClasses(); // Resolve the form-group first, it will be used for bmd-form-group if possible - // note: different components have different rules - - - _this.$formGroup = _this.findFormGroup(_this.config.formGroup.required); // Will add bmd-form-group to form-group or create an bmd-form-group - // Performance Note: for those forms that are really performance driven, create the markup with the .bmd-form-group to avoid - // rendering changes once added. - - _this.$bmdFormGroup = _this.resolveMdbFormGroup(); // Resolve and mark the bmdLabel if necessary as defined by the config - - _this.$bmdLabel = _this.resolveMdbLabel(); // Signal to the bmd-form-group that a form-control-* variation is being used - - _this.resolveMdbFormGroupSizing(); - - _this.addFocusListener(); - - _this.addChangeListener(); - - if (_this.$element.val() != "") { - _this.addIsFilled(); - } - - return _this; - } - - var _proto = BaseInput.prototype; - - _proto.dispose = function dispose(dataKey) { - _Base.prototype.dispose.call(this, dataKey); - - this.$bmdFormGroup = null; - this.$formGroup = null; - }; // ------------------------------------------------------------------------ - // protected - - - _proto.rejectWithoutRequiredStructure = function rejectWithoutRequiredStructure() {// implement - }; - - _proto.addFocusListener = function addFocusListener() { - var _this2 = this; - - this.$element.on("focus", function () { - _this2.addFormGroupFocus(); - }).on("blur", function () { - _this2.removeFormGroupFocus(); - }); - }; - - _proto.addChangeListener = function addChangeListener() { - var _this3 = this; - - this.$element.on("keydown paste", function (event) { - if (Util$2.isChar(event)) { - _this3.addIsFilled(); - } - }).on("keyup change", function () { - // make sure empty is added back when there is a programmatic value change. - // NOTE: programmatic changing of value using $.val() must trigger the change event i.e. $.val('x').trigger('change') - if (_this3.isEmpty()) { - _this3.removeIsFilled(); - } else { - _this3.addIsFilled(); - } - - if (_this3.config.validate) { - // Validation events do not bubble, so they must be attached directly to the text: http://jsfiddle.net/PEpRM/1/ - // Further, even the bind method is being caught, but since we are already calling #checkValidity here, just alter - // the form-group on change. - // - // NOTE: I'm not sure we should be intervening regarding validation, this seems better as a README and snippet of code. - // BUT, I've left it here for backwards compatibility. - var isValid = typeof _this3.$element[0].checkValidity === "undefined" || _this3.$element[0].checkValidity(); - - if (isValid) { - _this3.removeHasDanger(); - } else { - _this3.addHasDanger(); - } - } - }); - }; - - _proto.addHasDanger = function addHasDanger() { - this.$bmdFormGroup.addClass(ClassName.HAS_DANGER); - }; - - _proto.removeHasDanger = function removeHasDanger() { - this.$bmdFormGroup.removeClass(ClassName.HAS_DANGER); - }; - - _proto.isEmpty = function isEmpty() { - return this.$element.val() === null || this.$element.val() === undefined || this.$element.val() === ""; - }; // Will add bmd-form-group to form-group or create a bmd-form-group if necessary - - - _proto.resolveMdbFormGroup = function resolveMdbFormGroup() { - var mfg = this.findMdbFormGroup(false); - - if (mfg === undefined || mfg.length === 0) { - if (this.config.bmdFormGroup.create && (this.$formGroup === undefined || this.$formGroup.length === 0)) { - // If a form-group doesn't exist (not recommended), take a guess and wrap the element (assuming no label). - // note: it's possible to make this smarter, but I need to see valid cases before adding any complexity. - // this may be an input-group, wrap that instead - if (this.outerElement().parent().hasClass(ClassName.INPUT_GROUP)) { - this.outerElement().parent().wrap(this.config.bmdFormGroup.template); - } else { - this.outerElement().wrap(this.config.bmdFormGroup.template); - } - } else { - // a form-group does exist, add our marker class to it - this.$formGroup.addClass(ClassName.BMD_FORM_GROUP); // OLD: may want to implement this after all, see how the styling turns out, but using an existing form-group is less manipulation of the dom and therefore preferable - // A form-group does exist, so add an bmd-form-group wrapping it's internal contents - //fg.wrapInner(this.config.bmdFormGroup.template) - } - - mfg = this.findMdbFormGroup(this.config.bmdFormGroup.required); - } - - return mfg; - }; // Demarcation element (e.g. first child of a form-group) - // Subclasses such as file inputs may have different structures - - - _proto.outerElement = function outerElement() { - return this.$element; - }; // Will add bmd-label to bmd-form-group if not already specified - - - _proto.resolveMdbLabel = function resolveMdbLabel() { - var label = this.$bmdFormGroup.find(Selector.BMD_LABEL_WILDCARD); - - if (label === undefined || label.length === 0) { - // we need to find it based on the configured selectors - label = this.findMdbLabel(this.config.label.required); - - if (label === undefined || label.length === 0) {// no label found, and finder did not require one - } else { - // a candidate label was found, add the configured default class name - label.addClass(this.config.label.className); - } - } - - return label; - }; // Find bmd-label variant based on the config selectors - - - _proto.findMdbLabel = function findMdbLabel(raiseError) { - if (raiseError === void 0) { - raiseError = true; - } - - var label = null; // use the specified selector order - - for (var _iterator = this.config.label.selectors, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { - var _ref; - - if (_isArray) { - if (_i >= _iterator.length) break; - _ref = _iterator[_i++]; - } else { - _i = _iterator.next(); - if (_i.done) break; - _ref = _i.value; - } - - var _selector = _ref; - - if ($$$1.isFunction(_selector)) { - label = _selector(this); - } else { - label = this.$bmdFormGroup.find(_selector); - } - - if (label !== undefined && label.length > 0) { - break; - } - } - - if (label.length === 0 && raiseError) { - $$$1.error("Failed to find " + Selector.BMD_LABEL_WILDCARD + " within form-group for " + Util$2.describe(this.$element)); - } - - return label; - }; // Find bmd-form-group - - - _proto.findFormGroup = function findFormGroup(raiseError) { - if (raiseError === void 0) { - raiseError = true; - } - - var fg = this.$element.closest(Selector.FORM_GROUP); - - if (fg.length === 0 && raiseError) { - $$$1.error("Failed to find " + Selector.FORM_GROUP + " for " + Util$2.describe(this.$element)); - } - - return fg; - }; // Due to the interconnected nature of labels/inputs/help-blocks, signal the bmd-form-group-* size variation based on - // a found form-control-* size - - - _proto.resolveMdbFormGroupSizing = function resolveMdbFormGroupSizing() { - if (!this.config.convertInputSizeVariations) { - return; - } // Modification - Change text-sm/lg to form-group-sm/lg instead (preferred standard and simpler css/less variants) - - - for (var inputSize in FormControlSizeMarkers) { - if (this.$element.hasClass(inputSize)) { - //this.$element.removeClass(inputSize) - this.$bmdFormGroup.addClass(FormControlSizeMarkers[inputSize]); - } - } - }; // ------------------------------------------------------------------------ - // private - - - _proto._rejectInvalidComponentMatches = function _rejectInvalidComponentMatches() { - for (var _iterator2 = this.config.invalidComponentMatches, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { - var _ref2; - - if (_isArray2) { - if (_i2 >= _iterator2.length) break; - _ref2 = _iterator2[_i2++]; - } else { - _i2 = _iterator2.next(); - if (_i2.done) break; - _ref2 = _i2.value; - } - - var _otherComponent = _ref2; - - _otherComponent.rejectMatch(this.constructor.name, this.$element); - } - }; - - _proto._rejectWithoutRequiredClasses = function _rejectWithoutRequiredClasses() { - for (var _iterator3 = this.config.requiredClasses, _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { - var _ref3; - - if (_isArray3) { - if (_i3 >= _iterator3.length) break; - _ref3 = _iterator3[_i3++]; - } else { - _i3 = _iterator3.next(); - if (_i3.done) break; - _ref3 = _i3.value; - } - - var _requiredClass = _ref3; - if (_requiredClass.indexOf("||") !== -1) { - var oneOf = _requiredClass.split("||"); - - for (var _iterator4 = oneOf, _isArray4 = Array.isArray(_iterator4), _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) { - var _ref4; - - if (_isArray4) { - if (_i4 >= _iterator4.length) break; - _ref4 = _iterator4[_i4++]; - } else { - _i4 = _iterator4.next(); - if (_i4.done) break; - _ref4 = _i4.value; - } - - var _requiredClass3 = _ref4; - - if (this.$element.hasClass(_requiredClass3)) { - break; - } - } - } else if (this.$element.hasClass(_requiredClass)) { - - } - } - }; // ------------------------------------------------------------------------ - // static - - - return BaseInput; - }(Base); - - return BaseInput; - }(jQuery); - - var BaseSelection = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var Default = { - label: { - required: false // Prioritized find order for resolving the label to be used as an bmd-label if not specified in the markup - // - a function(thisComponent); or - // - a string selector used like $bmdFormGroup.find(selector) - // - // Note this only runs if $bmdFormGroup.find(Selector.BMD_LABEL_WILDCARD) fails to find a label (as authored in the markup) - // - //selectors: [ - // `.form-control-label`, // in the case of horizontal or inline forms, this will be marked - // `> label` // usual case for text inputs - //] - - } - }; - var Selector = { - LABEL: "label" - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var BaseSelection = - /*#__PURE__*/ - function (_BaseInput) { - _inheritsLoose(BaseSelection, _BaseInput); - - function BaseSelection($element, config, properties) { - var _this; - - // properties = {inputType: checkbox, outerClass: checkbox-inline} - // '.checkbox|switch|radio > label > input[type=checkbox|radio]' - // '.${this.outerClass} > label > input[type=${this.inputType}]' - _this = _BaseInput.call(this, $element, $$$1.extend(true, {}, Default, config), properties) || this; - - _this.decorateMarkup(); - - return _this; - } // ------------------------------------------------------------------------ - // protected - - - var _proto = BaseSelection.prototype; - - _proto.decorateMarkup = function decorateMarkup() { - var $decorator = $$$1(this.config.template); - this.$element.after($decorator); // initialize ripples after decorator has been inserted into DOM - - if (this.config.ripples !== false) { - $decorator.bmdRipples(); - } - }; // Demarcation element (e.g. first child of a form-group) - - - _proto.outerElement = function outerElement() { - // .checkbox|switch|radio > label > input[type=checkbox|radio] - // label.checkbox-inline > input[type=checkbox|radio] - // .${this.outerClass} > label > input[type=${this.inputType}] - return this.$element.parent().closest("." + this.outerClass); - }; - - _proto.rejectWithoutRequiredStructure = function rejectWithoutRequiredStructure() { - // '.checkbox|switch|radio > label > input[type=checkbox|radio]' - // '.${this.outerClass} > label > input[type=${this.inputType}]' - Util$2.assert(this.$element, !this.$element.parent().prop("tagName") === "label", this.constructor.name + "'s " + Util$2.describe(this.$element) + " parent element should be <label>."); - Util$2.assert(this.$element, !this.outerElement().hasClass(this.outerClass), this.constructor.name + "'s " + Util$2.describe(this.$element) + " outer element should have class " + this.outerClass + "."); - }; - - _proto.addFocusListener = function addFocusListener() { - var _this2 = this; - - // checkboxes didn't appear to bubble to the document, so we'll bind these directly - this.$element.closest(Selector.LABEL).hover(function () { - _this2.addFormGroupFocus(); - }, function () { - _this2.removeFormGroupFocus(); - }); - }; - - _proto.addChangeListener = function addChangeListener() { - var _this3 = this; - - this.$element.change(function () { - _this3.$element.blur(); - }); - }; // ------------------------------------------------------------------------ - // private - - - return BaseSelection; - }(BaseInput); - - return BaseSelection; - }(jQuery); - -//import File from './file' -//import Radio from './radio' -//import Textarea from './textarea' -//import Select from './select' - - var Checkbox = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "checkbox"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = { - template: "<span class='checkbox-decorator'><span class='check'></span></span>" - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Checkbox = - /*#__PURE__*/ - function (_BaseSelection) { - _inheritsLoose(Checkbox, _BaseSelection); - - function Checkbox($element, config, properties) { - if (properties === void 0) { - properties = { - inputType: NAME, - outerClass: NAME - }; - } - - return _BaseSelection.call(this, $element, $$$1.extend(true, //{invalidComponentMatches: [File, Radio, Text, Textarea, Select]}, - Default, config), properties) || this; - } - - var _proto = Checkbox.prototype; - - _proto.dispose = function dispose(dataKey) { - if (dataKey === void 0) { - dataKey = DATA_KEY; - } - - _BaseSelection.prototype.dispose.call(this, dataKey); - }; - - Checkbox.matches = function matches($element) { - // '.checkbox > label > input[type=checkbox]' - if ($element.attr("type") === "checkbox") { - return true; - } - - return false; - }; - - Checkbox.rejectMatch = function rejectMatch(component, $element) { - Util$2.assert(this.$element, this.matches($element), component + " component element " + Util$2.describe($element) + " is invalid for type='checkbox'."); - }; // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - Checkbox._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Checkbox($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Checkbox; - }(BaseSelection); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Checkbox._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Checkbox; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Checkbox._jQueryInterface; - }; - - return Checkbox; - }(jQuery); - - var CheckboxInline = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "checkboxInline"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = { - bmdFormGroup: { - create: false, - // no bmd-form-group creation if form-group not present. It messes with the layout. - required: false - } - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var CheckboxInline = - /*#__PURE__*/ - function (_Checkbox) { - _inheritsLoose(CheckboxInline, _Checkbox); - - function CheckboxInline($element, config, properties) { - if (properties === void 0) { - properties = { - inputType: "checkbox", - outerClass: "checkbox-inline" - }; - } - - return _Checkbox.call(this, $element, $$$1.extend(true, {}, Default, config), properties) || this; - } - - var _proto = CheckboxInline.prototype; - - _proto.dispose = function dispose() { - _Checkbox.prototype.dispose.call(this, DATA_KEY); - }; //static matches($element) { - // // '.checkbox-inline > input[type=checkbox]' - // if ($element.attr('type') === 'checkbox') { - // return true - // } - // return false - //} - // - //static rejectMatch(component, $element) { - // Util.assert(this.$element, this.matches($element), `${component} component element ${Util.describe($element)} is invalid for type='checkbox'.`) - //} - // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - CheckboxInline._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new CheckboxInline($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return CheckboxInline; - }(Checkbox); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = CheckboxInline._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = CheckboxInline; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return CheckboxInline._jQueryInterface; - }; - - return CheckboxInline; - }(jQuery); - - var CollapseInline = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "collapseInline"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Selector = { - ANY_INPUT: "input, select, textarea" - }; - var ClassName = { - IN: "in", - COLLAPSE: "collapse", - COLLAPSING: "collapsing", - COLLAPSED: "collapsed", - WIDTH: "width" - }; - var Default = {}; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var CollapseInline = - /*#__PURE__*/ - function (_Base) { - _inheritsLoose(CollapseInline, _Base); - - // $element is expected to be the trigger - // i.e. <button class="btn bmd-btn-icon" for="search" data-toggle="collapse" data-target="#search-field" aria-expanded="false" aria-controls="search-field"> - function CollapseInline($element, config) { - var _this; - - _this = _Base.call(this, $element, $$$1.extend(true, {}, Default, config)) || this; - _this.$bmdFormGroup = _this.findMdbFormGroup(true); - var collapseSelector = $element.data("target"); - _this.$collapse = $$$1(collapseSelector); - Util$2.assert($element, _this.$collapse.length === 0, "Cannot find collapse target for " + Util$2.describe($element)); - Util$2.assert(_this.$collapse, !_this.$collapse.hasClass(ClassName.COLLAPSE), Util$2.describe(_this.$collapse) + " is expected to have the '" + ClassName.COLLAPSE + "' class. It is being targeted by " + Util$2.describe($element)); // find the first input for focusing - - var $inputs = _this.$bmdFormGroup.find(Selector.ANY_INPUT); - - if ($inputs.length > 0) { - _this.$input = $inputs.first(); - } // automatically add the marker class to collapse width instead of height - nice convenience because it is easily forgotten - - - if (!_this.$collapse.hasClass(ClassName.WIDTH)) { - _this.$collapse.addClass(ClassName.WIDTH); - } - - if (_this.$input) { - // add a listener to set focus - _this.$collapse.on("shown.bs.collapse", function () { - _this.$input.focus(); - }); // add a listener to collapse field - - - _this.$input.blur(function () { - _this.$collapse.collapse("hide"); - }); - } - - return _this; - } - - var _proto = CollapseInline.prototype; - - _proto.dispose = function dispose() { - _Base.prototype.dispose.call(this, DATA_KEY); - - this.$bmdFormGroup = null; - this.$collapse = null; - this.$input = null; - }; // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - CollapseInline._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new CollapseInline($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return CollapseInline; - }(Base); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = CollapseInline._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = CollapseInline; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return CollapseInline._jQueryInterface; - }; - - return CollapseInline; - }(jQuery); - -//import Radio from './radio' -//import Switch from './switch' -//import Text from './text' -//import Textarea from './textarea' -//import Select from './select' - - var File = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "file"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = {}; - var ClassName = { - FILE: NAME, - IS_FILE: "is-file" - }; - var Selector = { - FILENAMES: "input.form-control[readonly]" - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var File = - /*#__PURE__*/ - function (_BaseInput) { - _inheritsLoose(File, _BaseInput); - - function File($element, config) { - var _this; - - _this = _BaseInput.call(this, $element, $$$1.extend(true, //{invalidComponentMatches: [Checkbox, Radio, Text, Textarea, Select, Switch]}, - Default, config)) || this; - - _this.$bmdFormGroup.addClass(ClassName.IS_FILE); - - return _this; - } - - var _proto = File.prototype; - - _proto.dispose = function dispose() { - _BaseInput.prototype.dispose.call(this, DATA_KEY); - }; - - File.matches = function matches($element) { - if ($element.attr("type") === "file") { - return true; - } - - return false; - }; - - File.rejectMatch = function rejectMatch(component, $element) { - Util$2.assert(this.$element, this.matches($element), component + " component element " + Util$2.describe($element) + " is invalid for type='file'."); - }; // ------------------------------------------------------------------------ - // protected - // Demarcation element (e.g. first child of a form-group) - - - _proto.outerElement = function outerElement() { - // label.file > input[type=file] - return this.$element.parent().closest("." + ClassName.FILE); - }; - - _proto.rejectWithoutRequiredStructure = function rejectWithoutRequiredStructure() { - // label.file > input[type=file] - Util$2.assert(this.$element, !this.outerElement().prop("tagName") === "label", this.constructor.name + "'s " + Util$2.describe(this.$element) + " parent element " + Util$2.describe(this.outerElement()) + " should be <label>."); - Util$2.assert(this.$element, !this.outerElement().hasClass(ClassName.FILE), this.constructor.name + "'s " + Util$2.describe(this.$element) + " parent element " + Util$2.describe(this.outerElement()) + " should have class ." + ClassName.FILE + "."); - }; - - _proto.addFocusListener = function addFocusListener() { - var _this2 = this; - - this.$bmdFormGroup.on("focus", function () { - _this2.addFormGroupFocus(); - }).on("blur", function () { - _this2.removeFormGroupFocus(); - }); - }; - - _proto.addChangeListener = function addChangeListener() { - var _this3 = this; - - // set the fileinput readonly field with the name of the file - this.$element.on("change", function () { - var value = ""; - $$$1.each(_this3.$element.files, function (i, file) { - value += file.name + " , "; - }); - value = value.substring(0, value.length - 2); - - if (value) { - _this3.addIsFilled(); - } else { - _this3.removeIsFilled(); - } - - _this3.$bmdFormGroup.find(Selector.FILENAMES).val(value); - }); - }; // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - File._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new File($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return File; - }(BaseInput); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = File._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = File; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return File._jQueryInterface; - }; - - return File; - }(jQuery); - -//import File from './file' -//import Checkbox from './checkbox' -//import Switch from './switch' - - var Radio = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "radio"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = { - template: "<span class='bmd-radio'></span>" - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Radio = - /*#__PURE__*/ - function (_BaseSelection) { - _inheritsLoose(Radio, _BaseSelection); - - function Radio($element, config, properties) { - if (properties === void 0) { - properties = { - inputType: NAME, - outerClass: NAME - }; - } - - return _BaseSelection.call(this, $element, $$$1.extend(true, //{invalidComponentMatches: [Checkbox, File, Switch, Text]}, - Default, config), properties) || this; - } - - var _proto = Radio.prototype; - - _proto.dispose = function dispose(dataKey) { - if (dataKey === void 0) { - dataKey = DATA_KEY; - } - - _BaseSelection.prototype.dispose.call(this, dataKey); - }; - - Radio.matches = function matches($element) { - // '.radio > label > input[type=radio]' - if ($element.attr("type") === "radio") { - return true; - } - - return false; - }; - - Radio.rejectMatch = function rejectMatch(component, $element) { - Util$2.assert(this.$element, this.matches($element), component + " component element " + Util$2.describe($element) + " is invalid for type='radio'."); - }; // ------------------------------------------------------------------------ - // protected - //decorateMarkup() { - // this.$element.after(this.config.template) - //} - // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - Radio._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Radio($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Radio; - }(BaseSelection); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Radio._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Radio; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Radio._jQueryInterface; - }; - - return Radio; - }(jQuery); - - var RadioInline = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "radioInline"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = { - bmdFormGroup: { - create: false, - // no bmd-form-group creation if form-group not present. It messes with the layout. - required: false - } - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var RadioInline = - /*#__PURE__*/ - function (_Radio) { - _inheritsLoose(RadioInline, _Radio); - - function RadioInline($element, config, properties) { - if (properties === void 0) { - properties = { - inputType: "radio", - outerClass: "radio-inline" - }; - } - - return _Radio.call(this, $element, $$$1.extend(true, {}, Default, config), properties) || this; - } - - var _proto = RadioInline.prototype; - - _proto.dispose = function dispose() { - _Radio.prototype.dispose.call(this, DATA_KEY); - }; // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - RadioInline._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new RadioInline($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return RadioInline; - }(Radio); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = RadioInline._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = RadioInline; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return RadioInline._jQueryInterface; - }; - - return RadioInline; - }(jQuery); - - var BaseFormControl = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var Default = { - requiredClasses: ["form-control"] - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var BaseFormControl = - /*#__PURE__*/ - function (_BaseInput) { - _inheritsLoose(BaseFormControl, _BaseInput); - - function BaseFormControl($element, config) { - var _this; - - _this = _BaseInput.call(this, $element, $$$1.extend(true, Default, config)) || this; // Initially mark as empty - - if (_this.isEmpty()) { - _this.removeIsFilled(); - } - - return _this; - } - - return BaseFormControl; - }(BaseInput); - - return BaseFormControl; - }(jQuery); - -//import File from './file' -//import Radio from './radio' -//import Switch from './switch' -//import Text from './text' -//import Textarea from './textarea' - - var Select = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "select"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = { - requiredClasses: ["form-control||custom-select"] - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Select = - /*#__PURE__*/ - function (_BaseFormControl) { - _inheritsLoose(Select, _BaseFormControl); - - function Select($element, config) { - var _this; - - _this = _BaseFormControl.call(this, $element, $$$1.extend(true, //{invalidComponentMatches: [Checkbox, File, Radio, Switch, Text, Textarea]}, - Default, config)) || this; // floating labels will cover the options, so trigger them to be above (if used) - - _this.addIsFilled(); - - return _this; - } - - var _proto = Select.prototype; - - _proto.dispose = function dispose() { - _BaseFormControl.prototype.dispose.call(this, DATA_KEY); - }; - - Select.matches = function matches($element) { - if ($element.prop("tagName") === "select") { - return true; - } - - return false; - }; - - Select.rejectMatch = function rejectMatch(component, $element) { - Util$2.assert(this.$element, this.matches($element), component + " component element " + Util$2.describe($element) + " is invalid for <select>."); - }; // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - Select._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Select($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Select; - }(BaseFormControl); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Select._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Select; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Select._jQueryInterface; - }; - - return Select; - }(jQuery); - - var Switch = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "switch"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = { - template: "<span class='bmd-switch-track'></span>" - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Switch = - /*#__PURE__*/ - function (_Checkbox) { - _inheritsLoose(Switch, _Checkbox); - - function Switch($element, config, properties) { - if (properties === void 0) { - properties = { - inputType: "checkbox", - outerClass: "switch" - }; - } - - return _Checkbox.call(this, $element, $$$1.extend(true, {}, Default, config), properties) || this; // selector: '.switch > label > input[type=checkbox]' - } - - var _proto = Switch.prototype; - - _proto.dispose = function dispose() { - _Checkbox.prototype.dispose.call(this, DATA_KEY); - }; // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - Switch._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Switch($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Switch; - }(Checkbox); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Switch._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Switch; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Switch._jQueryInterface; - }; - - return Switch; - }(jQuery); - -//import File from './file' -//import Radio from './radio' -//import Switch from './switch' -//import Textarea from './textarea' -//import Select from './select' - - var Text = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "text"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = {}; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Text = - /*#__PURE__*/ - function (_BaseFormControl) { - _inheritsLoose(Text, _BaseFormControl); - - function Text($element, config) { - return _BaseFormControl.call(this, $element, $$$1.extend(true, //{invalidComponentMatches: [Checkbox, File, Radio, Switch, Select, Textarea]}, - Default, config)) || this; - } - - var _proto = Text.prototype; - - _proto.dispose = function dispose(dataKey) { - if (dataKey === void 0) { - dataKey = DATA_KEY; - } - - _BaseFormControl.prototype.dispose.call(this, dataKey); - }; - - Text.matches = function matches($element) { - if ($element.attr("type") === "text") { - return true; - } - - return false; - }; - - Text.rejectMatch = function rejectMatch(component, $element) { - Util$2.assert(this.$element, this.matches($element), component + " component element " + Util$2.describe($element) + " is invalid for type='text'."); - }; // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - Text._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Text($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Text; - }(BaseFormControl); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Text._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Text; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Text._jQueryInterface; - }; - - return Text; - }(jQuery); - -//import File from './file' -//import Radio from './radio' -//import Switch from './switch' -//import Text from './text' -//import Select from './select' - - var Textarea = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "textarea"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = {}; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Textarea = - /*#__PURE__*/ - function (_BaseFormControl) { - _inheritsLoose(Textarea, _BaseFormControl); - - function Textarea($element, config) { - return _BaseFormControl.call(this, $element, $$$1.extend(true, //{invalidComponentMatches: [Checkbox, File, Radio, Text, Select, Switch]}, - Default, config)) || this; - } - - var _proto = Textarea.prototype; - - _proto.dispose = function dispose() { - _BaseFormControl.prototype.dispose.call(this, DATA_KEY); - }; - - Textarea.matches = function matches($element) { - if ($element.prop("tagName") === "textarea") { - return true; - } - - return false; - }; - - Textarea.rejectMatch = function rejectMatch(component, $element) { - Util$2.assert(this.$element, this.matches($element), component + " component element " + Util$2.describe($element) + " is invalid for <textarea>."); - }; // ------------------------------------------------------------------------ - // protected - // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - Textarea._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Textarea($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Textarea; - }(BaseFormControl); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Textarea._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Textarea; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Textarea._jQueryInterface; - }; - - return Textarea; - }(jQuery); - - /* global Popper */ - - /** - * This is a copy of the Bootstrap's original dropdown.js, with the only addition - * of two new classes: 'showing' and 'hiding', used to handle animaitons. - */ - /** - * -------------------------------------------------------------------------- - * Bootstrap (v4.1.0): dropdown.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - - var Dropdown = function ($$$1) { - /** - * Check for Popper dependency - * Popper - https://popper.js.org - */ - if (typeof Popper === 'undefined') { - throw new Error('Bootstrap dropdown require Popper.js (https://popper.js.org)'); - } - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - - var NAME = 'dropdown'; - var VERSION = '4.1.0'; - var DATA_KEY = 'bs.dropdown'; - var EVENT_KEY = "." + DATA_KEY; - var DATA_API_KEY = '.data-api'; - var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; - var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key - - var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key - - var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key - - var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key - - var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key - - var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse) - - var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + "|" + ARROW_DOWN_KEYCODE + "|" + ESCAPE_KEYCODE); - var Event = { - HIDE: "hide" + EVENT_KEY, - HIDDEN: "hidden" + EVENT_KEY, - SHOW: "show" + EVENT_KEY, - SHOWN: "shown" + EVENT_KEY, - CLICK: "click" + EVENT_KEY, - CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY, - KEYDOWN_DATA_API: "keydown" + EVENT_KEY + DATA_API_KEY, - KEYUP_DATA_API: "keyup" + EVENT_KEY + DATA_API_KEY, - TRANSITION_END: 'transitionend webkitTransitionEnd oTransitionEnd animationend webkitAnimationEnd oAnimationEnd' - }; - var ClassName = { - DISABLED: 'disabled', - SHOW: 'show', - SHOWING: 'showing', - HIDING: 'hiding', - DROPUP: 'dropup', - MENURIGHT: 'dropdown-menu-right', - MENULEFT: 'dropdown-menu-left' - }; - var Selector = { - DATA_TOGGLE: '[data-toggle="dropdown"]', - FORM_CHILD: '.dropdown form', - MENU: '.dropdown-menu', - NAVBAR_NAV: '.navbar-nav', - VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled)' - }; - var AttachmentMap = { - TOP: 'top-start', - TOPEND: 'top-end', - BOTTOM: 'bottom-start', - BOTTOMEND: 'bottom-end' - }; - var Default = { - placement: AttachmentMap.BOTTOM, - offset: 0, - flip: true - }; - var DefaultType = { - placement: 'string', - offset: '(number|string)', - flip: 'boolean' - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - }; - - var Dropdown = - /*#__PURE__*/ - function () { - function Dropdown(element, config) { - this._element = element; - this._popper = null; - this._config = this._getConfig(config); - this._menu = this._getMenuElement(); - this._inNavbar = this._detectNavbar(); - - this._addEventListeners(); - } // getters - - - var _proto = Dropdown.prototype; - - // public - _proto.toggle = function toggle() { - var _this = this; - - if (this._element.disabled || $$$1(this._element).hasClass(ClassName.DISABLED)) { - return; - } - - var parent = Dropdown._getParentFromElement(this._element); - - var isActive = $$$1(this._menu).hasClass(ClassName.SHOW); - - Dropdown._clearMenus(); - - if (isActive) { - return; - } - - var relatedTarget = { - relatedTarget: this._element - }; - var showEvent = $$$1.Event(Event.SHOW, relatedTarget); - $$$1(parent).trigger(showEvent); - - if (showEvent.isDefaultPrevented()) { - return; - } - - var element = this._element; // for dropup with alignment we use the parent as popper container - - if ($$$1(parent).hasClass(ClassName.DROPUP)) { - if ($$$1(this._menu).hasClass(ClassName.MENULEFT) || $$$1(this._menu).hasClass(ClassName.MENURIGHT)) { - element = parent; - } - } - - this._popper = new Popper(element, this._menu, this._getPopperConfig()); // if this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - - if ('ontouchstart' in document.documentElement && !$$$1(parent).closest(Selector.NAVBAR_NAV).length) { - $$$1('body').children().on('mouseover', null, $$$1.noop); - } - - this._element.focus(); - - this._element.setAttribute('aria-expanded', true); - - $$$1(this._menu).one(Event.TRANSITION_END, function () { - $$$1(parent).trigger($$$1.Event(Event.SHOWN, relatedTarget)); - $$$1(_this._menu).removeClass(ClassName.SHOWING); - }); - $$$1(this._menu).addClass(ClassName.SHOW + " " + ClassName.SHOWING); - $$$1(parent).addClass(ClassName.SHOW); - }; - - _proto.dispose = function dispose() { - $$$1.removeData(this._element, DATA_KEY); - $$$1(this._element).off(EVENT_KEY); - this._element = null; - this._menu = null; - - if (this._popper !== null) { - this._popper.destroy(); - } - - this._popper = null; - }; - - _proto.update = function update() { - this._inNavbar = this._detectNavbar(); - - if (this._popper !== null) { - this._popper.scheduleUpdate(); - } - }; // private - - - _proto._addEventListeners = function _addEventListeners() { - var _this2 = this; - - $$$1(this._element).on(Event.CLICK, function (event) { - event.preventDefault(); - event.stopPropagation(); - - _this2.toggle(); - }); - }; - - _proto._getConfig = function _getConfig(config) { - var elementData = $$$1(this._element).data(); - - if (elementData.placement !== undefined) { - elementData.placement = AttachmentMap[elementData.placement.toUpperCase()]; - } - - config = $$$1.extend({}, this.constructor.Default, $$$1(this._element).data(), config); - Util.typeCheckConfig(NAME, config, this.constructor.DefaultType); - return config; - }; - - _proto._getMenuElement = function _getMenuElement() { - if (!this._menu) { - var parent = Dropdown._getParentFromElement(this._element); - - this._menu = $$$1(parent).find(Selector.MENU)[0]; - } - - return this._menu; - }; - - _proto._getPlacement = function _getPlacement() { - var $parentDropdown = $$$1(this._element).parent(); - var placement = this._config.placement; // Handle dropup - - if ($parentDropdown.hasClass(ClassName.DROPUP) || this._config.placement === AttachmentMap.TOP) { - placement = AttachmentMap.TOP; - - if ($$$1(this._menu).hasClass(ClassName.MENURIGHT)) { - placement = AttachmentMap.TOPEND; - } - } else if ($$$1(this._menu).hasClass(ClassName.MENURIGHT)) { - placement = AttachmentMap.BOTTOMEND; - } - - return placement; - }; - - _proto._detectNavbar = function _detectNavbar() { - return $$$1(this._element).closest('.navbar').length > 0; - }; - - _proto._getPopperConfig = function _getPopperConfig() { - var popperConfig = { - placement: this._getPlacement(), - modifiers: { - offset: { - offset: this._config.offset - }, - flip: { - enabled: this._config.flip - } - } // Disable Popper.js for Dropdown in Navbar - - }; - - if (this._inNavbar) { - popperConfig.modifiers.applyStyle = { - enabled: !this._inNavbar - }; - } - - return popperConfig; - }; // static - - - Dropdown._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var data = $$$1(this).data(DATA_KEY); - - var _config = typeof config === 'object' ? config : null; - - if (!data) { - data = new Dropdown(this, _config); - $$$1(this).data(DATA_KEY, data); - } - - if (typeof config === 'string') { - if (data[config] === undefined) { - throw new Error("No method named \"" + config + "\""); - } - - data[config](); - } - }); - }; - - Dropdown._clearMenus = function _clearMenus(event) { - if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) { - return; - } - - var toggles = $$$1.makeArray($$$1(Selector.DATA_TOGGLE)); - - var _loop = function _loop(i) { - var parent = Dropdown._getParentFromElement(toggles[i]); - - var context = $$$1(toggles[i]).data(DATA_KEY); - var relatedTarget = { - relatedTarget: toggles[i] - }; - - if (!context) { - return "continue"; - } - - var dropdownMenu = context._menu; - - if (!$$$1(parent).hasClass(ClassName.SHOW)) { - return "continue"; - } - - if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $$$1.contains(parent, event.target)) { - return "continue"; - } - - var hideEvent = $$$1.Event(Event.HIDE, relatedTarget); - $$$1(parent).trigger(hideEvent); - - if (hideEvent.isDefaultPrevented()) { - return "continue"; - } // if this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - - - if ('ontouchstart' in document.documentElement) { - $$$1('body').children().off('mouseover', null, $$$1.noop); - } - - toggles[i].setAttribute('aria-expanded', 'false'); - $$$1(dropdownMenu).addClass(ClassName.HIDING).removeClass(ClassName.SHOW); - $$$1(parent).removeClass(ClassName.SHOW); - $$$1(dropdownMenu).one(Event.TRANSITION_END, function () { - $$$1(parent).trigger($$$1.Event(Event.HIDDEN, relatedTarget)); - $$$1(dropdownMenu).removeClass(ClassName.HIDING); - }); - }; - - for (var i = 0; i < toggles.length; i++) { - var _ret = _loop(i); - - if (_ret === "continue") continue; - } - }; - - Dropdown._getParentFromElement = function _getParentFromElement(element) { - var parent; - var selector = Util.getSelectorFromElement(element); - - if (selector) { - parent = $$$1(selector)[0]; - } - - return parent || element.parentNode; - }; - - Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) { - if (!REGEXP_KEYDOWN.test(event.which) || /button/i.test(event.target.tagName) && event.which === SPACE_KEYCODE || /input|textarea/i.test(event.target.tagName)) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - if (this.disabled || $$$1(this).hasClass(ClassName.DISABLED)) { - return; - } - - var parent = Dropdown._getParentFromElement(this); - - var isActive = $$$1(parent).hasClass(ClassName.SHOW); - - if (!isActive && (event.which !== ESCAPE_KEYCODE || event.which !== SPACE_KEYCODE) || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) { - if (event.which === ESCAPE_KEYCODE) { - var toggle = $$$1(parent).find(Selector.DATA_TOGGLE)[0]; - $$$1(toggle).trigger('focus'); - } - - $$$1(this).trigger('click'); - return; - } - - var items = $$$1(parent).find(Selector.VISIBLE_ITEMS).get(); - - if (!items.length) { - return; - } - - var index = items.indexOf(event.target); - - if (event.which === ARROW_UP_KEYCODE && index > 0) { - // up - index--; - } - - if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { - // down - index++; - } - - if (index < 0) { - index = 0; - } - - items[index].focus(); - }; - - _createClass(Dropdown, null, [{ - key: "VERSION", - get: function get() { - return VERSION; - } - }, { - key: "Default", - get: function get() { - return Default; - } - }, { - key: "DefaultType", - get: function get() { - return DefaultType; - } - }]); - return Dropdown; - }(); - /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - - - $$$1(document).on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler).on(Event.CLICK_DATA_API + " " + Event.KEYUP_DATA_API, Dropdown._clearMenus).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - event.preventDefault(); - event.stopPropagation(); - - Dropdown._jQueryInterface.call($$$1(this), 'toggle'); - }).on(Event.CLICK_DATA_API, Selector.FORM_CHILD, function (e) { - e.stopPropagation(); - }); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - $$$1.fn[NAME] = Dropdown._jQueryInterface; - $$$1.fn[NAME].Constructor = Dropdown; - - $$$1.fn[NAME].noConflict = function () { - $$$1.fn[NAME] = JQUERY_NO_CONFLICT; - return Dropdown._jQueryInterface; - }; - - return Dropdown; - }(jQuery); - - var BaseLayout = function ($$$1) { - var ClassName = { - CANVAS: "bmd-layout-canvas", - CONTAINER: "bmd-layout-container", - BACKDROP: "bmd-layout-backdrop" - }; - var Selector = { - CANVAS: "." + ClassName.CANVAS, - CONTAINER: "." + ClassName.CONTAINER, - BACKDROP: "." + ClassName.BACKDROP - }; - var Default = { - canvas: { - create: true, - required: true, - template: "<div class=\"" + ClassName.CANVAS + "\"></div>" - }, - backdrop: { - create: true, - required: true, - template: "<div class=\"" + ClassName.BACKDROP + "\"></div>" - } - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var BaseLayout = - /*#__PURE__*/ - function (_Base) { - _inheritsLoose(BaseLayout, _Base); - - function BaseLayout($element, config, properties) { - var _this; - - if (properties === void 0) { - properties = {}; - } - - _this = _Base.call(this, $element, $$$1.extend(true, {}, Default, config), properties) || this; - _this.$container = _this.findContainer(true); - _this.$backdrop = _this.resolveBackdrop(); - - _this.resolveCanvas(); - - return _this; - } - - var _proto = BaseLayout.prototype; - - _proto.dispose = function dispose(dataKey) { - _Base.prototype.dispose.call(this, dataKey); - - this.$container = null; - this.$backdrop = null; - }; // ------------------------------------------------------------------------ - // protected - // Will wrap container in bmd-layout-canvas if necessary - - - _proto.resolveCanvas = function resolveCanvas() { - var bd = this.findCanvas(false); - - if (bd === undefined || bd.length === 0) { - if (this.config.canvas.create) { - this.$container.wrap(this.config.canvas.template); - } - - bd = this.findCanvas(this.config.canvas.required); - } - - return bd; - }; // Find closest bmd-layout-container based on the given context - - - _proto.findCanvas = function findCanvas(raiseError, context) { - if (raiseError === void 0) { - raiseError = true; - } - - if (context === void 0) { - context = this.$container; - } - - var canvas = context.closest(Selector.CANVAS); - - if (canvas.length === 0 && raiseError) { - $$$1.error("Failed to find " + Selector.CANVAS + " for " + Util$2.describe(context)); - } - - return canvas; - }; // Will add bmd-layout-backdrop to bmd-layout-container if necessary - - - _proto.resolveBackdrop = function resolveBackdrop() { - var bd = this.findBackdrop(false); - - if (bd === undefined || bd.length === 0) { - if (this.config.backdrop.create) { - this.$container.append(this.config.backdrop.template); - } - - bd = this.findBackdrop(this.config.backdrop.required); - } - - return bd; - }; // Find closest bmd-layout-container based on the given context - - - _proto.findBackdrop = function findBackdrop(raiseError, context) { - if (raiseError === void 0) { - raiseError = true; - } - - if (context === void 0) { - context = this.$container; - } - - var backdrop = context.find("> " + Selector.BACKDROP); - - if (backdrop.length === 0 && raiseError) { - $$$1.error("Failed to find " + Selector.BACKDROP + " for " + Util$2.describe(context)); - } - - return backdrop; - }; // Find closest bmd-layout-container based on the given context - - - _proto.findContainer = function findContainer(raiseError, context) { - if (raiseError === void 0) { - raiseError = true; - } - - if (context === void 0) { - context = this.$element; - } - - var container = context.closest(Selector.CONTAINER); - - if (container.length === 0 && raiseError) { - $$$1.error("Failed to find " + Selector.CONTAINER + " for " + Util$2.describe(context)); - } - - return container; - }; // ------------------------------------------------------------------------ - // private - // ------------------------------------------------------------------------ - // static - - - return BaseLayout; - }(Base); - - return BaseLayout; - }(jQuery); - - var Drawer = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "drawer"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Keycodes = { - ESCAPE: 27 //ENTER: 13, - //SPACE: 32 - - }; - var ClassName = { - IN: "in", - DRAWER_IN: "bmd-drawer-in", - DRAWER_OUT: "bmd-drawer-out", - DRAWER: "bmd-layout-drawer", - CONTAINER: "bmd-layout-container" - }; - var Default = { - focusSelector: "a, button, input" - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Drawer = - /*#__PURE__*/ - function (_BaseLayout) { - _inheritsLoose(Drawer, _BaseLayout); - - // $element is expected to be the trigger - // i.e. <button class="btn bmd-btn-icon" for="search" data-toggle="drawer" data-target="#my-side-nav-drawer" aria-expanded="false" aria-controls="my-side-nav-drawer"> - function Drawer($element, config) { - var _this; - - _this = _BaseLayout.call(this, $element, $$$1.extend(true, {}, Default, config)) || this; - _this.$toggles = $$$1("[data-toggle=\"drawer\"][href=\"#" + _this.$element[0].id + "\"], [data-toggle=\"drawer\"][data-target=\"#" + _this.$element[0].id + "\"]"); - - _this._addAria(); // click or escape on the backdrop closes the drawer - - - _this.$backdrop.keydown(function (ev) { - if (ev.which === Keycodes.ESCAPE) { - _this.hide(); - } - }).click(function () { - _this.hide(); - }); // escape on the drawer closes it - - - _this.$element.keydown(function (ev) { - if (ev.which === Keycodes.ESCAPE) { - _this.hide(); - } - }); // any toggle button clicks - - - _this.$toggles.click(function () { - _this.toggle(); - }); - - return _this; - } - - var _proto = Drawer.prototype; - - _proto.dispose = function dispose() { - _BaseLayout.prototype.dispose.call(this, DATA_KEY); - - this.$toggles = null; - }; - - _proto.toggle = function toggle() { - if (this._isOpen()) { - this.hide(); - } else { - this.show(); - } - }; - - _proto.show = function show() { - if (this._isForcedClosed() || this._isOpen()) { - return; - } - - this.$toggles.attr("aria-expanded", true); - this.$element.attr("aria-expanded", true); - this.$element.attr("aria-hidden", false); // focus on the first focusable item - - var $focusOn = this.$element.find(this.config.focusSelector); - - if ($focusOn.length > 0) { - $focusOn.first().focus(); - } - - this.$container.addClass(ClassName.DRAWER_IN); // backdrop is responsively styled based on bmd-drawer-overlay, therefore style is none of our concern, simply add the marker class and let the scss determine if it should be displayed or not. - - this.$backdrop.addClass(ClassName.IN); - }; - - _proto.hide = function hide() { - if (!this._isOpen()) { - return; - } - - this.$toggles.attr("aria-expanded", false); - this.$element.attr("aria-expanded", false); - this.$element.attr("aria-hidden", true); - this.$container.removeClass(ClassName.DRAWER_IN); - this.$backdrop.removeClass(ClassName.IN); - }; // ------------------------------------------------------------------------ - // private - - - _proto._isOpen = function _isOpen() { - return this.$container.hasClass(ClassName.DRAWER_IN); - }; - - _proto._isForcedClosed = function _isForcedClosed() { - return this.$container.hasClass(ClassName.DRAWER_OUT); - }; - - _proto._addAria = function _addAria() { - var isOpen = this._isOpen(); - - this.$element.attr("aria-expanded", isOpen); - this.$element.attr("aria-hidden", isOpen); - - if (this.$toggles.length) { - this.$toggles.attr("aria-expanded", isOpen); - } - }; // ------------------------------------------------------------------------ - // static - - - Drawer._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Drawer($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Drawer; - }(BaseLayout); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Drawer._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Drawer; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Drawer._jQueryInterface; - }; - - return Drawer; - }(jQuery); - - var Ripples = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "ripples"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var ClassName = { - CONTAINER: "ripple-container", - DECORATOR: "ripple-decorator" - }; - var Selector = { - CONTAINER: "." + ClassName.CONTAINER, - DECORATOR: "." + ClassName.DECORATOR //, - - }; - var Default = { - container: { - template: "<div class='" + ClassName.CONTAINER + "'></div>" - }, - decorator: { - template: "<div class='" + ClassName.DECORATOR + "'></div>" - }, - trigger: { - start: "mousedown touchstart", - end: "mouseup mouseleave touchend" - }, - touchUserAgentRegex: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i, - duration: 500 - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Ripples = - /*#__PURE__*/ - function () { - function Ripples($element, config) { - var _this = this; - - this.$element = $element; // console.log(`Adding ripples to ${Util.describe(this.$element)}`) // eslint-disable-line no-console - - this.config = $$$1.extend(true, {}, Default, config); // attach initial listener - - this.$element.on(this.config.trigger.start, function (event) { - _this._onStartRipple(event); - }); - } - - var _proto = Ripples.prototype; - - _proto.dispose = function dispose() { - this.$element.data(DATA_KEY, null); - this.$element = null; - this.$container = null; - this.$decorator = null; - this.config = null; - }; // ------------------------------------------------------------------------ - // private - - - _proto._onStartRipple = function _onStartRipple(event) { - var _this2 = this; - - // Verify if the user is just touching on a device and return if so - if (this._isTouch() && event.type === "mousedown") { - return; - } // Find or create the ripple container element - - - this._findOrCreateContainer(); // Get relY and relX positions of the container element - - - var relY = this._getRelY(event); - - var relX = this._getRelX(event); // If relY and/or relX are false, return the event - - - if (!relY && !relX) { - return; - } // set the location and color each time (even if element is cached) - - - this.$decorator.css({ - left: relX, - top: relY, - "background-color": this._getRipplesColor() - }); // Make sure the ripple has the styles applied (ugly hack but it works) - - this._forceStyleApplication(); // Turn on the ripple animation - - - this.rippleOn(); // Call the rippleEnd function when the transition 'on' ends - - setTimeout(function () { - _this2.rippleEnd(); - }, this.config.duration); // Detect when the user leaves the element to cleanup if not already done? - - this.$element.on(this.config.trigger.end, function () { - if (_this2.$decorator) { - // guard against race condition/mouse attack - _this2.$decorator.data("mousedown", "off"); - - if (_this2.$decorator.data("animating") === "off") { - _this2.rippleOut(); - } - } - }); - }; - - _proto._findOrCreateContainer = function _findOrCreateContainer() { - if (!this.$container || !this.$container.length > 0) { - this.$element.append(this.config.container.template); - this.$container = this.$element.find(Selector.CONTAINER); - } // always add the rippleElement, it is always removed - - - this.$container.append(this.config.decorator.template); - this.$decorator = this.$container.find(Selector.DECORATOR); - }; // Make sure the ripple has the styles applied (ugly hack but it works) - - - _proto._forceStyleApplication = function _forceStyleApplication() { - return window.getComputedStyle(this.$decorator[0]).opacity; - }; - /** - * Get the relX - */ - - - _proto._getRelX = function _getRelX(event) { - var wrapperOffset = this.$container.offset(); - var result = null; - - if (!this._isTouch()) { - // Get the mouse position relative to the ripple wrapper - result = event.pageX - wrapperOffset.left; - } else { - // Make sure the user is using only one finger and then get the touch - // position relative to the ripple wrapper - event = event.originalEvent; - - if (event.touches.length === 1) { - result = event.touches[0].pageX - wrapperOffset.left; - } else { - result = false; - } - } - - return result; - }; - /** - * Get the relY - */ - - - _proto._getRelY = function _getRelY(event) { - var containerOffset = this.$container.offset(); - var result = null; - - if (!this._isTouch()) { - /** - * Get the mouse position relative to the ripple wrapper - */ - result = event.pageY - containerOffset.top; - } else { - /** - * Make sure the user is using only one finger and then get the touch - * position relative to the ripple wrapper - */ - event = event.originalEvent; - - if (event.touches.length === 1) { - result = event.touches[0].pageY - containerOffset.top; - } else { - result = false; - } - } - - return result; - }; - /** - * Get the ripple color - */ - - - _proto._getRipplesColor = function _getRipplesColor() { - var color = this.$element.data("ripple-color") ? this.$element.data("ripple-color") : window.getComputedStyle(this.$element[0]).color; - return color; - }; - /** - * Verify if the client is using a mobile device - */ - - - _proto._isTouch = function _isTouch() { - return this.config.touchUserAgentRegex.test(navigator.userAgent); - }; - /** - * End the animation of the ripple - */ - - - _proto.rippleEnd = function rippleEnd() { - if (this.$decorator) { - // guard against race condition/mouse attack - this.$decorator.data("animating", "off"); - - if (this.$decorator.data("mousedown") === "off") { - this.rippleOut(this.$decorator); - } - } - }; - /** - * Turn off the ripple effect - */ - - - _proto.rippleOut = function rippleOut() { - var _this3 = this; - - this.$decorator.off(); - - if (Util$2.transitionEndSupported()) { - this.$decorator.addClass("ripple-out"); - } else { - this.$decorator.animate({ - opacity: 0 - }, 100, function () { - _this3.$decorator.trigger("transitionend"); - }); - } - - this.$decorator.on(Util$2.transitionEndSelector(), function () { - if (_this3.$decorator) { - _this3.$decorator.remove(); - - _this3.$decorator = null; - } - }); - }; - /** - * Turn on the ripple effect - */ - - - _proto.rippleOn = function rippleOn() { - var _this4 = this; - - var size = this._getNewSize(); - - if (Util$2.transitionEndSupported()) { - this.$decorator.css({ - "-ms-transform": "scale(" + size + ")", - "-moz-transform": "scale(" + size + ")", - "-webkit-transform": "scale(" + size + ")", - transform: "scale(" + size + ")" - }).addClass("ripple-on").data("animating", "on").data("mousedown", "on"); - } else { - this.$decorator.animate({ - width: Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * 2, - height: Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * 2, - "margin-left": Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * -1, - "margin-top": Math.max(this.$element.outerWidth(), this.$element.outerHeight()) * -1, - opacity: 0.2 - }, this.config.duration, function () { - _this4.$decorator.trigger("transitionend"); - }); - } - }; - /** - * Get the new size based on the element height/width and the ripple width - */ - - - _proto._getNewSize = function _getNewSize() { - return Math.max(this.$element.outerWidth(), this.$element.outerHeight()) / this.$decorator.outerWidth() * 2.5; - }; // ------------------------------------------------------------------------ - // static - - - Ripples._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Ripples($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Ripples; - }(); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Ripples._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Ripples; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Ripples._jQueryInterface; - }; - - return Ripples; - }(jQuery); - - var Autofill = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "autofill"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = "bmd" + (NAME.charAt(0).toUpperCase() + NAME.slice(1)); - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - var Default = {}; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var Autofill = - /*#__PURE__*/ - function (_Base) { - _inheritsLoose(Autofill, _Base); - - function Autofill($element, config) { - var _this; - - _this = _Base.call(this, $element, $$$1.extend(true, {}, Default, config)) || this; - - _this._watchLoading(); - - _this._attachEventHandlers(); - - return _this; - } - - var _proto = Autofill.prototype; - - _proto.dispose = function dispose() { - _Base.prototype.dispose.call(this, DATA_KEY); - }; // ------------------------------------------------------------------------ - // private - - - _proto._watchLoading = function _watchLoading() { - var _this2 = this; - - // After 10 seconds we are quite sure all the needed inputs are autofilled then we can stop checking them - setTimeout(function () { - clearInterval(_this2._onLoading); - }, 10000); - }; // This part of code will detect autofill when the page is loading (username and password inputs for example) - - - _proto._onLoading = function _onLoading() { - setInterval(function () { - $$$1("input[type!=checkbox]").each(function (index, element) { - var $element = $$$1(element); - - if ($element.val() && $element.val() !== $element.attr("value")) { - $element.trigger("change"); - } - }); - }, 100); - }; - - _proto._attachEventHandlers = function _attachEventHandlers() { - // Listen on inputs of the focused form - // (because user can select from the autofill dropdown only when the input has focus) - var focused = null; - $$$1(document).on("focus", "input", function (event) { - var $inputs = $$$1(event.currentTarget).closest("form").find("input").not("[type=file]"); - focused = setInterval(function () { - $inputs.each(function (index, element) { - var $element = $$$1(element); - - if ($element.val() !== $element.attr("value")) { - $element.trigger("change"); - } - }); - }, 100); - }).on("blur", ".form-group input", function () { - clearInterval(focused); - }); - }; // ------------------------------------------------------------------------ - // static - - - Autofill._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new Autofill($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return Autofill; - }(Base); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = Autofill._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = Autofill; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return Autofill._jQueryInterface; - }; - - return Autofill; - }(jQuery); - - /* globals Popper */ - Popper.Defaults.modifiers.computeStyle.gpuAcceleration = false; - /** - * $.bootstrapMaterialDesign(config) is a macro class to configure the components generally - * used in Material Design for Bootstrap. You may pass overrides to the configurations - * which will be passed into each component, or you may omit use of this class and - * configure each component separately. - */ - - var BootstrapMaterialDesign = function ($$$1) { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - var NAME = "bootstrapMaterialDesign"; - var DATA_KEY = "bmd." + NAME; - var JQUERY_NAME = NAME; // retain this full name since it is long enough not to conflict - - var JQUERY_NO_CONFLICT = $$$1.fn[JQUERY_NAME]; - /** - * Global configuration: - * The global configuration hash will be mixed in to each components' config. - * e.g. calling $.bootstrapMaterialDesign({global: { validate: true } }) would pass `validate:true` to every component - * - * - * Component configuration: - * - selector: may be a string or an array. Any array will be joined with a comma to generate the selector - * - disable any component by defining it as false with an override. e.g. $.bootstrapMaterialDesign({ autofill: false }) - * - * @see each individual component for more configuration settings. - */ - - var Default = { - global: { - validate: false, - label: { - className: "bmd-label-static" // default style of label to be used if not specified in the html markup - - } - }, - autofill: { - selector: "body" - }, - checkbox: { - selector: ".checkbox > label > input[type=checkbox]" - }, - checkboxInline: { - selector: "label.checkbox-inline > input[type=checkbox]" - }, - collapseInline: { - selector: '.bmd-collapse-inline [data-toggle="collapse"]' - }, - drawer: { - selector: ".bmd-layout-drawer" - }, - file: { - selector: "input[type=file]" - }, - radio: { - selector: ".radio > label > input[type=radio]" - }, - radioInline: { - selector: "label.radio-inline > input[type=radio]" - }, - ripples: { - //selector: ['.btn:not(.btn-link):not(.ripple-none)'] // testing only - selector: [".btn:not(.btn-link):not(.ripple-none)", ".card-image:not(.ripple-none)", ".navbar a:not(.ripple-none)", ".dropdown-menu a:not(.ripple-none)", ".nav-tabs a:not(.ripple-none)", ".pagination li:not(.active):not(.disabled) a:not(.ripple-none)", ".ripple" // generic marker class to add ripple to elements - ] - }, - select: { - selector: ["select"] - }, - switch: { - selector: ".switch > label > input[type=checkbox]" - }, - text: { - // omit inputs we have specialized components to handle - we need to match text, email, etc. The easiest way to do this appears to be just omit the ones we don't want to match and let the rest fall through to this. - selector: ["input:not([type=hidden]):not([type=checkbox]):not([type=radio]):not([type=file]):not([type=button]):not([type=submit]):not([type=reset])"] - }, - textarea: { - selector: ["textarea"] - }, - arrive: true, - // create an ordered component list for instantiation - instantiation: ["ripples", "checkbox", "checkboxInline", "collapseInline", "drawer", //'file', - "radio", "radioInline", "switch", "text", "textarea", "select", "autofill"] - }; - /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - - var BootstrapMaterialDesign = - /*#__PURE__*/ - function () { - function BootstrapMaterialDesign($element, config) { - var _this = this; - - this.$element = $element; - this.config = $$$1.extend(true, {}, Default, config); - var $document = $$$1(document); - - var _loop = function _loop(component) { - // the component's config fragment is passed in directly, allowing users to override - var componentConfig = _this.config[component]; // check to make sure component config is enabled (not `false`) - - if (componentConfig) { - // assemble the selector as it may be an array - var selector = _this._resolveSelector(componentConfig); // mix in global options - - - componentConfig = $$$1.extend(true, {}, _this.config.global, componentConfig); // create the jquery fn name e.g. 'bmdText' for 'text' - - var componentName = "" + (component.charAt(0).toUpperCase() + component.slice(1)); - var jqueryFn = "bmd" + componentName; - - try { - // safely instantiate component on selector elements with config, report errors and move on. - // console.debug(`instantiating: $('${selector}')[${jqueryFn}](${componentConfig})`) // eslint-disable-line no-console - $$$1(selector)[jqueryFn](componentConfig); // add to arrive if present and enabled - - if (document.arrive && _this.config.arrive) { - $document.arrive(selector, function () { - // eslint-disable-line no-loop-func - $$$1(this)[jqueryFn](componentConfig); - }); - } - } catch (e) { - var message = "Failed to instantiate component: $('" + selector + "')[" + jqueryFn + "](" + componentConfig + ")"; - console.error(message, e, "\nSelected elements: ", $$$1(selector)); // eslint-disable-line no-console - - throw e; - } - } - }; - - for (var _iterator = this.config.instantiation, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { - var _ref; - - if (_isArray) { - if (_i >= _iterator.length) break; - _ref = _iterator[_i++]; - } else { - _i = _iterator.next(); - if (_i.done) break; - _ref = _i.value; - } - - var component = _ref; - - _loop(component); - } - } - - var _proto = BootstrapMaterialDesign.prototype; - - _proto.dispose = function dispose() { - this.$element.data(DATA_KEY, null); - this.$element = null; - this.config = null; - }; // ------------------------------------------------------------------------ - // private - - - _proto._resolveSelector = function _resolveSelector(componentConfig) { - var selector = componentConfig.selector; - - if (Array.isArray(selector)) { - selector = selector.join(", "); - } - - return selector; - }; // ------------------------------------------------------------------------ - // static - - - BootstrapMaterialDesign._jQueryInterface = function _jQueryInterface(config) { - return this.each(function () { - var $element = $$$1(this); - var data = $element.data(DATA_KEY); - - if (!data) { - data = new BootstrapMaterialDesign($element, config); - $element.data(DATA_KEY, data); - } - }); - }; - - return BootstrapMaterialDesign; - }(); - /** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ - - - $$$1.fn[JQUERY_NAME] = BootstrapMaterialDesign._jQueryInterface; - $$$1.fn[JQUERY_NAME].Constructor = BootstrapMaterialDesign; - - $$$1.fn[JQUERY_NAME].noConflict = function () { - $$$1.fn[JQUERY_NAME] = JQUERY_NO_CONFLICT; - return BootstrapMaterialDesign._jQueryInterface; - }; - - return BootstrapMaterialDesign; - }(jQuery); - - /* - * This is the main entry point. - * - * You can import other modules here, including external packages. When bundling using rollup you can mark those modules as external and have them excluded or, if they have a jsnext:main entry in their package.json (like this package does), let rollup bundle them into your dist file. - * - * at your application entry point. This is necessary for browsers that do not yet support some ES2015 runtime necessities such as Symbol. We do this in `index-iife.js` for our iife rollup bundle. - */ -// Bootstrap components - -}))); -//# sourceMappingURL=bootstrap-material-design.js.map diff --git a/vendor/emoji-picker/.gitignore b/vendor/emoji-picker/.gitignore new file mode 100644 index 00000000..2ccbe465 --- /dev/null +++ b/vendor/emoji-picker/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/vendor/emoji-picker/LICENSE b/vendor/emoji-picker/LICENSE new file mode 100644 index 00000000..a6f59843 --- /dev/null +++ b/vendor/emoji-picker/LICENSE @@ -0,0 +1,3 @@ +LsxEmojiPicker +A simple and lightweight emoji picker plugin for jQuery +(c) 2018 Lascaux s.r.l. \ No newline at end of file diff --git a/vendor/emoji-picker/README.md b/vendor/emoji-picker/README.md new file mode 100644 index 00000000..0335c3ef --- /dev/null +++ b/vendor/emoji-picker/README.md @@ -0,0 +1,2 @@ +# lsx-emojipicker +A simple emoji picker plugin for jQuery using https://github.com/twitter/twemoji diff --git a/vendor/emoji-picker/index.html b/vendor/emoji-picker/index.html new file mode 100644 index 00000000..e299fa58 --- /dev/null +++ b/vendor/emoji-picker/index.html @@ -0,0 +1,70 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> +<meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Lascaux jQuery Emoji picker plugin demo + + + + + + +

+
+ +
+
+
+
+
+
+

Lascaux jQuery Emoji picker plugin demo

+ + +
+
+ + + + + + + diff --git a/vendor/emoji-picker/package.json b/vendor/emoji-picker/package.json new file mode 100644 index 00000000..3bd12676 --- /dev/null +++ b/vendor/emoji-picker/package.json @@ -0,0 +1,29 @@ +{ + "name": "lsx-emojipicker", + "version": "1.1.2", + "description": "A simple emoji picker plugin for jQuery", + "repository": "https://github.com/LascauxSRL/lsx-emojipicker.git", + "author": "Lascaux S.r.l.", + "license": "MIT", + "scripts": { + "build": "yarn build:dev", + "build:dev": "webpack --config webpack.config.js --progress", + "start": "webpack --config webpack.config.js --progress --watch", + "build:prod": "webpack --config webpack.config.js --env=production", + "version": "yarn install && yarn build:prod && git add -A", + "postversion": "git push" + }, + "devDependencies": { + "css-loader": "^0.28.2", + "style-loader": "^0.18.0", + "uglifyjs-webpack-plugin": "1.1.2", + "webpack": "3.10.0" + }, + "contributors": [ + { + "name": "Lorenzo Cioni", + "email": "lorenzo.cioni@lascaux.it", + "url": "https://github.com/lorecioni" + } + ] +} diff --git a/vendor/emoji-picker/src/jquery.lsxemojipicker.css b/vendor/emoji-picker/src/jquery.lsxemojipicker.css new file mode 100644 index 00000000..b1fd2ffa --- /dev/null +++ b/vendor/emoji-picker/src/jquery.lsxemojipicker.css @@ -0,0 +1,103 @@ +.lsx-emojipicker-emoji span { + display: inline-block; + font-size: 24px; + width: 33px; + height: 35px; + cursor: pointer; +} +.lsx-emojipicker-appender { + position: relative; +} +.lsx-emojipicker-container { + background: #ffffff; + border-radius: 5px; + z-index: 99999999999; + position: absolute; + top: -270px; + right: -20px; + box-shadow: 0 12px 29px rgba(0,0,0,.2); + transition: all 0.5s ease-in-out; + -webkit-transition: all 0.5s ease-in-out; + display: none; +} +ul.lsx-emojipicker-tabs { + margin: 0; + padding: 0 10px; + list-style: none; + text-align: left; + background-color: #eee; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top: 1px solid #ddd; +} +ul.lsx-emojipicker-tabs li { + display: inline-block; + text-align: left; + font-size: 15px; + padding: 6px; + cursor: pointer; + align-self: center; + opacity: 0.5; +} +ul.lsx-emojipicker-tabs li.selected, +ul.lsx-emojipicker-tabs li:hover { + opacity: 1; +} +.lsx-emojipicker-tabs img.emoji { + width: 22px; + margin: 5px 10px; + opacity: 0.5; + cursor: pointer; +} +.lsx-emojipicker-tabs img.emoji:hover, +.lsx-emojipicker-tabs li.selected img.emoji { + opacity: 1; +} + + +.lsx-emojipicker-wrapper .lsx-emoji-tab { + width: 220px; + padding: 8px; + height: 200px; + border-radius: 4px; + overflow: auto; +} +.lsx-emojipicker-wrapper .lsx-emoji-tab img.emoji { + width: 25px; + margin: 5px 4px; + cursor: pointer; + transition: all 0.1s ease-in-out; + -webkit-transition: all 0.1s ease-in-out; +} + +.lsx-emojipicker-wrapper span:hover, +.lsx-emojipicker-wrapper img.emoji:hover { + transform: scale(1.1); + -webkit-transform: scale(1.1); + -ms-transform: scale(1.1); +} +ul.lsx-emojipicker-tabs li.selected { + border-bottom: 2px solid #b5b5b5; +} +.lsx-emojipicker-wrapper { + width: 100%; + height: 100%; + position: relative; +} +.lsx-emojipicker-container:after { + position: absolute; + display: block; + content: ''; + clear: both; + top: 100%; + right: 35px; + margin-bottom: -15px; + width: 0; + height: 0; + border-style: solid; + border-width: 15px 15px 0 15px; + border-color: #eeeeee transparent transparent transparent; +} +.lsx-emojipicker-emoji.lsx-emoji-tab.hidden { + display: none; +} \ No newline at end of file diff --git a/vendor/emoji-picker/src/jquery.lsxemojipicker.ts b/vendor/emoji-picker/src/jquery.lsxemojipicker.ts new file mode 100644 index 00000000..508f4b76 --- /dev/null +++ b/vendor/emoji-picker/src/jquery.lsxemojipicker.ts @@ -0,0 +1,814 @@ + +declare let twemoji: any; + +interface Window { + setup_lsx_emoji_picker: (options: JSXEmojiPickerSetupOptions) => Promise; +} + +interface JSXEmojiPickerSetupOptions { + twemoji: boolean +} + +if(typeof jQuery !== 'undefined'){ + (function ($, win) { + 'use strict'; + + let setup_options: JSXEmojiPickerSetupOptions; + var emoji = { + 'people': [ + {'name': 'smile', 'value': '😄'}, + {'name': 'smiley', 'value': '😃'}, + {'name': 'grinning', 'value': '😀'}, + {'name': 'blush', 'value': '😊'}, + {'name': 'wink', 'value': '😉'}, + {'name': 'heart-eyes', 'value': '😍'}, + {'name': 'kissing-heart', 'value': '😘'}, + {'name': 'kissing-closed-eyes', 'value': '😚'}, + {'name': 'kissing', 'value': '😗'}, + {'name': 'kissing-smiling-eyes', 'value': '😙'}, + {'name': 'stuck-out-tongue-winking-eye', 'value': '😜'}, + {'name': 'stuck-out-tongue-closed-eyes', 'value': '😝'}, + {'name': 'stuck-out-tongue', 'value': '😛'}, + {'name': 'flushed', 'value': '😳'}, + {'name': 'grin', 'value': '😁'}, + {'name': 'pensive', 'value': '😔'}, + {'name': 'satisfied', 'value': '😌'}, + {'name': 'unamused', 'value': '😒'}, + {'name': 'disappointed', 'value': '😞'}, + {'name': 'persevere', 'value': '😣'}, + {'name': 'cry', 'value': '😢'}, + {'name': 'joy', 'value': '😂'}, + {'name': 'sob', 'value': '😭'}, + {'name': 'sleepy', 'value': '😪'}, + {'name': 'relieved', 'value': '😥'}, + {'name': 'cold-sweat', 'value': '😰'}, + {'name': 'sweat-smile', 'value': '😅'}, + {'name': 'sweat', 'value': '😓'}, + {'name': 'weary', 'value': '😩'}, + {'name': 'tired-face', 'value': '😫'}, + {'name': 'fearful', 'value': '😨'}, + {'name': 'scream', 'value': '😱'}, + {'name': 'angry', 'value': '😠'}, + {'name': 'rage', 'value': '😡'}, + {'name': 'triumph', 'value': '😤'}, + {'name': 'confounded', 'value': '😖'}, + {'name': 'laughing', 'value': '😆'}, + {'name': 'yum', 'value': '😋'}, + {'name': 'mask', 'value': '😷'}, + {'name': 'sunglasses', 'value': '😎'}, + {'name': 'sleeping', 'value': '😴'}, + {'name': 'dizzy-face', 'value': '😵'}, + {'name': 'astonished', 'value': '😲'}, + {'name': 'worried', 'value': '😟'}, + {'name': 'frowning', 'value': '😦'}, + {'name': 'anguished', 'value': '😧'}, + {'name': 'smiling-imp', 'value': '😈'}, + {'name': 'imp', 'value': '👿'}, + {'name': 'open-mouth', 'value': '😮'}, + {'name': 'grimacing', 'value': '😬'}, + {'name': 'neutral-face', 'value': '😐'}, + {'name': 'confused', 'value': '😕'}, + {'name': 'hushed', 'value': '😯'}, + {'name': 'no-mouth', 'value': '😶'}, + {'name': 'innocent', 'value': '😇'}, + {'name': 'smirk', 'value': '😏'}, + {'name': 'expressionless', 'value': '😑'}, + {'name': 'man-with-gua-pi-mao', 'value': '👲'}, + {'name': 'man-with-turban', 'value': '👳'}, + {'name': 'cop', 'value': '👮'}, + {'name': 'construction-worker', 'value': '👷'}, + {'name': 'guardsman', 'value': '💂'}, + {'name': 'baby', 'value': '👶'}, + {'name': 'boy', 'value': '👦'}, + {'name': 'girl', 'value': '👧'}, + {'name': 'man', 'value': '👨'}, + {'name': 'woman', 'value': '👩'}, + {'name': 'older-man', 'value': '👴'}, + {'name': 'older-woman', 'value': '👵'}, + {'name': 'person-with-blond-hair', 'value': '👱'}, + {'name': 'angel', 'value': '👼'}, + {'name': 'princess', 'value': '👸'}, + {'name': 'smiley-cat', 'value': '😺'}, + {'name': 'smile-cat', 'value': '😸'}, + {'name': 'heart-eyes-cat', 'value': '😻'}, + {'name': 'kissing-cat', 'value': '😽'}, + {'name': 'smirk-cat', 'value': '😼'}, + {'name': 'scream-cat', 'value': '🙀'}, + {'name': 'crying-cat-face', 'value': '😿'}, + {'name': 'joy-cat', 'value': '😹'}, + {'name': 'pouting-cat', 'value': '😾'}, + {'name': 'japanese-ogre', 'value': '👹'}, + {'name': 'japanese-goblin', 'value': '👺'}, + {'name': 'see-no-evil', 'value': '🙈'}, + {'name': 'hear-no-evil', 'value': '🙉'}, + {'name': 'speak-no-evil', 'value': '🙊'}, + {'name': 'skull', 'value': '💀'}, + {'name': 'alien', 'value': '👽'}, + {'name': 'poop', 'value': '💩'}, + {'name': 'fire', 'value': '🔥'}, + {'name': 'sparkles', 'value': '✨'}, + {'name': 'star2', 'value': '🌟'}, + {'name': 'dizzy', 'value': '💫'}, + {'name': 'boom', 'value': '💥'}, + {'name': 'anger', 'value': '💢'}, + {'name': 'sweat-drops', 'value': '💦'}, + {'name': 'droplet', 'value': '💧'}, + {'name': 'zzz', 'value': '💤'}, + {'name': 'dash', 'value': '💨'}, + {'name': 'ear', 'value': '👂'}, + {'name': 'eyes', 'value': '👀'}, + {'name': 'nose', 'value': '👃'}, + {'name': 'tongue', 'value': '👅'}, + {'name': 'lips', 'value': '👄'}, + {'name': 'thumbsup', 'value': '👍'}, + {'name': 'thumbsdown', 'value': '👎'}, + {'name': 'ok-hand', 'value': '👌'}, + {'name': 'punch', 'value': '👊'}, + {'name': 'fist', 'value': '✊'}, + {'name': 'v', 'value': '✌'}, + {'name': 'wave', 'value': '👋'}, + {'name': 'hand', 'value': '✋'}, + {'name': 'open-hands', 'value': '👐'}, + {'name': 'point-up-2', 'value': '👆'}, + {'name': 'point-down', 'value': '👇'}, + {'name': 'point-right', 'value': '👉'}, + {'name': 'point-left', 'value': '👈'}, + {'name': 'raised-hands', 'value': '🙌'}, + {'name': 'pray', 'value': '🙏'}, + {'name': 'point-up', 'value': '☝'}, + {'name': 'clap', 'value': '👏'}, + {'name': 'muscle', 'value': '💪'}, + {'name': 'walking', 'value': '🚶'}, + {'name': 'runner', 'value': '🏃'}, + {'name': 'dancer', 'value': '💃'}, + {'name': 'couple', 'value': '👫'}, + {'name': 'family', 'value': '👪'}, + {'name': 'two-men-holding-hands', 'value': '👬'}, + {'name': 'two-women-holding-hands', 'value': '👭'}, + {'name': 'couplekiss', 'value': '💏'}, + {'name': 'couple-with-heart', 'value': '💑'}, + {'name': 'dancers', 'value': '👯'}, + {'name': 'ok-woman', 'value': '🙆'}, + {'name': 'no-good', 'value': '🙅'}, + {'name': 'information-desk-person', 'value': '💁'}, + {'name': 'raised-hand', 'value': '🙋'}, + {'name': 'massage', 'value': '💆'}, + {'name': 'haircut', 'value': '💇'}, + {'name': 'nail-care', 'value': '💅'}, + {'name': 'bride-with-veil', 'value': '👰'}, + {'name': 'person-with-pouting-face', 'value': '🙎'}, + {'name': 'person-frowning', 'value': '🙍'}, + {'name': 'bow', 'value': '🙇'}, + {'name': 'tophat', 'value': '🎩'}, + {'name': 'crown', 'value': '👑'}, + {'name': 'womans-hat', 'value': '👒'}, + {'name': 'athletic-shoe', 'value': '👟'}, + {'name': 'mans-shoe', 'value': '👞'}, + {'name': 'sandal', 'value': '👡'}, + {'name': 'high-heel', 'value': '👠'}, + {'name': 'boot', 'value': '👢'}, + {'name': 'shirt', 'value': '👕'}, + {'name': 'necktie', 'value': '👔'}, + {'name': 'womans-clothes', 'value': '👚'}, + {'name': 'dress', 'value': '👗'}, + {'name': 'running-shirt-with-sash', 'value': '🎽'}, + {'name': 'jeans', 'value': '👖'}, + {'name': 'kimono', 'value': '👘'}, + {'name': 'bikini', 'value': '👙'}, + {'name': 'briefcase', 'value': '💼'}, + {'name': 'handbag', 'value': '👜'}, + {'name': 'pouch', 'value': '👝'}, + {'name': 'purse', 'value': '👛'}, + {'name': 'eyeglasses', 'value': '👓'}, + {'name': 'ribbon', 'value': '🎀'}, + {'name': 'closed-umbrella', 'value': '🌂'}, + {'name': 'lipstick', 'value': '💄'}, + {'name': 'yellow-heart', 'value': '💛'}, + {'name': 'blue-heart', 'value': '💙'}, + {'name': 'purple-heart', 'value': '💜'}, + {'name': 'green-heart', 'value': '💚'}, + {'name': 'heart', 'value': '❤'}, + {'name': 'broken-heart', 'value': '💔'}, + {'name': 'heartpulse', 'value': '💗'}, + {'name': 'heartbeat', 'value': '💓'}, + {'name': 'two-hearts', 'value': '💕'}, + {'name': 'sparkling-heart', 'value': '💖'}, + {'name': 'revolving-hearts', 'value': '💞'}, + {'name': 'love-letter', 'value': '💌'}, + {'name': 'cupid', 'value': '💘'}, + {'name': 'kiss', 'value': '💋'}, + {'name': 'ring', 'value': '💍'}, + {'name': 'gem', 'value': '💎'}, + {'name': 'bust-in-silhouette', 'value': '👤'}, + {'name': 'busts-in-silhouette', 'value': '👥'}, + {'name': 'speech-balloon', 'value': '💬'}, + {'name': 'feet', 'value': '👣'}, + {'name': 'thought-balloon', 'value': '💭'} + ], + 'nature': [ + {'name': 'dog', 'value': '🐶'}, + {'name': 'wolf', 'value': '🐺'}, + {'name': 'cat', 'value': '🐱'}, + {'name': 'mouse', 'value': '🐭'}, + {'name': 'hamster', 'value': '🐹'}, + {'name': 'rabbit', 'value': '🐰'}, + {'name': 'frog', 'value': '🐸'}, + {'name': 'tiger', 'value': '🐯'}, + {'name': 'koala', 'value': '🐨'}, + {'name': 'bear', 'value': '🐻'}, + {'name': 'pig', 'value': '🐷'}, + {'name': 'pig-nose', 'value': '🐽'}, + {'name': 'cow', 'value': '🐮'}, + {'name': 'boar', 'value': '🐗'}, + {'name': 'monkey-face', 'value': '🐵'}, + {'name': 'monkey', 'value': '🐒'}, + {'name': 'horse', 'value': '🐴'}, + {'name': 'sheep', 'value': '🐑'}, + {'name': 'elephant', 'value': '🐘'}, + {'name': 'panda-face', 'value': '🐼'}, + {'name': 'penguin', 'value': '🐧'}, + {'name': 'bird', 'value': '🐦'}, + {'name': 'baby-chick', 'value': '🐤'}, + {'name': 'hatched-chick', 'value': '🐥'}, + {'name': 'hatching-chick', 'value': '🐣'}, + {'name': 'chicken', 'value': '🐔'}, + {'name': 'snake', 'value': '🐍'}, + {'name': 'turtle', 'value': '🐢'}, + {'name': 'bug', 'value': '🐛'}, + {'name': 'honeybee', 'value': '🐝'}, + {'name': 'ant', 'value': '🐜'}, + {'name': 'beetle', 'value': '🐞'}, + {'name': 'snail', 'value': '🐌'}, + {'name': 'octopus', 'value': '🐙'}, + {'name': 'shell', 'value': '🐚'}, + {'name': 'tropical-fish', 'value': '🐠'}, + {'name': 'fish', 'value': '🐟'}, + {'name': 'dolphin', 'value': '🐬'}, + {'name': 'whale', 'value': '🐳'}, + {'name': 'whale2', 'value': '🐋'}, + {'name': 'cow2', 'value': '🐄'}, + {'name': 'ram', 'value': '🐏'}, + {'name': 'rat', 'value': '🐀'}, + {'name': 'water-buffalo', 'value': '🐃'}, + {'name': 'tiger2', 'value': '🐅'}, + {'name': 'rabbit2', 'value': '🐇'}, + {'name': 'dragon', 'value': '🐉'}, + {'name': 'racehorse', 'value': '🐎'}, + {'name': 'goat', 'value': '🐐'}, + {'name': 'rooster', 'value': '🐓'}, + {'name': 'dog2', 'value': '🐕'}, + {'name': 'pig2', 'value': '🐖'}, + {'name': 'mouse2', 'value': '🐁'}, + {'name': 'ox', 'value': '🐂'}, + {'name': 'dragon-face', 'value': '🐲'}, + {'name': 'blowfish', 'value': '🐡'}, + {'name': 'crocodile', 'value': '🐊'}, + {'name': 'camel', 'value': '🐫'}, + {'name': 'dromedary-camel', 'value': '🐪'}, + {'name': 'leopard', 'value': '🐆'}, + {'name': 'cat2', 'value': '🐈'}, + {'name': 'poodle', 'value': '🐩'}, + {'name': 'paw-prints', 'value': '🐾'}, + {'name': 'bouquet', 'value': '💐'}, + {'name': 'cherry-blossom', 'value': '🌸'}, + {'name': 'tulip', 'value': '🌷'}, + {'name': 'four-leaf-clover', 'value': '🍀'}, + {'name': 'rose', 'value': '🌹'}, + {'name': 'sunflower', 'value': '🌻'}, + {'name': 'hibiscus', 'value': '🌺'}, + {'name': 'maple-leaf', 'value': '🍁'}, + {'name': 'leaves', 'value': '🍃'}, + {'name': 'fallen-leaf', 'value': '🍂'}, + {'name': 'herb', 'value': '🌿'}, + {'name': 'ear-of-rice', 'value': '🌾'}, + {'name': 'mushroom', 'value': '🍄'}, + {'name': 'cactus', 'value': '🌵'}, + {'name': 'palm-tree', 'value': '🌴'}, + {'name': 'evergreen-tree', 'value': '🌲'}, + {'name': 'deciduous-tree', 'value': '🌳'}, + {'name': 'chestnut', 'value': '🌰'}, + {'name': 'seedling', 'value': '🌱'}, + {'name': 'blossom', 'value': '🌼'}, + {'name': 'globe-with-meridians', 'value': '🌐'}, + {'name': 'sun-with-face', 'value': '🌞'}, + {'name': 'full-moon-with-face', 'value': '🌝'}, + {'name': 'new-moon-with-face', 'value': '🌚'}, + {'name': 'new-moon', 'value': '🌑'}, + {'name': 'waxing-crescent-moon', 'value': '🌒'}, + {'name': 'first-quarter-moon', 'value': '🌓'}, + {'name': 'waxing-gibbous-moon', 'value': '🌔'}, + {'name': 'full-moon', 'value': '🌕'}, + {'name': 'waning-gibbous-moon', 'value': '🌖'}, + {'name': 'last-quarter-moon', 'value': '🌗'}, + {'name': 'waning-crescent-moon', 'value': '🌘'}, + {'name': 'last-quarter-moon-with-face', 'value': '🌜'}, + {'name': 'first-quarter-moon-with-face', 'value': '🌛'}, + {'name': 'moon', 'value': '🌙'}, + {'name': 'earth-africa', 'value': '🌍'}, + {'name': 'earth-americas', 'value': '🌎'}, + {'name': 'earth-asia', 'value': '🌏'}, + {'name': 'volcano', 'value': '🌋'}, + {'name': 'milky-way', 'value': '🌌'}, + {'name': 'shooting-star', 'value': '🌠'}, + {'name': 'star', 'value': '⭐'}, + {'name': 'sunny', 'value': '☀'}, + {'name': 'partly-sunny', 'value': '⛅'}, + {'name': 'cloud', 'value': '☁'}, + {'name': 'zap', 'value': '⚡'}, + {'name': 'umbrella', 'value': '☔'}, + {'name': 'snowflake', 'value': '❄'}, + {'name': 'snowman', 'value': '⛄'}, + {'name': 'cyclone', 'value': '🌀'}, + {'name': 'foggy', 'value': '🌁'}, + {'name': 'rainbow', 'value': '🌈'}, + {'name': 'ocean', 'value': '🌊'} + ], + 'object': [ + {'name': 'bamboo', 'value': '🎍'}, + {'name': 'gift-heart', 'value': '💝'}, + {'name': 'dolls', 'value': '🎎'}, + {'name': 'school-satchel', 'value': '🎒'}, + {'name': 'mortar-board', 'value': '🎓'}, + {'name': 'flags', 'value': '🎏'}, + {'name': 'fireworks', 'value': '🎆'}, + {'name': 'sparkler', 'value': '🎇'}, + {'name': 'wind-chime', 'value': '🎐'}, + {'name': 'rice-scene', 'value': '🎑'}, + {'name': 'jack-o-lantern', 'value': '🎃'}, + {'name': 'ghost', 'value': '👻'}, + {'name': 'santa', 'value': '🎅'}, + {'name': 'christmas-tree', 'value': '🎄'}, + {'name': 'gift', 'value': '🎁'}, + {'name': 'tanabata-tree', 'value': '🎋'}, + {'name': 'tada', 'value': '🎉'}, + {'name': 'confetti-ball', 'value': '🎊'}, + {'name': 'balloon', 'value': '🎈'}, + {'name': 'crossed-flags', 'value': '🎌'}, + {'name': 'crystal-ball', 'value': '🔮'}, + {'name': 'movie-camera', 'value': '🎥'}, + {'name': 'camera', 'value': '📷'}, + {'name': 'video-camera', 'value': '📹'}, + {'name': 'vhs', 'value': '📼'}, + {'name': 'cd', 'value': '💿'}, + {'name': 'dvd', 'value': '📀'}, + {'name': 'minidisc', 'value': '💽'}, + {'name': 'floppy-disk', 'value': '💾'}, + {'name': 'computer', 'value': '💻'}, + {'name': 'iphone', 'value': '📱'}, + {'name': 'phone', 'value': '☎'}, + {'name': 'telephone-receiver', 'value': '📞'}, + {'name': 'pager', 'value': '📟'}, + {'name': 'fax', 'value': '📠'}, + {'name': 'satellite', 'value': '📡'}, + {'name': 'tv', 'value': '📺'}, + {'name': 'radio', 'value': '📻'}, + {'name': 'speaker-waves', 'value': '🔊'}, + {'name': 'sound', 'value': '🔉'}, + {'name': 'speaker', 'value': '🔈'}, + {'name': 'mute', 'value': '🔇'}, + {'name': 'bell', 'value': '🔔'}, + {'name': 'no-bell', 'value': '🔕'}, + {'name': 'loudspeaker', 'value': '📢'}, + {'name': 'mega', 'value': '📣'}, + {'name': 'hourglass-flowing-sand', 'value': '⏳'}, + {'name': 'hourglass', 'value': '⌛'}, + {'name': 'alarm-clock', 'value': '⏰'}, + {'name': 'watch', 'value': '⌚'}, + {'name': 'unlock', 'value': '🔓'}, + {'name': 'lock', 'value': '🔒'}, + {'name': 'lock-with-ink-pen', 'value': '🔏'}, + {'name': 'closed-lock-with-key', 'value': '🔐'}, + {'name': 'key', 'value': '🔑'}, + {'name': 'mag-right', 'value': '🔎'}, + {'name': 'bulb', 'value': '💡'}, + {'name': 'flashlight', 'value': '🔦'}, + {'name': 'high-brightness', 'value': '🔆'}, + {'name': 'low-brightness', 'value': '🔅'}, + {'name': 'electric-plug', 'value': '🔌'}, + {'name': 'battery', 'value': '🔋'}, + {'name': 'mag', 'value': '🔍'}, + {'name': 'bathtub', 'value': '🛁'}, + {'name': 'bath', 'value': '🛀'}, + {'name': 'shower', 'value': '🚿'}, + {'name': 'toilet', 'value': '🚽'}, + {'name': 'wrench', 'value': '🔧'}, + {'name': 'nut-and-bolt', 'value': '🔩'}, + {'name': 'hammer', 'value': '🔨'}, + {'name': 'door', 'value': '🚪'}, + {'name': 'smoking', 'value': '🚬'}, + {'name': 'bomb', 'value': '💣'}, + {'name': 'gun', 'value': '🔫'}, + {'name': 'hocho', 'value': '🔪'}, + {'name': 'pill', 'value': '💊'}, + {'name': 'syringe', 'value': '💉'}, + {'name': 'moneybag', 'value': '💰'}, + {'name': 'yen', 'value': '💴'}, + {'name': 'dollar', 'value': '💵'}, + {'name': 'pound', 'value': '💷'}, + {'name': 'euro', 'value': '💶'}, + {'name': 'credit-card', 'value': '💳'}, + {'name': 'money-with-wings', 'value': '💸'}, + {'name': 'calling', 'value': '📲'}, + {'name': 'e-mail', 'value': '📧'}, + {'name': 'inbox-tray', 'value': '📥'}, + {'name': 'outbox-tray', 'value': '📤'}, + {'name': 'email', 'value': '✉'}, + {'name': 'enveloppe', 'value': '📩'}, + {'name': 'incoming-envelope', 'value': '📨'}, + {'name': 'postal-horn', 'value': '📯'}, + {'name': 'mailbox', 'value': '📫'}, + {'name': 'mailbox-closed', 'value': '📪'}, + {'name': 'mailbox-with-mail', 'value': '📬'}, + {'name': 'mailbox-with-no-mail', 'value': '📭'}, + {'name': 'postbox', 'value': '📮'}, + {'name': 'package', 'value': '📦'}, + {'name': 'memo', 'value': '📝'}, + {'name': 'page-facing-up', 'value': '📄'}, + {'name': 'page-with-curl', 'value': '📃'}, + {'name': 'bookmark-tabs', 'value': '📑'}, + {'name': 'bar-chart', 'value': '📊'}, + {'name': 'chart-with-upwards-trend', 'value': '📈'}, + {'name': 'chart-with-downwards-trend', 'value': '📉'}, + {'name': 'scroll', 'value': '📜'}, + {'name': 'clipboard', 'value': '📋'}, + {'name': 'date', 'value': '📅'}, + {'name': 'calendar', 'value': '📆'}, + {'name': 'card-index', 'value': '📇'}, + {'name': 'file-folder', 'value': '📁'}, + {'name': 'open-file-folder', 'value': '📂'}, + {'name': 'scissors', 'value': '✂'}, + {'name': 'pushpin', 'value': '📌'}, + {'name': 'paperclip', 'value': '📎'}, + {'name': 'black-nib', 'value': '✒'}, + {'name': 'pencil2', 'value': '✏'}, + {'name': 'straight-ruler', 'value': '📏'}, + {'name': 'triangular-ruler', 'value': '📐'}, + {'name': 'closed-book', 'value': '📕'}, + {'name': 'green-book', 'value': '📗'}, + {'name': 'blue-book', 'value': '📘'}, + {'name': 'orange-book', 'value': '📙'}, + {'name': 'notebook', 'value': '📓'}, + {'name': 'notebook-with-decorative-cover', 'value': '📔'}, + {'name': 'ledger', 'value': '📒'}, + {'name': 'books', 'value': '📚'}, + {'name': 'open-book', 'value': '📖'}, + {'name': 'bookmark', 'value': '🔖'}, + {'name': 'name-badge', 'value': '📛'}, + {'name': 'microscope', 'value': '🔬'}, + {'name': 'telescope', 'value': '🔭'}, + {'name': 'newspaper', 'value': '📰'}, + {'name': 'art', 'value': '🎨'}, + {'name': 'clapper', 'value': '🎬'}, + {'name': 'microphone', 'value': '🎤'}, + {'name': 'headphones', 'value': '🎧'}, + {'name': 'musical-score', 'value': '🎼'}, + {'name': 'musical-note', 'value': '🎵'}, + {'name': 'notes', 'value': '🎶'}, + {'name': 'musical-keyboard', 'value': '🎹'}, + {'name': 'violin', 'value': '🎻'}, + {'name': 'trumpet', 'value': '🎺'}, + {'name': 'saxophone', 'value': '🎷'}, + {'name': 'guitar', 'value': '🎸'}, + {'name': 'space-invader', 'value': '👾'}, + {'name': 'video-game', 'value': '🎮'}, + {'name': 'black-joker', 'value': '🃏'}, + {'name': 'flower-playing-cards', 'value': '🎴'}, + {'name': 'mahjong', 'value': '🀄'}, + {'name': 'game-die', 'value': '🎲'}, + {'name': 'dart', 'value': '🎯'}, + {'name': 'football', 'value': '🏈'}, + {'name': 'basketball', 'value': '🏀'}, + {'name': 'soccer', 'value': '⚽'}, + {'name': 'baseball', 'value': '⚾'}, + {'name': 'tennis', 'value': '🎾'}, + {'name': '8ball', 'value': '🎱'}, + {'name': 'rugby-football', 'value': '🏉'}, + {'name': 'bowling', 'value': '🎳'}, + {'name': 'golf', 'value': '⛳'}, + {'name': 'mountain-bicyclist', 'value': '🚵'}, + {'name': 'bicyclist', 'value': '🚴'}, + {'name': 'checkered-flag', 'value': '🏁'}, + {'name': 'horse-racing', 'value': '🏇'}, + {'name': 'trophy', 'value': '🏆'}, + {'name': 'ski', 'value': '🎿'}, + {'name': 'snowboarder', 'value': '🏂'}, + {'name': 'swimmer', 'value': '🏊'}, + {'name': 'surfer', 'value': '🏄'}, + {'name': 'fishing-pole-and-fish', 'value': '🎣'}, + {'name': 'coffee', 'value': '☕'}, + {'name': 'tea', 'value': '🍵'}, + {'name': 'sake', 'value': '🍶'}, + {'name': 'baby-bottle', 'value': '🍼'}, + {'name': 'beer', 'value': '🍺'}, + {'name': 'beers', 'value': '🍻'}, + {'name': 'cocktail', 'value': '🍸'}, + {'name': 'tropical-drink', 'value': '🍹'}, + {'name': 'wine-glass', 'value': '🍷'}, + {'name': 'fork-and-knife', 'value': '🍴'}, + {'name': 'pizza', 'value': '🍕'}, + {'name': 'hamburger', 'value': '🍔'}, + {'name': 'fries', 'value': '🍟'}, + {'name': 'poultry-leg', 'value': '🍗'}, + {'name': 'meat-on-bone', 'value': '🍖'}, + {'name': 'spaghetti', 'value': '🍝'}, + {'name': 'curry', 'value': '🍛'}, + {'name': 'fried-shrimp', 'value': '🍤'}, + {'name': 'bento', 'value': '🍱'}, + {'name': 'sushi', 'value': '🍣'}, + {'name': 'fish-cake', 'value': '🍥'}, + {'name': 'rice-ball', 'value': '🍙'}, + {'name': 'rice-cracker', 'value': '🍘'}, + {'name': 'rice', 'value': '🍚'}, + {'name': 'ramen', 'value': '🍜'}, + {'name': 'stew', 'value': '🍲'}, + {'name': 'oden', 'value': '🍢'}, + {'name': 'dango', 'value': '🍡'}, + {'name': 'egg', 'value': '🍳'}, + {'name': 'bread', 'value': '🍞'}, + {'name': 'doughnut', 'value': '🍩'}, + {'name': 'custard', 'value': '🍮'}, + {'name': 'icecream', 'value': '🍦'}, + {'name': 'ice-cream', 'value': '🍨'}, + {'name': 'shaved-ice', 'value': '🍧'}, + {'name': 'birthday', 'value': '🎂'}, + {'name': 'cake', 'value': '🍰'}, + {'name': 'cookie', 'value': '🍪'}, + {'name': 'chocolate-bar', 'value': '🍫'}, + {'name': 'candy', 'value': '🍬'}, + {'name': 'lollipop', 'value': '🍭'}, + {'name': 'honey-pot', 'value': '🍯'}, + {'name': 'apple', 'value': '🍎'}, + {'name': 'green-apple', 'value': '🍏'}, + {'name': 'tangerine', 'value': '🍊'}, + {'name': 'lemon', 'value': '🍋'}, + {'name': 'cherries', 'value': '🍒'}, + {'name': 'grapes', 'value': '🍇'}, + {'name': 'watermelon', 'value': '🍉'}, + {'name': 'strawberry', 'value': '🍓'}, + {'name': 'peach', 'value': '🍑'}, + {'name': 'melon', 'value': '🍈'}, + {'name': 'banana', 'value': '🍌'}, + {'name': 'pear', 'value': '🍐'}, + {'name': 'pineapple', 'value': '🍍'}, + {'name': 'sweet-potato', 'value': '🍠'}, + {'name': 'eggplant', 'value': '🍆'}, + {'name': 'tomato', 'value': '🍅'}, + {'name': 'corn', 'value': '🌽'} + ], + 'place': [ + {'name': 'house', 'value': '🏠'}, + {'name': 'house-with-garden', 'value': '🏡'}, + {'name': 'school', 'value': '🏫'}, + {'name': 'office', 'value': '🏢'}, + {'name': 'post-office', 'value': '🏣'}, + {'name': 'hospital', 'value': '🏥'}, + {'name': 'bank', 'value': '🏦'}, + {'name': 'convenience-store', 'value': '🏪'}, + {'name': 'love-hotel', 'value': '🏩'}, + {'name': 'hotel', 'value': '🏨'}, + {'name': 'wedding', 'value': '💒'}, + {'name': 'church', 'value': '⛪'}, + {'name': 'department-store', 'value': '🏬'}, + {'name': 'european-post-office', 'value': '🏤'}, + {'name': 'private-use', 'value': ''}, + {'name': 'city-sunrise', 'value': '🌇'}, + {'name': 'city-sunset', 'value': '🌆'}, + {'name': 'japanese-castle', 'value': '🏯'}, + {'name': 'european-castle', 'value': '🏰'}, + {'name': 'tent', 'value': '⛺'}, + {'name': 'factory', 'value': '🏭'}, + {'name': 'tokyo-tower', 'value': '🗼'}, + {'name': 'japan', 'value': '🗾'}, + {'name': 'mount-fuji', 'value': '🗻'}, + {'name': 'sunrise-over-mountains', 'value': '🌄'}, + {'name': 'sunrise', 'value': '🌅'}, + {'name': 'stars', 'value': '🌃'}, + {'name': 'statue-of-liberty', 'value': '🗽'}, + {'name': 'bridge-at-night', 'value': '🌉'}, + {'name': 'carousel-horse', 'value': '🎠'}, + {'name': 'ferris-wheel', 'value': '🎡'}, + {'name': 'fountain', 'value': '⛲'}, + {'name': 'roller-coaster', 'value': '🎢'}, + {'name': 'ship', 'value': '🚢'}, + {'name': 'boat', 'value': '⛵'}, + {'name': 'speedboat', 'value': '🚤'}, + {'name': 'rowboat', 'value': '🚣'}, + {'name': 'anchor', 'value': '⚓'}, + {'name': 'rocket', 'value': '🚀'}, + {'name': 'airplane', 'value': '✈'}, + {'name': 'seat', 'value': '💺'}, + {'name': 'helicopter', 'value': '🚁'}, + {'name': 'steam-locomotive', 'value': '🚂'}, + {'name': 'tram', 'value': '🚊'}, + {'name': 'station', 'value': '🚉'}, + {'name': 'mountain-railway', 'value': '🚞'}, + {'name': 'train2', 'value': '🚆'}, + {'name': 'bullettrain-side', 'value': '🚄'}, + {'name': 'bullettrain-front', 'value': '🚅'}, + {'name': 'light-rail', 'value': '🚈'}, + {'name': 'metro', 'value': '🚇'}, + {'name': 'monorail', 'value': '🚝'}, + {'name': 'tram-car', 'value': '🚋'}, + {'name': 'railway-car', 'value': '🚃'}, + {'name': 'trolleybus', 'value': '🚎'}, + {'name': 'bus', 'value': '🚌'}, + {'name': 'oncoming-bus', 'value': '🚍'}, + {'name': 'blue-car', 'value': '🚙'}, + {'name': 'oncoming-automobile', 'value': '🚘'}, + {'name': 'car', 'value': '🚗'}, + {'name': 'taxi', 'value': '🚕'}, + {'name': 'oncoming-taxi', 'value': '🚖'}, + {'name': 'articulated-lorry', 'value': '🚛'}, + {'name': 'truck', 'value': '🚚'}, + {'name': 'rotating-light', 'value': '🚨'}, + {'name': 'police-car', 'value': '🚓'}, + {'name': 'oncoming-police-car', 'value': '🚔'}, + {'name': 'fire-engine', 'value': '🚒'}, + {'name': 'ambulance', 'value': '🚑'}, + {'name': 'minibus', 'value': '🚐'}, + {'name': 'bike', 'value': '🚲'}, + {'name': 'aerial-tramway', 'value': '🚡'}, + {'name': 'suspension-railway', 'value': '🚟'}, + {'name': 'mountain-cableway', 'value': '🚠'}, + {'name': 'tractor', 'value': '🚜'}, + {'name': 'barber', 'value': '💈'}, + {'name': 'busstop', 'value': '🚏'}, + {'name': 'ticket', 'value': '🎫'}, + {'name': 'vertical-traffic-light', 'value': '🚦'}, + {'name': 'traffic-light', 'value': '🚥'}, + {'name': 'warning', 'value': '⚠'}, + {'name': 'construction', 'value': '🚧'}, + {'name': 'beginner', 'value': '🔰'}, + {'name': 'fuelpump', 'value': '⛽'}, + {'name': 'izakaya-lantern', 'value': '🏮'}, + {'name': 'slot-machine', 'value': '🎰'}, + {'name': 'hotsprings', 'value': '♨'}, + {'name': 'moyai', 'value': '🗿'}, + {'name': 'circus-tent', 'value': '🎪'}, + {'name': 'performing-arts', 'value': '🎭'}, + {'name': 'round-pushpin', 'value': '📍'}, + {'name': 'triangular-flag-on-post', 'value': '🚩'}, + {'name': 'cn', 'value': '🇨🇳'}, + {'name': 'de', 'value': '🇩🇪'}, + {'name': 'es', 'value': '🇪🇸'}, + {'name': 'fr', 'value': '🇫🇷'}, + {'name': 'gb', 'value': '🇬🇧'}, + {'name': 'it', 'value': '🇮🇹'}, + {'name': 'jp', 'value': '🇯🇵'}, + {'name': 'kr', 'value': '🇰🇷'}, + {'name': 'ru', 'value': '🇷🇺'}, + {'name': 'us', 'value': '🇺🇸'} + ] + }; + + /* preprocess */ + /* ~10ms in total, each section about 2ms so we're not really blocking the side totally */ + window.setup_lsx_emoji_picker = options => { + setup_options = options; + + if(options.twemoji) { + const generator = function*() { + for(const category_name of Object.keys(emoji)) { + for(const entry of emoji[category_name]) { + entry["str"] = entry.value.replace(/&#x([0-9a-f]{1,6});?/g, (match, $1) => { + return twemoji.convert.fromCodePoint($1); + }); + + entry["img"] = twemoji.parse(entry.str, { + folder: 'svg', + ext: '.svg' + }); + } + yield category_name; + } + }(); + + return new Promise(resolve => { + const _loop = () => { + if(generator.next().done) + resolve(); + else + setTimeout(_loop, 0); + }; + _loop(); + }); + } + return Promise.resolve(); + }; + + $.fn.lsxEmojiPicker = function (options) { + if(typeof(setup_options) === "undefined") + throw "lsx emoji picker hasn't been initialized"; + // Overriding default options + let settings = $.extend({ + width: 220, + height: 200, + twemoji: false, + closeOnSelect: true, + onSelect: function(em){} + }, options); + + if(settings.twemoji && !setup_options.twemoji) + throw "lsx emoji picker hasn't been initialized with twenmoji support" + + var appender = $('
') + .addClass('lsx-emojipicker-appender'); + var container = $('
') + .addClass('lsx-emojipicker-container') + .css({ + 'top': "-" + (settings.height + 37 + 15) + "px" //37 is the bottom and 15 the select thing + }); + var wrapper = $('
') + .addClass('lsx-emojipicker-wrapper'); + + var spinnerContainer = $('
') + .addClass('spinner-container'); + var spinner = $('
') + .addClass('loader'); + spinnerContainer.append(spinner); + + var tabs = $('
    ') + .addClass('lsx-emojipicker-tabs'); + + const create_category_li = (icon, name, selected) => $(document.createElement("li")) + .html(settings.twemoji ? icon.img : icon.value) + .click(event => { + event.preventDefault(); + + tabs.find("li.selected").removeClass("selected"); + $(event.target).parent("li").addClass("selected"); + + wrapper.find("> .lsx-emoji-tab").addClass("hidden"); + wrapper.find("> .lsx-emoji-tab.lsx-emoji-" + name).removeClass("hidden"); + }).toggleClass("selected", selected); + + tabs.append(create_category_li(emoji['people'][1], "people", true)) + .append(create_category_li(emoji['nature'][0], "nature", false)) + .append(create_category_li(emoji['place'][38], "place", false)) + .append(create_category_li(emoji['object'][4], "object", false)); + + + (async () => { + const _tab = (name: string, hidden: boolean) => { + let tab_html = '
    '; + + if(settings.twemoji) { + for(const e of emoji[name]) + tab_html += e.img; + } else { + for(const e of emoji[name]) + tab_html += "" + e.value + ""; + } + + return tab_html + "
    "; + }; + + //wrapper.append(spinnerContainer); + wrapper[0].innerHTML = + _tab("people", false) + + _tab("nature", true) + + _tab("place", true) + + _tab("object", true); + + wrapper.find("img, span").on('click', event => { + const target = $(event.target); + + settings.onSelect({ + name: target.attr("title"), + value: target.attr("alt") || target.attr("value") + }); + if(settings.closeOnSelect){ + wrapper.hide(); + } + }); + wrapper.append(tabs); + container.append(wrapper); + appender.append(container); + })(); + this.append(appender); + + this.click(e => { + if(this.hasClass("disabled")) + return; + + e.preventDefault(); + + if(!$(e.target).parent().hasClass('lsx-emojipicker-tabs') + && !$(e.target).parent().parent().hasClass('lsx-emojipicker-tabs') + && !$(e.target).parent().hasClass('lsx-emoji-tab') + && !$(e.target).parent().parent().hasClass('lsx-emoji-tab')){ + if(container.is(':visible')){ + container.hide(); + } else { + container.fadeIn(); + } + } + }); + + return this; + }; + }(jQuery, window)); +} diff --git a/vendor/emoji-picker/webpack.config.js b/vendor/emoji-picker/webpack.config.js new file mode 100644 index 00000000..36b884c4 --- /dev/null +++ b/vendor/emoji-picker/webpack.config.js @@ -0,0 +1,57 @@ +//Webpack configuration file + +var webpack = require('webpack'); +var path = require('path'); +var fs = require("fs"); + +var plugins = [], outputFile; +var APP_NAME = 'jquery.lsxemojipicker'; + +outputFile = APP_NAME + '.min.js'; + +module.exports = (env) => { + if(false && env === 'production'){ + console.log('Production mode enabled'); + plugins.push(new webpack.optimize.UglifyJsPlugin({ + beautify: false, + comments: false, + sourceMap: false, + compress: { + screw_ie8: true, + warnings: false + }, + mangle: { + keep_fnames: true, + screw_i8: true + } + })); + } else { + console.log('Development mode enabled'); + } + //Banner plugin for license + plugins.push(new webpack.BannerPlugin(fs.readFileSync('./LICENSE', 'utf8'))); + plugins.push(new webpack.DefinePlugin({ + PRODUCTION_MODE: env === 'production' + })); + + return { + entry: [ + __dirname + '/src/jquery.lsxemojipicker.css', + __dirname + '/src/jquery.lsxemojipicker.js' + ], + devtool: 'source-map', + output: { + path: __dirname, + filename: outputFile, + library: APP_NAME, + libraryTarget: 'umd', + umdNamedDefine: true + }, + module: { + loaders: [ + { test: /\.s?css/, loader: "style-loader!css-loader" } + ] + }, + plugins: plugins + }; +} diff --git a/vendor/emoji-picker/yarn.lock b/vendor/emoji-picker/yarn.lock new file mode 100644 index 00000000..edcff7c6 --- /dev/null +++ b/vendor/emoji-picker/yarn.lock @@ -0,0 +1,3002 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822" + +ajv-keywords@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.0.0, ajv@^5.1.5: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +asn1.js@^4.0.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async@^2.1.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +autoprefixer@^6.3.1: + version "6.7.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" + dependencies: + browserslist "^1.7.6" + caniuse-db "^1.0.30000634" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^5.2.16" + postcss-value-parser "^3.2.3" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + +binary-extensions@^1.0.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^3.5.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + dependencies: + pako "~1.0.5" + +browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" + dependencies: + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +cacache@^10.0.0: + version "10.0.2" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.2.tgz#105a93a162bbedf3a25da42e1939ed99ffb145f8" + dependencies: + bluebird "^3.5.0" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^1.3.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.1" + ssri "^5.0.0" + unique-filename "^1.1.0" + y18n "^3.2.1" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +caniuse-api@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" + dependencies: + browserslist "^1.3.6" + caniuse-db "^1.0.30000529" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: + version "1.0.30000793" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000793.tgz#3c00c66e423a7a1907c7dd96769a78b2afa8a72e" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clap@^1.0.9: + version "1.2.3" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" + dependencies: + chalk "^1.1.3" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +coa@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" + dependencies: + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.3.0, color-convert@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-name@^1.0.0, color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + dependencies: + color-name "^1.0.0" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +colormin@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" + dependencies: + color "^0.11.0" + css-color-names "0.0.4" + has "^1.0.1" + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@~2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-color-names@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + +css-loader@^0.28.2: + version "0.28.8" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.8.tgz#ff36381464dea18fe60f2601a060ba6445886bd5" + dependencies: + babel-code-frame "^6.26.0" + css-selector-tokenizer "^0.7.0" + cssnano "^3.10.0" + icss-utils "^2.1.0" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + object-assign "^4.1.1" + postcss "^5.0.6" + postcss-modules-extract-imports "^1.1.0" + postcss-modules-local-by-default "^1.2.0" + postcss-modules-scope "^1.1.0" + postcss-modules-values "^1.3.0" + postcss-value-parser "^3.3.0" + source-list-map "^2.0.0" + +css-selector-tokenizer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" + dependencies: + cssesc "^0.1.0" + fastparse "^1.1.1" + regexpu-core "^1.0.0" + +cssesc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" + +cssnano@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" + dependencies: + autoprefixer "^6.3.1" + decamelize "^1.1.2" + defined "^1.0.0" + has "^1.0.1" + object-assign "^4.0.1" + postcss "^5.0.14" + postcss-calc "^5.2.0" + postcss-colormin "^2.1.8" + postcss-convert-values "^2.3.4" + postcss-discard-comments "^2.0.4" + postcss-discard-duplicates "^2.0.1" + postcss-discard-empty "^2.0.1" + postcss-discard-overridden "^0.1.1" + postcss-discard-unused "^2.2.1" + postcss-filter-plugins "^2.0.0" + postcss-merge-idents "^2.1.5" + postcss-merge-longhand "^2.0.1" + postcss-merge-rules "^2.0.3" + postcss-minify-font-values "^1.0.2" + postcss-minify-gradients "^1.0.1" + postcss-minify-params "^1.0.4" + postcss-minify-selectors "^2.0.4" + postcss-normalize-charset "^1.1.0" + postcss-normalize-url "^3.0.7" + postcss-ordered-values "^2.1.0" + postcss-reduce-idents "^2.2.2" + postcss-reduce-initial "^1.0.0" + postcss-reduce-transforms "^1.0.3" + postcss-svgo "^2.1.1" + postcss-unique-selectors "^2.0.2" + postcss-value-parser "^3.2.3" + postcss-zindex "^2.0.1" + +csso@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" + dependencies: + clap "^1.0.9" + source-map "^0.5.3" + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +domain-browser@^1.1.1: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +duplexify@^3.4.2, duplexify@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +electron-releases@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/electron-releases/-/electron-releases-2.1.0.tgz#c5614bf811f176ce3c836e368a0625782341fd4e" + +electron-to-chromium@^1.2.7: + version "1.3.30" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.30.tgz#9666f532a64586651fc56a72513692e820d06a80" + dependencies: + electron-releases "^2.1.0" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.7" + +errno@^0.1.3, errno@^0.1.4: + version "0.1.6" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026" + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.38" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.38.tgz#fa7d40d65bbc9bb8a67e1d3f9cc656a00530eed3" + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esprima@^2.6.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +extend@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fastparse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +flush-write-stream@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.39" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@^7.0.5, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hawk@3.1.3, hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +html-comment-regex@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + +icss-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" + dependencies: + postcss "^6.0.1" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +interpret@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-svg@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" + dependencies: + html-comment-regex "^1.1.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +js-base64@^2.1.9: + version "2.4.1" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.1.tgz#e02813181cd53002888e918935467acb2910e596" + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@~3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-loader@^0.5.4: + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@^4.14.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +lru-cache@^4.0.1, lru-cache@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +macaddress@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" + +make-dir@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" + dependencies: + pify "^3.0.0" + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +micromatch@^2.1.5: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + +mime-types@^2.1.12, mime-types@~2.1.7: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mississippi@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.0.tgz#d201583eb12327e3c5c1642a404a9cacf94e34f5" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^1.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +nan@^2.3.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" + +node-libs-browser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.0" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-pre-gyp@^0.6.39: + version "0.6.39" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" + dependencies: + detect-libc "^1.0.2" + hawk "3.1.3" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.0, normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +normalize-url@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +pako@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.14" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + +postcss-calc@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + dependencies: + postcss "^5.0.2" + postcss-message-helpers "^2.0.0" + reduce-css-calc "^1.2.6" + +postcss-colormin@^2.1.8: + version "2.2.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" + dependencies: + colormin "^1.0.5" + postcss "^5.0.13" + postcss-value-parser "^3.2.3" + +postcss-convert-values@^2.3.4: + version "2.6.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" + dependencies: + postcss "^5.0.11" + postcss-value-parser "^3.1.2" + +postcss-discard-comments@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" + dependencies: + postcss "^5.0.14" + +postcss-discard-duplicates@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" + dependencies: + postcss "^5.0.4" + +postcss-discard-empty@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" + dependencies: + postcss "^5.0.14" + +postcss-discard-overridden@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" + dependencies: + postcss "^5.0.16" + +postcss-discard-unused@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" + dependencies: + postcss "^5.0.14" + uniqs "^2.0.0" + +postcss-filter-plugins@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" + dependencies: + postcss "^5.0.4" + uniqid "^4.0.0" + +postcss-merge-idents@^2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" + dependencies: + has "^1.0.1" + postcss "^5.0.10" + postcss-value-parser "^3.1.1" + +postcss-merge-longhand@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" + dependencies: + postcss "^5.0.4" + +postcss-merge-rules@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" + dependencies: + browserslist "^1.5.2" + caniuse-api "^1.5.2" + postcss "^5.0.4" + postcss-selector-parser "^2.2.2" + vendors "^1.0.0" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + +postcss-minify-font-values@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-minify-gradients@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" + dependencies: + postcss "^5.0.12" + postcss-value-parser "^3.3.0" + +postcss-minify-params@^1.0.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.2" + postcss-value-parser "^3.0.2" + uniqs "^2.0.0" + +postcss-minify-selectors@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" + dependencies: + alphanum-sort "^1.0.2" + has "^1.0.1" + postcss "^5.0.14" + postcss-selector-parser "^2.0.0" + +postcss-modules-extract-imports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz#b614c9720be6816eaee35fb3a5faa1dba6a05ddb" + dependencies: + postcss "^6.0.1" + +postcss-modules-local-by-default@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-scope@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-values@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" + +postcss-normalize-charset@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" + dependencies: + postcss "^5.0.5" + +postcss-normalize-url@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^1.4.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + +postcss-ordered-values@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.1" + +postcss-reduce-idents@^2.2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-reduce-initial@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" + dependencies: + postcss "^5.0.4" + +postcss-reduce-transforms@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" + dependencies: + has "^1.0.1" + postcss "^5.0.8" + postcss-value-parser "^3.0.1" + +postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^2.1.1: + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" + dependencies: + is-svg "^2.0.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + svgo "^0.7.0" + +postcss-unique-selectors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" + +postcss-zindex@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" + dependencies: + has "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0.1: + version "6.0.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.16.tgz#112e2fe2a6d2109be0957687243170ea5589e146" + dependencies: + chalk "^2.3.0" + source-map "^0.6.1" + supports-color "^5.1.0" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +pump@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.0.tgz#7946da1c8d622b098e2ceb2d3476582470829c9d" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" + dependencies: + duplexify "^3.5.3" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +rc@^1.1.7: + version "1.2.4" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.4.tgz#a0f606caae2a3b862bbd0ef85482c0125b315fa3" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +reduce-css-calc@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + dependencies: + balanced-match "^0.4.2" + +regenerate@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +regexpu-core@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +request@2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sax@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.9" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +ssri@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.0.0.tgz#13c19390b606c821f2a10d02b351c1729b94d8cf" + dependencies: + safe-buffer "^5.1.0" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.3" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.0.0, string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +style-loader@^0.18.0: + version "0.18.2" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.18.2.tgz#cc31459afbcd6d80b7220ee54b291a9fd66ff5eb" + dependencies: + loader-utils "^1.0.2" + schema-utils "^0.3.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^4.0.0, supports-color@^4.2.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +supports-color@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5" + dependencies: + has-flag "^2.0.0" + +svgo@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" + dependencies: + coa "~1.0.1" + colors "~1.1.2" + csso "~2.3.1" + js-yaml "~3.7.0" + mkdirp "~0.5.1" + sax "~1.2.1" + whet.extend "~0.9.9" + +tapable@^0.2.7: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" + +tar-pack@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +timers-browserify@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" + dependencies: + setimmediate "^1.0.4" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +tough-cookie@~2.3.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + dependencies: + punycode "^1.4.1" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-es@^3.2.0: + version "3.3.7" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.7.tgz#d1249af668666aba7cb1163e277455be9eb393cf" + dependencies: + commander "~2.13.0" + source-map "~0.6.1" + +uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyjs-webpack-plugin@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.1.2.tgz#8a9abc238d01a33daaf86fa9a84c7ebc1e67b0f9" + dependencies: + cacache "^10.0.0" + find-cache-dir "^1.0.0" + schema-utils "^0.3.0" + source-map "^0.6.1" + uglify-es "^3.2.0" + webpack-sources "^1.0.1" + worker-farm "^1.4.1" + +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +uniqid@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" + dependencies: + macaddress "^0.2.8" + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + +unique-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +uuid@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +vendors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +watchpack@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + dependencies: + async "^2.1.2" + chokidar "^1.7.0" + graceful-fs "^4.1.2" + +webpack-sources@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.10.0.tgz#5291b875078cf2abf42bdd23afe3f8f96c17d725" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^5.1.5" + ajv-keywords "^2.0.0" + async "^2.1.2" + enhanced-resolve "^3.4.0" + escope "^3.6.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" + webpack-sources "^1.0.1" + yargs "^8.0.2" + +whet.extend@~0.9.9: + version "0.9.9" + resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +worker-farm@^1.4.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" diff --git a/vendor/highlight/CHANGES.md b/vendor/highlight/CHANGES.md new file mode 100644 index 00000000..453e1d1d --- /dev/null +++ b/vendor/highlight/CHANGES.md @@ -0,0 +1,1896 @@ +## Master + +New languages: + +New styles: + +Improvements: + +## Version 9.15.9 + +Improvements: + - fix(AutoHotkey): order and extended highlighting (#1579) + - fix(Go): correctly highlight hex numbers, rather than stopping at last 'd' or 'f'. (#2060) + - fix(Mathematica): Improvements to language (#2065) + - fix(Node): Adds SCSS build (#2079) + - fix(Rust): update keywords (#2052) + - fix(Stata): Added keywords for the meta-analysis suite introduced in Stata 16 (#2081) + - fix(Bash): escape double quotes (#2048) + +## Version 9.15.8 + +New languages: + none. +New styles: + none. +Improvements: + - fix(bash): revert escaped double quotes - broke Firefox/Safari. + +## Version 9.15.7 +New languages: + none. +New styles: + none. +Improvements: + - fix(powershell): Add cmdlets (#2022) + - fix(Bash): escaped double quotes (#2041) + - fix(c++): add aliases 'hh', 'hxx', 'cxx' (#2017) + - fix(ini/toml): Support comments on the same line. (#2039) + - fix(JSX): not rendering well in a function without parentheses. (#2024) + - fix(LiveCode): language definition update (#2021) + - fix(markdown): indented lists (#2004) + - fix(styles/school-book): don't style all the pre, use .hljs instead (#2034) + - fix(JSX): Modify JSX tag detection to use XML language regex in place of simplistic \w+ + +## Version 9.15.6 +New languages: + none. +New styles: + none. +Improvements: + - Move dependencies to be devDependencies. + - Fixed security issues in dev dependencies. + +## Version 9.15.5 +New languages: + none. +New styles: + none. +Improvements: + 🔥 Hot fix: updated build tool. + +## Version 9.15.4 +New languages: + none. +New styles: + none. +Improvements: + 🔥 Hot fix: reverted hljs cli build tool, as it was causing issues with install. + +## Version 9.15.3 +New languages: + none. +New styles: + none. +Improvements: + 🔥 Hot fix: reverted hljs cli build tool, as it was causing issues with install. + +## Version 9.15.2 +New languages: + none. +New styles: + none. +Improvements: + 🔥 Hot fix that was preventing highlight.js from installing. + +## Version 9.15.1 + +New languages: + none. + +New styles: + none. + +Improvements: + +- Pony: Fixed keywords without spaces at line ends, highlighting of `iso` in class definitions, and function heads without bodies in traits and interfaces. Removed FUNCTION and CLASS modes until they are found to be needed and to provide some of the fixes. + - Support external language files in minified version of highlight.js (#1888) + +## Version 9.15 + +New languages: + none. + +New styles: + none. + +Improvements: + - new cli tool `hljs` - allows easier [building from command line](docs/building-testing.rst#building-a-bundle-from-the-command-line). + - cpp: Fully support C++11 raw strings. (#1897) + - Python: Treat False None and True as literals (#1920) + +## Version 9.14.2 + +New languages: + none. +New styles: + none. +Improvements: +- *Gauss* fixed to stop global namespace pollution [Scott Hyndman][]. +- fix(Tcl): removed apostrophe string delimiters (don't exist) + +[Scott Hyndman]: https://github.com/shyndman + +## Version 9.14.1 + +New languages: + none. +New styles: + none. +Improvements: +- Pony: language improvements (#1958) + +## Version 9.14.0 + +New languages: + none. +New styles: + none. +Improvements: +- Pony: add missing "object" highlighting (#1932) +- Added *XQuery* built-in functions, prolog declarations, as well as parsing of function bodies, computed and direct constructors, by [Duncan Paterson][] +- fix(dart): Corrects highlighting with string interpolation. (#1946) +- fix(swift): be eager on optional-using types (!/?) (#1919) +- fix(tex): Changed cyrillic to unicode (IE11 throw SCRIPT5021) (#1601) +- fix(JavaScript): Recognize get/set accessor keywords (#1940) +- Fixed Dockerfile definition when using highlight continuation parameter, by [Laurent Voullemier][] +- Added tests & new `annotation` and `verbatim` keywords to *Crystal*, by [Benoit de Chezelles][] +- Added missing dockerfile markup tests, by [Laurent Voullemier][] + Allow empty prompt text in clojure-repl, by [Egor Rogov][] +- Fixed several issues with *Crystal* language definition, by [Johannes Müller][] +- Added `C#` as an alias for *CSharp* language, by [Ahmed Atito][] +- Added generic user-defined proc support, new compiler define, refactor to re-use rules, and add tests to *GAUSS*, by [Matthew Evans][] +- Improve *Crystal* language to highlight regexes after some keywords, by [Tsuyusato Kitsune][] +- Fix filterByQualifiers: fileInfo can be null +- Fixed String interpolation in Dart, by [Scott Hyndman][]. + +[Laurent Voullemier]: https://github.com/l-vo +[Benoit de Chezelles]: https://github.com/bew +[Johannes Müller]: https://github.com/straight-shoota +[Ahmed Atito]: https://github.com/atitoa93 +[Matthew Evans]: https://github.com/matthewevans +[Tsuyusato Kitsune]: https://github.com/MakeNowJust +[Scott Hyndman]: https://github.com/shyndman +[Duncan Paterson]: https://github.com/duncdrum + +## Version 9.13.1 + +Improvements: + +- *C#* function declarations no longer include trailing whitespace, by [JeremyTCD][] +- Added new and missing keywords to *AngelScript*, by [Melissa Geels][] +- *TypeScript* decorator factories highlighting fix, by [Antoine Boisier-Michaud][] +- Added support for multiline strings to *Swift*, by [Alejandro Isaza][] +- Fixed issue that was causing some minifiers to fail. +- Fixed `autoDetection` to accept language aliases. + +[JeremyTCD]: https://github.com/JeremyTCD +[Melissa Geels]: https://github.com/codecat +[Antoine Boisier-Michaud]: https://github.com/Aboisier +[Alejandro Isaza]: https://github.com/alejandro-isaza + +## Version 9.13.0 + +New languages: + +- *ArcGIS Arcade* by [John Foster][] +- *AngelScript* by [Melissa Geels][] +- *GML* by [meseta][] +- *isbl* built-in language DIRECTUM and Conterra by [Dmitriy Tarasov][]. +- *PostgreSQL* SQL dialect and PL/pgSQL language by [Egor Rogov][]. +- *ReasonML* by [Gidi Meir Morris][] +- *SAS* by [Mauricio Caceres Bravo][] +- *Plaintext* by [Egor Rogov][] +- *.properties* by [bostko][] and [Egor Rogov][] + +New styles: + +- *a11y-dark theme* by [Eric Bailey][] +- *a11y-light theme* by [Eric Bailey][] +- *An Old Hope* by [Gustavo Costa][] +- *Atom One Dark Reasonable* by [Gidi Meir Morris][] +- *isbl editor dark* by [Dmitriy Tarasov][] +- *isbl editor light* by [Dmitriy Tarasov][] +- *Lightfair* by [Tristian Kelly][] +- [*Nord*][nord-highlightjs] by [Arctic Ice Studio][] +- *[🦄 Shades of Purple](https://github.com/ahmadawais/Shades-of-Purple-HighlightJS)* by [Ahmad Awais][] + +Improvements: + +- New attribute `endSameAsBegin` for nested constructs with variable names + by [Egor Rogov][]. +- *Python* highlighting of escaped quotes fixed by [Harmon][] +- *PHP*: Added alias for php7, by [Vijaya Chandran Mani][] +- *C++* string handling, by [David Benjamin][] +- *Swift* Add `@objcMembers` to `@attributes`, by [Berk Çebi][] +- Infrastructural changes by [Marcos Cáceres][] +- Fixed metachars highighting for *NSIS* by [Jan T. Sott][] +- *Yaml* highlight local tags as types by [Léo Lam][] +- Improved highlighting for *Elixir* by [Piotr Kaminski][] +- New attribute `disableAutodetect` for preventing autodetection by [Egor Rogov][] +- *Matlab*: transpose operators and double quote strings, by [JohnC32][] and [Egor Rogov][] +- Various documentation typos and improvemets by [Jimmy Wärting][], [Lutz Büch][], [bcleland][] +- *Cmake* updated with new keywords and commands by [Deniz Bahadir][] + +[Ahmad Awais]: https://github.com/ahmadawais +[Arctic Ice Studio]: https://github.com/arcticicestudio +[Dmitriy Tarasov]: https://github.com/MedvedTMN +[Egor Rogov]: https://github.com/egor-rogov +[Eric Bailey]: https://github.com/ericwbailey +[Gidi Meir Morris]: https://github.com/gmmorris +[Gustavo Costa]: https://github.com/gusbemacbe +[Harmon]: https://github.com/Harmon758 +[Melissa Geels]: https://github.com/codecat +[meseta]: https://github.com/meseta +[nord-highlightjs]: https://github.com/arcticicestudio/nord-highlightjs +[Tristian Kelly]: https://github.com/TristianK3604 +[Vijaya Chandran Mani]: https://github.com/vijaycs85 +[John Foster]: https://github.com/jf990 +[David Benjamin]: https://github.com/davidben +[Berk Çebi]: https://github.com/berkcebi +[Mauricio Caceres Bravo]: https://github.com/mcaceresb +[bostko]: https://github.com/bostko +[Deniz Bahadir]: https://github.com/Bagira80 +[bcleland]: https://github.com/bcleland +[JohnC32]: https://github.com/JohnC32 +[Lutz Büch]: https://github.com/lutz-100worte +[Piotr Kaminski]: https://github.com/pkaminski +[Léo Lam]: https://github.com/leoetlino +[Jan T. Sott]: https://github.com/idleberg +[Jimmy Wärting]: https://github.com/jimmywarting +[Marcos Cáceres]: https://github.com/marcoscaceres + +## Version 9.12.0 + +New language: + +- *MikroTik* RouterOS Scripting language by [Ivan Dementev][]. + +New style: + +- *VisualStudio 2015 Dark* by [Nicolas LLOBERA][] + +Improvements: +- *Crystal* updated with new keywords and syntaxes by [Tsuyusato Kitsune][]. +- *Julia* updated to the modern definitions by [Alex Arslan][]. +- *julia-repl* added by [Morten Piibeleht][]. +- [Stanislav Belov][] wrote a new definition for *1C*, replacing the one that + has not been updated for more than 8 years. The new version supports syntax + for versions 7.7 and 8. +- [Nicolas LLOBERA][] improved C# definition fixing edge cases with function + titles detection and added highlighting of `[Attributes]`. +- [nnnik][] provided a few correctness fixes for *Autohotkey*. +- [Martin Clausen][] made annotation collections in *Clojure* to look + consistently with other kinds. +- [Alejandro Alonso][] updated *Swift* keywords. + +[Tsuyusato Kitsune]: https://github.com/MakeNowJust +[Alex Arslan]: https://github.com/ararslan +[Morten Piibeleht]: https://github.com/mortenpi +[Stanislav Belov]: https://github.com/4ppl +[Ivan Dementev]: https://github.com/DiVAN1x +[Nicolas LLOBERA]: https://github.com/Nicolas01 +[nnnik]: https://github.com/nnnik +[Martin Clausen]: https://github.com/maacl +[Alejandro Alonso]: https://github.com/Azoy + +## Version 9.11.0 + +New languages: + +- *Shell* by [Tsuyusato Kitsune][] +- *jboss-cli* by [Raphaël Parrëe][] + +Improvements: + +- [Joël Porquet] has [greatly improved the definition of *makefile*][5b3e0e6]. +- *C++* class titles are now highlighted as in other languages with classes. +- [Jordi Petit][] added rarely used `or`, `and` and `not` keywords to *C++*. +- [Pieter Vantorre][] fixed highlighting of negative floating point values. + + +[Tsuyusato Kitsune]: https://github.com/MakeNowJust +[Jordi Petit]: https://github.com/jordi-petit +[Raphaël Parrëe]: https://github.com/rparree +[Pieter Vantorre]: https://github.com/NuclearCookie +[5b3e0e6]: https://github.com/isagalaev/highlight.js/commit/5b3e0e68bfaae282faff6697d6a490567fa9d44b + + +## Version 9.10.0 + +Apologies for missing the previous release cycle. Some thing just can't be +automated… Anyway, we're back! + +New languages: + +- *Hy* by [Sergey Sobko][] +- *Leaf* by [Hale Chan][] +- *N1QL* by [Andres Täht][] and [Rene Saarsoo][] + +Improvements: + +- *Rust* got updated with new keywords by [Kasper Andersen][] and then + significantly modernized even more by [Eduard-Mihai Burtescu][] (yes, @eddyb, + Rust core team member!) +- *Python* updated with f-literals by [Philipp A][]. +- *YAML* updated with unquoted strings support. +- *Gauss* updated with new keywords by [Matt Evans][]. +- *Lua* updated with new keywords by [Joe Blow][]. +- *Kotlin* updated with new keywords by [Philipp Hauer][]. +- *TypeScript* got highlighting of function params and updated keywords by + [Ike Ku][]. +- *Scheme* now correctly handles \`-quoted lists thanks to [Guannan Wei]. +- [Sam Wu][] fixed handling of `<<` in *C++* defines. + +[Philipp A]: https://github.com/flying-sheep +[Philipp Hauer]: https://github.com/phauer +[Sergey Sobko]: https://github.com/profitware +[Hale Chan]: https://github.com/halechan +[Matt Evans]: https://github.com/matthewevans +[Joe Blow]: https://github.com/mossarelli +[Kasper Andersen]: https://github.com/kasma1990 +[Eduard-Mihai Burtescu]: https://github.com/eddyb +[Andres Täht]: https://github.com/andrestaht +[Rene Saarsoo]: https://github.com/nene +[Philipp Hauer]: https://github.com/phauer +[Ike Ku]: https://github.com/dempfi +[Guannan Wei]: https://github.com/Kraks +[Sam Wu]: https://github.com/samsam2310 + + +## Version 9.9.0 + +New languages + +- *LLVM* by [Michael Rodler][] + +Improvements: + +- *TypeScript* updated with annotations and param lists inside constructors, by + [Raphael Parree][]. +- *CoffeeScript* updated with new keywords and fixed to recognize JavaScript + in \`\`\`, thanks to thanks to [Geoffrey Booth][]. +- Compiler directives in *Delphi* are now correctly highlighted as "meta". + +[Raphael Parree]: https://github.com/rparree +[Michael Rodler]: https://github.com/f0rki +[Geoffrey Booth]: https://github.com/GeoffreyBooth + + +## Version 9.8.0 "New York" + +This version is the second one that deserved a name. Because I'm in New York, +and the release isn't missing the deadline only because it's still Tuesday on +West Coast. + +New languages: + +- *Clean* by [Camil Staps][] +- *Flix* by [Magnus Madsen][] + +Improvements: + +- [Kenton Hamaluik][] did a comprehensive update for *Haxe*. +- New commands for *PowerShell* from [Nicolas Le Gall][]. +- [Jan T. Sott][] updated *NSIS*. +- *Java* and *Swift* support unicode characters in identifiers thanks to + [Alexander Lichter][]. + +[Camil Staps]: https://github.com/camilstaps +[Magnus Madsen]: https://github.com/magnus-madsen +[Kenton Hamaluik]: https://github.com/FuzzyWuzzie +[Nicolas Le Gall]: https://github.com/darkitty +[Jan T. Sott]: https://github.com/idleberg +[Alexander Lichter]: https://github.com/manniL + + +## Version 9.7.0 + +A comprehensive bugfix release. This is one of the best things about +highlight.js: even boring things keep getting better (even if slow). + +- VHDL updated with PSL keywords and uses more consistent styling. +- Nested C-style comments no longer break highlighting in many languages. +- JavaScript updated with `=>` functions, highlighted object attributes and + parsing within template string substitution blocks (`${...}`). +- Fixed another corner case with self-closing `` in JSX. +- Added `HEALTHCHECK` directive in Docker. +- Delphi updated with new Free Pascal keywords. +- Fixed digit separator parsing in C++. +- C# updated with new keywords and fixed to allow multiple identifiers within + generics `<...>`. +- Fixed another slow regex in Less. + + +## Version 9.6.0 + +New languages: + +- *ABNF* and *EBNF* by [Alex McKibben][] +- *Awk* by [Matthew Daly][] +- *SubUnit* by [Sergey Bronnikov][] + +New styles: + +- *Atom One* in both Dark and Light variants by [Daniel Gamage][] + +Plus, a few smaller updates for *Lasso*, *Elixir*, *C++* and *SQL*. + +[Alex McKibben]: https://github.com/mckibbenta +[Daniel Gamage]: https://github.com/danielgamage +[Matthew Daly]: https://github.com/matthewbdaly +[Sergey Bronnikov]: https://github.com/ligurio + + +## Version 9.5.0 + +New languages: + +- *Excel* by [Victor Zhou][] +- *Linden Scripting Language* by [Builder's Brewery][] +- *TAP* (Test Anything Protocol) by [Sergey Bronnikov][] +- *Pony* by [Joe Eli McIlvain][] +- *Coq* by [Stephan Boyer][] +- *dsconfig* and *LDIF* by [Jacob Childress][] + +New styles: + +- *Ocean Dark* by [Gavin Siu][] + +Notable changes: + +- [Minh Nguyễn][] added more built-ins to Objective C. +- [Jeremy Hull][] fixed corner cases in C++ preprocessor directives and Diff + comments. +- [Victor Zhou][] added support for digit separators in C++ numbers. + +[Gavin Siu]: https://github.com/gavsiu +[Builder's Brewery]: https://github.com/buildersbrewery +[Victor Zhou]: https://github.com/OiCMudkips +[Sergey Bronnikov]: https://github.com/ligurio +[Joe Eli McIlvain]: https://github.com/jemc +[Stephan Boyer]: https://github.com/boyers +[Jacob Childress]: https://github.com/braveulysses +[Minh Nguyễn]: https://github.com/1ec5 +[Jeremy Hull]: https://github.com/sourrust + + +## Version 9.4.0 + +New languages: + +- *PureBASIC* by [Tristano Ajmone][] +- *BNF* by [Oleg Efimov][] +- *Ada* by [Lars Schulna][] + +New styles: + +- *PureBASIC* by [Tristano Ajmone][] + +Improvements to existing languages and styles: + +- We now highlight function declarations in Go. +- [Taisuke Fujimoto][] contributed very convoluted rules for raw and + interpolated strings in C#. +- [Boone Severson][] updated Verilog to comply with IEEE 1800-2012 + SystemVerilog. +- [Victor Zhou][] improved rules for comments and strings in PowerShell files. +- [Janis Voigtländer][] updated the definition of Elm to version 0.17 of the + languages. Elm is now featured on the front page of . +- Special variable `$this` is highlighted as a keyword in PHP. +- `usize` and `isize` are now highlighted in Rust. +- Fixed labels and directives in x86 assembler. + +[Tristano Ajmone]: https://github.com/tajmone +[Taisuke Fujimoto]: https://github.com/temp-impl +[Oleg Efimov]: https://github.com/Sannis +[Boone Severson]: https://github.com/BooneJS +[Victor Zhou]: https://github.com/OiCMudkips +[Lars Schulna]: https://github.com/captain-hanuta +[Janis Voigtländer]: https://github.com/jvoigtlaender + + +## Version 9.3.0 + +New languages: + +- *Tagger Script* by [Philipp Wolfer][] +- *MoonScript* by [Billy Quith][] + +New styles: + +- *xt256* by [Herbert Shin][] + +Improvements to existing languages and styles: + +- More robust handling of unquoted HTML tag attributes +- Relevance tuning for QML which was unnecessary eager at seizing other + languages' code +- Improve GAMS language parsing +- Fixed a bunch of bugs around selectors in Less +- Kotlin's got a new definition for annotations, updated keywords and other + minor improvements +- Added `move` to Rust keywords +- Markdown now recognizes \`\`\`-fenced code blocks +- Improved detection of function declarations in C++ and C# + +[Philipp Wolfer]: https://github.com/phw +[Billy Quith]: https://github.com/billyquith +[Herbert Shin]: https://github.com/initbar + + +## Version 9.2.0 + +New languages: + +- *QML* by [John Foster][] +- *HTMLBars* by [Michael Johnston][] +- *CSP* by [Taras][] +- *Maxima* by [Robert Dodier][] + +New styles: + +- *Gruvbox* by [Qeole][] +- *Dracula* by [Denis Ciccale][] + +Improvements to existing languages and styles: + +- We now correctly handle JSX with arbitrary node tree depth. +- Argument list for `(lambda)` in Scheme is no longer highlighted as a function + call. +- Stylus syntax doesn't break on valid CSS. +- More correct handling of comments and strings and other improvements for + VimScript. +- More subtle work on the default style. +- We now use anonymous modules for AMD. +- `macro_rules!` is now recognized as a built-in in Rust. + +[John Foster]: https://github.com/jf990 +[Qeole]: https://github.com/Qeole +[Denis Ciccale]: https://github.com/dciccale +[Michael Johnston]: https://github.com/lastobelus +[Taras]: https://github.com/oxdef +[Robert Dodier]: https://github.com/robert-dodier + + +## Version 9.1.0 + +New languages: + +- *Stan* by [Brendan Rocks][] +- *BASIC* by [Raphaël Assénat][] +- *GAUSS* by [Matt Evans][] +- *DTS* by [Martin Braun][] +- *Arduino* by [Stefania Mellai][] + +New Styles: + +- *Arduino Light* by [Stefania Mellai][] + +Improvements to existing languages and styles: + +- Handle return type annotations in Python +- Allow shebang headers in Javascript +- Support strings in Rust meta +- Recognize `struct` as a class-level definition in Rust +- Recognize b-prefixed chars and strings in Rust +- Better numbers handling in Verilog + +[Brendan Rocks]: http://brendanrocks.com +[Raphaël Assénat]: https://github.com/raphnet +[Matt Evans]: https://github.com/matthewevans +[Martin Braun]: https://github.com/mbr0wn +[Stefania Mellai]: https://github.com/smellai + + +## Version 9.0.0 + +The new major version brings a reworked styling system. Highlight.js now defines +a limited set of highlightable classes giving a consistent result across all the +styles and languages. You can read a more detailed explanation and background in +the [tracking issue][#348] that started this long process back in May. + +This change is backwards incompatible for those who uses highlight.js with a +custom stylesheet. The [new style guide][sg] explains how to write styles +in this new world. + +Bundled themes have also suffered a significant amount of improvements and may +look different in places, but all the things now consistent and make more sense. +Among others, the Default style has got a refresh and will probably be tweaked +some more in next releases. Please do give your feedback in our +[issue tracker][issues]. + +New languages in this release: + +- *Caché Object Script* by [Nikita Savchenko][] +- *YAML* by [Stefan Wienert][] +- *MIPS Assembler* by [Nebuleon Fumika][] +- *HSP* by [prince][] + +Improvements to existing languages and styles: + +- ECMAScript 6 modules import now do not require closing semicolon. +- ECMAScript 6 classes constructors now highlighted. +- Template string support for Typescript, as for ECMAScript 6. +- Scala case classes params highlight fixed. +- Built-in names introduced in Julia v0.4 added by [Kenta Sato][]. +- Refreshed Default style. + +Other notable changes: + +- [Web workers support][webworkers] added bu [Jan Kühle][]. +- We now have tests for compressed browser builds as well. +- The building tool chain has been switched to node.js 4.x. and is now + shamelessly uses ES6 features all over the place, courtesy of [Jeremy Hull][]. +- License added to non-compressed browser build. + +[Jan Kühle]: https://github.com/frigus02 +[Stefan Wienert]: https://github.com/zealot128 +[Kenta Sato]: https://github.com/bicycle1885 +[Nikita Savchenko]: https://github.com/ZitRos +[webworkers]: https://github.com/isagalaev/highlight.js#web-workers +[Jeremy Hull]: https://github.com/sourrust +[#348]: https://github.com/isagalaev/highlight.js/issues/348 +[sg]: http://highlightjs.readthedocs.org/en/latest/style-guide.html +[issues]: https://github.com/isagalaev/highlight.js/issues +[Nebuleon Fumika]: https://github.com/Nebuleon +[prince]: https://github.com/prince-0203 + + +## Version 8.9.1 + +Some last-minute changes reverted due to strange bug with minified browser build: + +- Scala case classes params highlight fixed +- ECMAScript 6 modules import now do not require closing semicolon +- ECMAScript 6 classes constructors now highlighted +- Template string support for Typescript, as for ECMAScript 6 +- License added to not minified browser build + + +## Version 8.9.0 + +New languages: + +- *crmsh* by [Kristoffer Gronlund][] +- *SQF* by [Soren Enevoldsen][] + +[Kristoffer Gronlund]: https://github.com/krig +[Soren Enevoldsen]: https://github.com/senevoldsen90 + +Notable fixes and improvements to existing languages: + +- Added `abstract` and `namespace` keywords to TypeScript by [Daniel Rosenwasser][] +- Added `label` support to Dockerfile by [Ladislav Prskavec][] +- Crystal highlighting improved by [Tsuyusato Kitsune][] +- Missing Swift keywords added by [Nate Cook][] +- Improve detection of C block comments +- ~~Scala case classes params highlight fixed~~ +- ~~ECMAScript 6 modules import now do not require closing semicolon~~ +- ~~ECMAScript 6 classes constructors now highlighted~~ +- ~~Template string support for Typescript, as for ECMAScript 6~~ + +Other notable changes: + +- ~~License added to not minified browser build~~ + +[Kristoffer Gronlund]: https://github.com/krig +[Søren Enevoldsen]: https://github.com/senevoldsen90 +[Daniel Rosenwasser]: https://github.com/DanielRosenwasser +[Ladislav Prskavec]: https://github.com/abtris +[Tsuyusato Kitsune]: https://github.com/MakeNowJust +[Nate Cook]: https://github.com/natecook1000 + + +## Version 8.8.0 + +New languages: + +- *Golo* by [Philippe Charrière][] +- *GAMS* by [Stefan Bechert][] +- *IRPF90* by [Anthony Scemama][] +- *Access logs* by [Oleg Efimov][] +- *Crystal* by [Tsuyusato Kitsune][] + +Notable fixes and improvements to existing languages: + +- JavaScript highlighting no longer fails with ES6 default parameters +- Added keywords `async` and `await` to Python +- PHP heredoc support improved +- Allow preprocessor directives within C++ functions + +Other notable changes: + +- Change versions to X.Y.Z SemVer-compatible format +- Added ability to build all targets at once + +[Philippe Charrière]: https://github.com/k33g +[Stefan Bechert]: https://github.com/b-pos465 +[Anthony Scemama]: https://github.com/scemama +[Oleg Efimov]: https://github.com/Sannis +[Tsuyusato Kitsune]: https://github.com/MakeNowJust + + +## Version 8.7 + +New languages: + +- *Zephir* by [Oleg Efimov][] +- *Elm* by [Janis Voigtländer][] +- *XQuery* by [Dirk Kirsten][] +- *Mojolicious* by [Dotan Dimet][] +- *AutoIt* by Manh Tuan from [J2TeaM][] +- *Toml* (ini extension) by [Guillaume Gomez][] + +New styles: + +- *Hopscotch* by [Jan T. Sott][] +- *Grayscale* by [MY Sun][] + +Notable fixes and improvements to existing languages: + +- Fix encoding of images when copied over in certain builds +- Fix incorrect highlighting of the word "bug" in comments +- Treat decorators different from matrix multiplication in Python +- Fix traits inheritance highlighting in Rust +- Fix incorrect document +- Oracle keywords added to SQL language definition by [Vadimtro][] +- Postgres keywords added to SQL language definition by [Benjamin Auder][] +- Fix registers in x86asm being highlighted as a hex number +- Fix highlighting for numbers with a leading decimal point +- Correctly highlight numbers and strings inside of C/C++ macros +- C/C++ functions now support pointer, reference, and move returns + +[Oleg Efimov]: https://github.com/Sannis +[Guillaume Gomez]: https://github.com/GuillaumeGomez +[Janis Voigtländer]: https://github.com/jvoigtlaender +[Jan T. Sott]: https://github.com/idleberg +[Dirk Kirsten]: https://github.com/dirkk +[MY Sun]: https://github.com/simonmysun +[Vadimtro]: https://github.com/Vadimtro +[Benjamin Auder]: https://github.com/ghost +[Dotan Dimet]: https://github.com/dotandimet +[J2TeaM]: https://github.com/J2TeaM + + +## Version 8.6 + +New languages: + +- *C/AL* by [Kenneth Fuglsang][] +- *DNS zone file* by [Tim Schumacher][] +- *Ceylon* by [Lucas Werkmeister][] +- *OpenSCAD* by [Dan Panzarella][] +- *Inform7* by [Bruno Dias][] +- *armasm* by [Dan Panzarella][] +- *TP* by [Jay Strybis][] + +New styles: + +- *Atelier Cave*, *Atelier Estuary*, + *Atelier Plateau* and *Atelier Savanna* by [Bram de Haan][] +- *Github Gist* by [Louis Barranqueiro][] + +Notable fixes and improvements to existing languages: + +- Multi-line raw strings from C++11 are now supported +- Fix class names with dashes in HAML +- The `async` keyword from ES6/7 is now supported +- TypeScript functions handle type and parameter complexity better +- We unified phpdoc/javadoc/yardoc etc modes across all languages +- CSS .class selectors relevance was dropped to prevent wrong language detection +- Images is now included to CDN build +- Release process is now automated + +[Bram de Haan]: https://github.com/atelierbram +[Kenneth Fuglsang]: https://github.com/kfuglsang +[Louis Barranqueiro]: https://github.com/LouisBarranqueiro +[Tim Schumacher]: https://github.com/enko +[Lucas Werkmeister]: https://github.com/lucaswerkmeister +[Dan Panzarella]: https://github.com/pzl +[Bruno Dias]: https://github.com/sequitur +[Jay Strybis]: https://github.com/unreal + + +## Version 8.5 + +New languages: + +- *pf.conf* by [Peter Piwowarski][] +- *Julia* by [Kenta Sato][] +- *Prolog* by [Raivo Laanemets][] +- *Docker* by [Alexis Hénaut][] +- *Fortran* by [Anthony Scemama][] and [Thomas Applencourt][] +- *Kotlin* by [Sergey Mashkov][] + +New styles: + +- *Agate* by [Taufik Nurrohman][] +- *Darcula* by [JetBrains][] +- *Atelier Sulphurpool* by [Bram de Haan][] +- *Android Studio* by [Pedro Oliveira][] + +Notable fixes and improvements to existing languages: + +- ES6 features in JavaScript are better supported now by [Gu Yiling][]. +- Swift now recognizes body-less method definitions. +- Single expression functions `def foo, do: ... ` now work in Elixir. +- More uniform detection of built-in classes in Objective C. +- Fixes for number literals and processor directives in Rust. +- HTML ` + ``` + +- `tabReplace` and `useBR` that were used in different places are also unified + into the global options object and are to be set using `configure(options)`. + This function is documented in our [API docs][]. Also note that these + parameters are gone from `highlightBlock` and `fixMarkup` which are now also + rely on `configure`. + +- We removed public-facing (though undocumented) object `hljs.LANGUAGES` which + was used to register languages with the library in favor of two new methods: + `registerLanguage` and `getLanguage`. Both are documented in our [API docs][]. + +- Result returned from `highlight` and `highlightAuto` no longer contains two + separate attributes contributing to relevance score, `relevance` and + `keyword_count`. They are now unified in `relevance`. + +Another technically compatible change that nonetheless might need attention: + +- The structure of the NPM package was refactored, so if you had installed it + locally, you'll have to update your paths. The usual `require('highlight.js')` + works as before. This is contributed by [Dmitry Smolin][]. + +New features: + +- Languages now can be recognized by multiple names like "js" for JavaScript or + "html" for, well, HTML (which earlier insisted on calling it "xml"). These + aliases can be specified in the class attribute of the code container in your + HTML as well as in various API calls. For now there are only a few very common + aliases but we'll expand it in the future. All of them are listed in the + [class reference][cr]. + +- Language detection can now be restricted to a subset of languages relevant in + a given context — a web page or even a single highlighting call. This is + especially useful for node.js build that includes all the known languages. + Another example is a StackOverflow-style site where users specify languages + as tags rather than in the markdown-formatted code snippets. This is + documented in the [API reference][] (see methods `highlightAuto` and + `configure`). + +- Language definition syntax streamlined with [variants][] and + [beginKeywords][]. + +New languages and styles: + +- *Oxygene* by [Carlo Kok][] +- *Mathematica* by [Daniel Kvasnička][] +- *Autohotkey* by [Seongwon Lee][] +- *Atelier* family of styles in 10 variants by [Bram de Haan][] +- *Paraíso* styles by [Jan T. Sott][] + +Miscellaneous improvements: + +- Highlighting `=>` prompts in Clojure. +- [Jeremy Hull][] fixed a lot of styles for consistency. +- Finally, highlighting PHP and HTML [mixed in peculiar ways][php-html]. +- Objective C and C# now properly highlight titles in method definition. +- Big overhaul of relevance counting for a number of languages. Please do report + bugs about mis-detection of non-trivial code snippets! + +[API reference]: http://highlightjs.readthedocs.org/en/latest/api.html + +[cr]: http://highlightjs.readthedocs.org/en/latest/css-classes-reference.html +[api docs]: http://highlightjs.readthedocs.org/en/latest/api.html +[variants]: https://groups.google.com/d/topic/highlightjs/VoGC9-1p5vk/discussion +[beginKeywords]: https://github.com/isagalaev/highlight.js/commit/6c7fdea002eb3949577a85b3f7930137c7c3038d +[php-html]: https://twitter.com/highlightjs/status/408890903017689088 + +[Carlo Kok]: https://github.com/carlokok +[Bram de Haan]: https://github.com/atelierbram +[Daniel Kvasnička]: https://github.com/dkvasnicka +[Dmitry Smolin]: https://github.com/dimsmol +[Jeremy Hull]: https://github.com/sourrust +[Seongwon Lee]: https://github.com/dlimpid +[Jan T. Sott]: https://github.com/idleberg + + +## Version 7.5 + +A catch-up release dealing with some of the accumulated contributions. This one +is probably will be the last before the 8.0 which will be slightly backwards +incompatible regarding some advanced use-cases. + +One outstanding change in this version is the addition of 6 languages to the +[hosted script][d]: Markdown, ObjectiveC, CoffeeScript, Apache, Nginx and +Makefile. It now weighs about 6K more but we're going to keep it under 30K. + +New languages: + +- OCaml by [Mehdi Dogguy][mehdid] and [Nicolas Braud-Santoni][nbraud] +- [LiveCode Server][lcs] by [Ralf Bitter][revig] +- Scilab by [Sylvestre Ledru][sylvestre] +- basic support for Makefile by [Ivan Sagalaev][isagalaev] + +Improvements: + +- Ruby's got support for characters like `?A`, `?1`, `?\012` etc. and `%r{..}` + regexps. +- Clojure now allows a function call in the beginning of s-expressions + `(($filter "myCount") (arr 1 2 3 4 5))`. +- Haskell's got new keywords and now recognizes more things like pragmas, + preprocessors, modules, containers, FFIs etc. Thanks to [Zena Treep][treep] + for the implementation and to [Jeremy Hull][sourrust] for guiding it. +- Miscellaneous fixes in PHP, Brainfuck, SCSS, Asciidoc, CMake, Python and F#. + +[mehdid]: https://github.com/mehdid +[nbraud]: https://github.com/nbraud +[revig]: https://github.com/revig +[lcs]: http://livecode.com/developers/guides/server/ +[sylvestre]: https://github.com/sylvestre +[isagalaev]: https://github.com/isagalaev +[treep]: https://github.com/treep +[sourrust]: https://github.com/sourrust +[d]: http://highlightjs.org/download/ + + +## New core developers + +The latest long period of almost complete inactivity in the project coincided +with growing interest to it led to a decision that now seems completely obvious: +we need more core developers. + +So without further ado let me welcome to the core team two long-time +contributors: [Jeremy Hull][] and [Oleg +Efimov][]. + +Hope now we'll be able to work through stuff faster! + +P.S. The historical commit is [here][1] for the record. + +[Jeremy Hull]: https://github.com/sourrust +[Oleg Efimov]: https://github.com/sannis +[1]: https://github.com/isagalaev/highlight.js/commit/f3056941bda56d2b72276b97bc0dd5f230f2473f + + +## Version 7.4 + +This long overdue version is a snapshot of the current source tree with all the +changes that happened during the past year. Sorry for taking so long! + +Along with the changes in code highlight.js has finally got its new home at +, moving from its cradle on Software Maniacs which it +outgrew a long time ago. Be sure to report any bugs about the site to +. + +On to what's new… + +New languages: + +- Handlebars templates by [Robin Ward][] +- Oracle Rules Language by [Jason Jacobson][] +- F# by [Joans Follesø][] +- AsciiDoc and Haml by [Dan Allen][] +- Lasso by [Eric Knibbe][] +- SCSS by [Kurt Emch][] +- VB.NET by [Poren Chiang][] +- Mizar by [Kelley van Evert][] + +[Robin Ward]: https://github.com/eviltrout +[Jason Jacobson]: https://github.com/jayce7 +[Joans Follesø]: https://github.com/follesoe +[Dan Allen]: https://github.com/mojavelinux +[Eric Knibbe]: https://github.com/EricFromCanada +[Kurt Emch]: https://github.com/kemch +[Poren Chiang]: https://github.com/rschiang +[Kelley van Evert]: https://github.com/kelleyvanevert + +New style themes: + +- Monokai Sublime by [noformnocontent][] +- Railscasts by [Damien White][] +- Obsidian by [Alexander Marenin][] +- Docco by [Simon Madine][] +- Mono Blue by [Ivan Sagalaev][] (uses a single color hue for everything) +- Foundation by [Dan Allen][] + +[noformnocontent]: http://nn.mit-license.org/ +[Damien White]: https://github.com/visoft +[Alexander Marenin]: https://github.com/ioncreature +[Simon Madine]: https://github.com/thingsinjars +[Ivan Sagalaev]: https://github.com/isagalaev + +Other notable changes: + +- Corrected many corner cases in CSS. +- Dropped Python 2 version of the build tool. +- Implemented building for the AMD format. +- Updated Rust keywords (thanks to [Dmitry Medvinsky][]). +- Literal regexes can now be used in language definitions. +- CoffeeScript highlighting is now significantly more robust and rich due to + input from [Cédric Néhémie][]. + +[Dmitry Medvinsky]: https://github.com/dmedvinsky +[Cédric Néhémie]: https://github.com/abe33 + + +## Version 7.3 + +- Since this version highlight.js no longer works in IE version 8 and older. + It's made it possible to reduce the library size and dramatically improve code + readability and made it easier to maintain. Time to go forward! + +- New languages: AppleScript (by [Nathan Grigg][ng] and [Dr. Drang][dd]) and + Brainfuck (by [Evgeny Stepanischev][bolk]). + +- Improvements to existing languages: + + - interpreter prompt in Python (`>>>` and `...`) + - @-properties and classes in CoffeeScript + - E4X in JavaScript (by [Oleg Efimov][oe]) + - new keywords in Perl (by [Kirk Kimmel][kk]) + - big Ruby syntax update (by [Vasily Polovnyov][vast]) + - small fixes in Bash + +- Also Oleg Efimov did a great job of moving all the docs for language and style + developers and contributors from the old wiki under the source code in the + "docs" directory. Now these docs are nicely presented at + . + +[ng]: https://github.com/nathan11g +[dd]: https://github.com/drdrang +[bolk]: https://github.com/bolknote +[oe]: https://github.com/Sannis +[kk]: https://github.com/kimmel +[vast]: https://github.com/vast + + +## Version 7.2 + +A regular bug-fix release without any significant new features. Enjoy! + + +## Version 7.1 + +A Summer crop: + +- [Marc Fornos][mf] made the definition for Clojure along with the matching + style Rainbow (which, of course, works for other languages too). +- CoffeeScript support continues to improve getting support for regular + expressions. +- Yoshihide Jimbo ported to highlight.js [five Tomorrow styles][tm] from the + [project by Chris Kempson][tm0]. +- Thanks to [Casey Duncun][cd] the library can now be built in the popular + [AMD format][amd]. +- And last but not least, we've got a fair number of correctness and consistency + fixes, including a pretty significant refactoring of Ruby. + +[mf]: https://github.com/mfornos +[tm]: http://jmblog.github.com/color-themes-for-highlightjs/ +[tm0]: https://github.com/ChrisKempson/Tomorrow-Theme +[cd]: https://github.com/caseman +[amd]: http://requirejs.org/docs/whyamd.html + + +## Version 7.0 + +The reason for the new major version update is a global change of keyword syntax +which resulted in the library getting smaller once again. For example, the +hosted build is 2K less than at the previous version while supporting two new +languages. + +Notable changes: + +- The library now works not only in a browser but also with [node.js][]. It is + installable with `npm install highlight.js`. [API][] docs are available on our + wiki. + +- The new unique feature (apparently) among syntax highlighters is highlighting + *HTTP* headers and an arbitrary language in the request body. The most useful + languages here are *XML* and *JSON* both of which highlight.js does support. + Here's [the detailed post][p] about the feature. + +- Two new style themes: a dark "south" *[Pojoaque][]* by Jason Tate and an + emulation of*XCode* IDE by [Angel Olloqui][ao]. + +- Three new languages: *D* by [Aleksandar Ružičić][ar], *R* by [Joe Cheng][jc] + and *GLSL* by [Sergey Tikhomirov][st]. + +- *Nginx* syntax has become a million times smaller and more universal thanks to + remaking it in a more generic manner that doesn't require listing all the + directives in the known universe. + +- Function titles are now highlighted in *PHP*. + +- *Haskell* and *VHDL* were significantly reworked to be more rich and correct + by their respective maintainers [Jeremy Hull][sr] and [Igor Kalnitsky][ik]. + +And last but not least, many bugs have been fixed around correctness and +language detection. + +Overall highlight.js currently supports 51 languages and 20 style themes. + +[node.js]: http://nodejs.org/ +[api]: http://softwaremaniacs.org/wiki/doku.php/highlight.js:api +[p]: http://softwaremaniacs.org/blog/2012/05/10/http-and-json-in-highlight-js/en/ +[pojoaque]: http://web-cms-designs.com/ftopict-10-pojoaque-style-for-highlight-js-code-highlighter.html +[ao]: https://github.com/angelolloqui +[ar]: https://github.com/raleksandar +[jc]: https://github.com/jcheng5 +[st]: https://github.com/tikhomirov +[sr]: https://github.com/sourrust +[ik]: https://github.com/ikalnitsky + + +## Version 6.2 + +A lot of things happened in highlight.js since the last version! We've got nine +new contributors, the discussion group came alive, and the main branch on GitHub +now counts more than 350 followers. Here are most significant results coming +from all this activity: + +- 5 (five!) new languages: Rust, ActionScript, CoffeeScript, MatLab and + experimental support for markdown. Thanks go to [Andrey Vlasovskikh][av], + [Alexander Myadzel][am], [Dmytrii Nagirniak][dn], [Oleg Efimov][oe], [Denis + Bardadym][db] and [John Crepezzi][jc]. + +- 2 new style themes: Monokai by [Luigi Maselli][lm] and stylistic imitation of + another well-known highlighter Google Code Prettify by [Aahan Krish][ak]. + +- A vast number of [correctness fixes and code refactorings][log], mostly made + by [Oleg Efimov][oe] and [Evgeny Stepanischev][es]. + +[av]: https://github.com/vlasovskikh +[am]: https://github.com/myadzel +[dn]: https://github.com/dnagir +[oe]: https://github.com/Sannis +[db]: https://github.com/btd +[jc]: https://github.com/seejohnrun +[lm]: http://grigio.org/ +[ak]: https://github.com/geekpanth3r +[es]: https://github.com/bolknote +[log]: https://github.com/isagalaev/highlight.js/commits/ + + +## Version 6.1 — Solarized + +[Jeremy Hull][jh] has implemented my dream feature — a port of [Solarized][] +style theme famous for being based on the intricate color theory to achieve +correct contrast and color perception. It is now available for highlight.js in +both variants — light and dark. + +This version also adds a new original style Arta. Its author pumbur maintains a +[heavily modified fork of highlight.js][pb] on GitHub. + +[jh]: https://github.com/sourrust +[solarized]: http://ethanschoonover.com/solarized +[pb]: https://github.com/pumbur/highlight.js + + +## Version 6.0 + +New major version of the highlighter has been built on a significantly +refactored syntax. Due to this it's even smaller than the previous one while +supporting more languages! + +New languages are: + +- Haskell by [Jeremy Hull][sourrust] +- Erlang in two varieties — module and REPL — made collectively by [Nikolay + Zakharov][desh], [Dmitry Kovega][arhibot] and [Sergey Ignatov][ignatov] +- Objective C by [Valerii Hiora][vhbit] +- Vala by [Antono Vasiljev][antono] +- Go by [Stephan Kountso][steplg] + +[sourrust]: https://github.com/sourrust +[desh]: http://desh.su/ +[arhibot]: https://github.com/arhibot +[ignatov]: https://github.com/ignatov +[vhbit]: https://github.com/vhbit +[antono]: https://github.com/antono +[steplg]: https://github.com/steplg + +Also this version is marginally faster and fixes a number of small long-standing +bugs. + +Developer overview of the new language syntax is available in a [blog post about +recent beta release][beta]. + +[beta]: http://softwaremaniacs.org/blog/2011/04/25/highlight-js-60-beta/en/ + +P.S. New version is not yet available on a Yandex CDN, so for now you have to +download [your own copy][d]. + +[d]: /soft/highlight/en/download/ + + +## Version 5.14 + +Fixed bugs in HTML/XML detection and relevance introduced in previous +refactoring. + +Also test.html now shows the second best result of language detection by +relevance. + + +## Version 5.13 + +Past weekend began with a couple of simple additions for existing languages but +ended up in a big code refactoring bringing along nice improvements for language +developers. + +### For users + +- Description of C++ has got new keywords from the upcoming [C++ 0x][] standard. +- Description of HTML has got new tags from [HTML 5][]. +- CSS-styles have been unified to use consistent padding and also have lost + pop-outs with names of detected languages. +- [Igor Kalnitsky][ik] has sent two new language descriptions: CMake & VHDL. + +This makes total number of languages supported by highlight.js to reach 35. + +Bug fixes: + +- Custom classes on `
    ` tags are not being overridden anymore
    +- More correct highlighting of code blocks inside non-`
    ` containers:
    +  highlighter now doesn't insist on replacing them with its own container and
    +  just replaces the contents.
    +- Small fixes in browser compatibility and heuristics.
    +
    +[c++ 0x]: http://ru.wikipedia.org/wiki/C%2B%2B0x
    +[html 5]: http://en.wikipedia.org/wiki/HTML5
    +[ik]: http://kalnitsky.org.ua/
    +
    +### For developers
    +
    +The most significant change is the ability to include language submodes right
    +under `contains` instead of defining explicit named submodes in the main array:
    +
    +    contains: [
    +      'string',
    +      'number',
    +      {begin: '\\n', end: hljs.IMMEDIATE_RE}
    +    ]
    +
    +This is useful for auxiliary modes needed only in one place to define parsing.
    +Note that such modes often don't have `className` and hence won't generate a
    +separate `` in the resulting markup. This is similar in effect to
    +`noMarkup: true`. All existing languages have been refactored accordingly.
    +
    +Test file test.html has at last become a real test. Now it not only puts the
    +detected language name under the code snippet but also tests if it matches the
    +expected one. Test summary is displayed right above all language snippets.
    +
    +
    +## CDN
    +
    +Fine people at [Yandex][] agreed to host highlight.js on their big fast servers.
    +[Link up][l]!
    +
    +[yandex]: http://yandex.com/
    +[l]: http://softwaremaniacs.org/soft/highlight/en/download/
    +
    +
    +## Version 5.10 — "Paris".
    +
    +Though I'm on a vacation in Paris, I decided to release a new version with a
    +couple of small fixes:
    +
    +- Tomas Vitvar discovered that TAB replacement doesn't always work when used
    +  with custom markup in code
    +- SQL parsing is even more rigid now and doesn't step over SmallTalk in tests
    +
    +
    +## Version 5.9
    +
    +A long-awaited version is finally released.
    +
    +New languages:
    +
    +- Andrew Fedorov made a definition for Lua
    +- a long-time highlight.js contributor [Peter Leonov][pl] made a definition for
    +  Nginx config
    +- [Vladimir Moskva][vm] made a definition for TeX
    +
    +[pl]: http://kung-fu-tzu.ru/
    +[vm]: http://fulc.ru/
    +
    +Fixes for existing languages:
    +
    +- [Loren Segal][ls] reworked the Ruby definition and added highlighting for
    +  [YARD][] inline documentation
    +- the definition of SQL has become more solid and now it shouldn't be overly
    +  greedy when it comes to language detection
    +
    +[ls]: http://gnuu.org/
    +[yard]: http://yardoc.org/
    +
    +The highlighter has become more usable as a library allowing to do highlighting
    +from initialization code of JS frameworks and in ajax methods (see.
    +readme.eng.txt).
    +
    +Also this version drops support for the [WordPress][wp] plugin. Everyone is
    +welcome to [pick up its maintenance][p] if needed.
    +
    +[wp]: http://wordpress.org/
    +[p]: http://bazaar.launchpad.net/~isagalaev/+junk/highlight/annotate/342/src/wp_highlight.js.php
    +
    +
    +## Version 5.8
    +
    +- Jan Berkel has contributed a definition for Scala. +1 to hotness!
    +- All CSS-styles are rewritten to work only inside `
    ` tags to avoid
    +  conflicts with host site styles.
    +
    +
    +## Version 5.7.
    +
    +Fixed escaping of quotes in VBScript strings.
    +
    +
    +## Version 5.5
    +
    +This version brings a small change: now .ini-files allow digits, underscores and
    +square brackets in key names.
    +
    +
    +## Version 5.4
    +
    +Fixed small but upsetting bug in the packer which caused incorrect highlighting
    +of explicitly specified languages. Thanks to Andrew Fedorov for precise
    +diagnostics!
    +
    +
    +## Version 5.3
    +
    +The version to fulfil old promises.
    +
    +The most significant change is that highlight.js now preserves custom user
    +markup in code along with its own highlighting markup. This means that now it's
    +possible to use, say, links in code. Thanks to [Vladimir Dolzhenko][vd] for the
    +[initial proposal][1] and for making a proof-of-concept patch.
    +
    +Also in this version:
    +
    +- [Vasily Polovnyov][vp] has sent a GitHub-like style and has implemented
    +  support for CSS @-rules and Ruby symbols.
    +- Yura Zaripov has sent two styles: Brown Paper and School Book.
    +- Oleg Volchkov has sent a definition for [Parser 3][p3].
    +
    +[1]: http://softwaremaniacs.org/forum/highlightjs/6612/
    +[p3]: http://www.parser.ru/
    +[vp]: http://vasily.polovnyov.ru/
    +[vd]: http://dolzhenko.blogspot.com/
    +
    +
    +## Version 5.2
    +
    +- at last it's possible to replace indentation TABs with something sensible
    +  (e.g. 2 or 4 spaces)
    +- new keywords and built-ins for 1C by Sergey Baranov
    +- a couple of small fixes to Apache highlighting
    +
    +
    +## Version 5.1
    +
    +This is one of those nice version consisting entirely of new and shiny
    +contributions!
    +
    +- [Vladimir Ermakov][vooon] created highlighting for AVR Assembler
    +- [Ruslan Keba][rukeba] created highlighting for Apache config file. Also his
    +  original visual style for it is now available for all highlight.js languages
    +  under the name "Magula".
    +- [Shuen-Huei Guan][drake] (aka Drake) sent new keywords for RenderMan
    +  languages. Also thanks go to [Konstantin Evdokimenko][ke] for his advice on
    +  the matter.
    +
    +[vooon]: http://vehq.ru/about/
    +[rukeba]: http://rukeba.com/
    +[drake]: http://drakeguan.org/
    +[ke]: http://k-evdokimenko.moikrug.ru/
    +
    +
    +## Version 5.0
    +
    +The main change in the new major version of highlight.js is a mechanism for
    +packing several languages along with the library itself into a single compressed
    +file. Now sites using several languages will load considerably faster because
    +the library won't dynamically include additional files while loading.
    +
    +Also this version fixes a long-standing bug with Javascript highlighting that
    +couldn't distinguish between regular expressions and division operations.
    +
    +And as usually there were a couple of minor correctness fixes.
    +
    +Great thanks to all contributors! Keep using highlight.js.
    +
    +
    +## Version 4.3
    +
    +This version comes with two contributions from [Jason Diamond][jd]:
    +
    +- language definition for C# (yes! it was a long-missed thing!)
    +- Visual Studio-like highlighting style
    +
    +Plus there are a couple of minor bug fixes for parsing HTML and XML attributes.
    +
    +[jd]: http://jason.diamond.name/weblog/
    +
    +
    +## Version 4.2
    +
    +The biggest news is highlighting for Lisp, courtesy of Vasily Polovnyov. It's
    +somewhat experimental meaning that for highlighting "keywords" it doesn't use
    +any pre-defined set of a Lisp dialect. Instead it tries to highlight first word
    +in parentheses wherever it makes sense. I'd like to ask people programming in
    +Lisp to confirm if it's a good idea and send feedback to [the forum][f].
    +
    +Other changes:
    +
    +- Smalltalk was excluded from DEFAULT_LANGUAGES to save traffic
    +- [Vladimir Epifanov][voldmar] has implemented javascript style switcher for
    +  test.html
    +- comments now allowed inside Ruby function definition
    +- [MEL][] language from [Shuen-Huei Guan][drake]
    +- whitespace now allowed between `
    ` and ``
    +- better auto-detection of C++ and PHP
    +- HTML allows embedded VBScript (`<% .. %>`)
    +
    +[f]: http://softwaremaniacs.org/forum/highlightjs/
    +[voldmar]: http://voldmar.ya.ru/
    +[mel]: http://en.wikipedia.org/wiki/Maya_Embedded_Language
    +[drake]: http://drakeguan.org/
    +
    +
    +## Version 4.1
    +
    +Languages:
    +
    +- Bash from Vah
    +- DOS bat-files from Alexander Makarov (Sam)
    +- Diff files from Vasily Polovnyov
    +- Ini files from myself though initial idea was from Sam
    +
    +Styles:
    +
    +- Zenburn from Vladimir Epifanov, this is an imitation of a
    +  [well-known theme for Vim][zenburn].
    +- Ascetic from myself, as a realization of ideals of non-flashy highlighting:
    +  just one color in only three gradations :-)
    +
    +In other news. [One small bug][bug] was fixed, built-in keywords were added for
    +Python and C++ which improved auto-detection for the latter (it was shame that
    +[my wife's blog][alenacpp] had issues with it from time to time). And lastly
    +thanks go to Sam for getting rid of my stylistic comments in code that were
    +getting in the way of [JSMin][].
    +
    +[zenburn]: http://en.wikipedia.org/wiki/Zenburn
    +[alenacpp]: http://alenacpp.blogspot.com/
    +[bug]: http://softwaremaniacs.org/forum/viewtopic.php?id=1823
    +[jsmin]: http://code.google.com/p/jsmin-php/
    +
    +
    +## Version 4.0
    +
    +New major version is a result of vast refactoring and of many contributions.
    +
    +Visible new features:
    +
    +- Highlighting of embedded languages. Currently is implemented highlighting of
    +  Javascript and CSS inside HTML.
    +- Bundled 5 ready-made style themes!
    +
    +Invisible new features:
    +
    +- Highlight.js no longer pollutes global namespace. Only one object and one
    +  function for backward compatibility.
    +- Performance is further increased by about 15%.
    +
    +Changing of a major version number caused by a new format of language definition
    +files. If you use some third-party language files they should be updated.
    +
    +
    +## Version 3.5
    +
    +A very nice version in my opinion fixing a number of small bugs and slightly
    +increased speed in a couple of corner cases. Thanks to everybody who reports
    +bugs in he [forum][f] and by email!
    +
    +There is also a new language — XML. A custom XML formerly was detected as HTML
    +and didn't highlight custom tags. In this version I tried to make custom XML to
    +be detected and highlighted by its own rules. Which by the way include such
    +things as CDATA sections and processing instructions (``).
    +
    +[f]: http://softwaremaniacs.org/forum/viewforum.php?id=6
    +
    +
    +## Version 3.3
    +
    +[Vladimir Gubarkov][xonix] has provided an interesting and useful addition.
    +File export.html contains a little program that shows and allows to copy and
    +paste an HTML code generated by the highlighter for any code snippet. This can
    +be useful in situations when one can't use the script itself on a site.
    +
    +
    +[xonix]: http://xonixx.blogspot.com/
    +
    +
    +## Version 3.2 consists completely of contributions:
    +
    +- Vladimir Gubarkov has described SmallTalk
    +- Yuri Ivanov has described 1C
    +- Peter Leonov has packaged the highlighter as a Firefox extension
    +- Vladimir Ermakov has compiled a mod for phpBB
    +
    +Many thanks to you all!
    +
    +
    +## Version 3.1
    +
    +Three new languages are available: Django templates, SQL and Axapta. The latter
    +two are sent by [Dmitri Roudakov][1]. However I've almost entirely rewrote an
    +SQL definition but I'd never started it be it from the ground up :-)
    +
    +The engine itself has got a long awaited feature of grouping keywords
    +("keyword", "built-in function", "literal"). No more hacks!
    +
    +[1]: http://roudakov.ru/
    +
    +
    +## Version 3.0
    +
    +It is major mainly because now highlight.js has grown large and has become
    +modular. Now when you pass it a list of languages to highlight it will
    +dynamically load into a browser only those languages.
    +
    +Also:
    +
    +- Konstantin Evdokimenko of [RibKit][] project has created a highlighting for
    +  RenderMan Shading Language and RenderMan Interface Bytestream. Yay for more
    +  languages!
    +- Heuristics for C++ and HTML got better.
    +- I've implemented (at last) a correct handling of backslash escapes in C-like
    +  languages.
    +
    +There is also a small backwards incompatible change in the new version. The
    +function initHighlighting that was used to initialize highlighting instead of
    +initHighlightingOnLoad a long time ago no longer works. If you by chance still
    +use it — replace it with the new one.
    +
    +[RibKit]: http://ribkit.sourceforge.net/
    +
    +
    +## Version 2.9
    +
    +Highlight.js is a parser, not just a couple of regular expressions. That said
    +I'm glad to announce that in the new version 2.9 has support for:
    +
    +- in-string substitutions for Ruby -- `#{...}`
    +- strings from from numeric symbol codes (like #XX) for Delphi
    +
    +
    +## Version 2.8
    +
    +A maintenance release with more tuned heuristics. Fully backwards compatible.
    +
    +
    +## Version 2.7
    +
    +- Nikita Ledyaev presents highlighting for VBScript, yay!
    +- A couple of bugs with escaping in strings were fixed thanks to Mickle
    +- Ongoing tuning of heuristics
    +
    +Fixed bugs were rather unpleasant so I encourage everyone to upgrade!
    +
    +
    +## Version 2.4
    +
    +- Peter Leonov provides another improved highlighting for Perl
    +- Javascript gets a new kind of keywords — "literals". These are the words
    +  "true", "false" and "null"
    +
    +Also highlight.js homepage now lists sites that use the library. Feel free to
    +add your site by [dropping me a message][mail] until I find the time to build a
    +submit form.
    +
    +[mail]: mailto:Maniac@SoftwareManiacs.Org
    +
    +
    +## Version 2.3
    +
    +This version fixes IE breakage in previous version. My apologies to all who have
    +already downloaded that one!
    +
    +
    +## Version 2.2
    +
    +- added highlighting for Javascript
    +- at last fixed parsing of Delphi's escaped apostrophes in strings
    +- in Ruby fixed highlighting of keywords 'def' and 'class', same for 'sub' in
    +  Perl
    +
    +
    +## Version 2.0
    +
    +- Ruby support by [Anton Kovalyov][ak]
    +- speed increased by orders of magnitude due to new way of parsing
    +- this same way allows now correct highlighting of keywords in some tricky
    +  places (like keyword "End" at the end of Delphi classes)
    +
    +[ak]: http://anton.kovalyov.net/
    +
    +
    +## Version 1.0
    +
    +Version 1.0 of javascript syntax highlighter is released!
    +
    +It's the first version available with English description. Feel free to post
    +your comments and question to [highlight.js forum][forum]. And don't be afraid
    +if you find there some fancy Cyrillic letters -- it's for Russian users too :-)
    +
    +[forum]: http://softwaremaniacs.org/forum/viewforum.php?id=6
    diff --git a/vendor/highlight/LICENSE b/vendor/highlight/LICENSE
    new file mode 100644
    index 00000000..422deb73
    --- /dev/null
    +++ b/vendor/highlight/LICENSE
    @@ -0,0 +1,24 @@
    +Copyright (c) 2006, Ivan Sagalaev
    +All rights reserved.
    +Redistribution and use in source and binary forms, with or without
    +modification, are permitted provided that the following conditions are met:
    +
    +    * Redistributions of source code must retain the above copyright
    +      notice, this list of conditions and the following disclaimer.
    +    * Redistributions in binary form must reproduce the above copyright
    +      notice, this list of conditions and the following disclaimer in the
    +      documentation and/or other materials provided with the distribution.
    +    * Neither the name of highlight.js nor the names of its contributors 
    +      may be used to endorse or promote products derived from this software 
    +      without specific prior written permission.
    +
    +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
    +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    +DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
    +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    diff --git a/vendor/highlight/README.md b/vendor/highlight/README.md
    new file mode 100644
    index 00000000..4c287ae2
    --- /dev/null
    +++ b/vendor/highlight/README.md
    @@ -0,0 +1,188 @@
    +# Highlight.js
    +
    +[![Build Status](https://travis-ci.org/highlightjs/highlight.js.svg?branch=master)](https://travis-ci.org/highlightjs/highlight.js) [![Greenkeeper badge](https://badges.greenkeeper.io/highlightjs/highlight.js.svg)](https://greenkeeper.io/)
    +
    +Highlight.js is a syntax highlighter written in JavaScript. It works in
    +the browser as well as on the server. It works with pretty much any
    +markup, doesn’t depend on any framework, and has automatic language
    +detection.
    +
    +## Getting Started
    +
    +The bare minimum for using highlight.js on a web page is linking to the
    +library along with one of the styles and calling
    +[`initHighlightingOnLoad`][1]:
    +
    +```html
    +
    +
    +
    +```
    +
    +This will find and highlight code inside of `
    ` tags; it tries
    +to detect the language automatically. If automatic detection doesn’t
    +work for you, you can specify the language in the `class` attribute:
    +
    +```html
    +
    ...
    +``` + +The list of supported language classes is available in the [class +reference][2]. Classes can also be prefixed with either `language-` or +`lang-`. + +To make arbitrary text look like code, but without highlighting, use the +`plaintext` class: + +```html +
    ...
    +``` + +To disable highlighting altogether use the `nohighlight` class: + +```html +
    ...
    +``` + +## Custom Initialization + +When you need a bit more control over the initialization of +highlight.js, you can use the [`highlightBlock`][3] and [`configure`][4] +functions. This allows you to control *what* to highlight and *when*. + +Here’s an equivalent way to calling [`initHighlightingOnLoad`][1] using +vanilla JS: + +```js +document.addEventListener('DOMContentLoaded', (event) => { + document.querySelectorAll('pre code').forEach((block) => { + hljs.highlightBlock(block); + }); +}); +``` + +You can use any tags instead of `
    ` to mark up your code. If
    +you don't use a container that preserves line breaks you will need to
    +configure highlight.js to use the `
    ` tag: + +```js +hljs.configure({useBR: true}); + +document.querySelectorAll('div.code').forEach((block) => { + hljs.highlightBlock(block); +}); +``` + +For other options refer to the documentation for [`configure`][4]. + + +## Web Workers + +You can run highlighting inside a web worker to avoid freezing the browser +window while dealing with very big chunks of code. + +In your main script: + +```js +addEventListener('load', () => { + const code = document.querySelector('#code'); + const worker = new Worker('worker.js'); + worker.onmessage = (event) => { code.innerHTML = event.data; } + worker.postMessage(code.textContent); +}); +``` + +In worker.js: + +```js +onmessage = (event) => { + importScripts('/highlight.pack.js'); + const result = self.hljs.highlightAuto(event.data); + postMessage(result.value); +}; +``` + + +## Getting the Library + +You can get highlight.js as a hosted, or custom-build, browser script or +as a server module. Right out of the box the browser script supports +both AMD and CommonJS, so if you wish you can use RequireJS or +Browserify without having to build from source. The server module also +works perfectly fine with Browserify, but there is the option to use a +build specific to browsers rather than something meant for a server. +Head over to the [download page][5] for all the options. + +**Don't link to GitHub directly.** The library is not supposed to work straight +from the source, it requires building. If none of the pre-packaged options +work for you refer to the [building documentation][6]. + +**The CDN-hosted package doesn't have all the languages.** Otherwise it'd be +too big. If you don't see the language you need in the ["Common" section][5], +it can be added manually: + +```html + +``` + +**On Almond.** You need to use the optimizer to give the module a name. For +example: + +```bash +r.js -o name=hljs paths.hljs=/path/to/highlight out=highlight.js +``` + + +### CommonJS + +You can import Highlight.js as a CommonJS-module: + +```bash +npm install highlight.js --save +``` + +In your application: + +```js +import hljs from 'highlight.js'; +``` + +The default import imports all languages! Therefore it is likely to be more efficient to import only the library and the languages you need: + +```js +import hljs from 'highlight.js/lib/highlight'; +import javascript from 'highlight.js/lib/languages/javascript'; +hljs.registerLanguage('javascript', javascript); +``` + +To set the syntax highlighting style, if your build tool processes CSS from your JavaScript entry point, you can import the stylesheet directly into your CommonJS-module: + +```js +import hljs from 'highlight.js/lib/highlight'; +import 'highlight.js/styles/github.css'; +``` + +## License + +Highlight.js is released under the BSD License. See [LICENSE][7] file +for details. + +## Links + +The official site for the library is at . + +Further in-depth documentation for the API and other topics is at +. + +Authors and contributors are listed in the [AUTHORS.en.txt][8] file. + +[1]: http://highlightjs.readthedocs.io/en/latest/api.html#inithighlightingonload +[2]: http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html +[3]: http://highlightjs.readthedocs.io/en/latest/api.html#highlightblock-block +[4]: http://highlightjs.readthedocs.io/en/latest/api.html#configure-options +[5]: https://highlightjs.org/download/ +[6]: http://highlightjs.readthedocs.io/en/latest/building-testing.html +[7]: https://github.com/highlightjs/highlight.js/blob/master/LICENSE +[8]: https://github.com/highlightjs/highlight.js/blob/master/AUTHORS.en.txt diff --git a/vendor/highlight/README.ru.md b/vendor/highlight/README.ru.md new file mode 100644 index 00000000..198ee969 --- /dev/null +++ b/vendor/highlight/README.ru.md @@ -0,0 +1,142 @@ +# Highlight.js + +Highlight.js — это инструмент для подсветки синтаксиса, написанный на JavaScript. Он работает +и в браузере, и на сервере. Он работает с практически любой HTML разметкой, не +зависит от каких-либо фреймворков и умеет автоматически определять язык. + + +## Начало работы + +Минимум, что нужно сделать для использования highlight.js на веб-странице — это +подключить библиотеку, CSS-стили и вызывать [`initHighlightingOnLoad`][1]: + +```html + + + +``` + +Библиотека найдёт и раскрасит код внутри тегов `
    `, попытавшись
    +автоматически определить язык. Когда автоопределение не срабатывает, можно явно
    +указать язык в атрибуте class:
    +
    +```html
    +
    ...
    +``` + +Список поддерживаемых классов языков доступен в [справочнике по классам][2]. +Класс также можно предварить префиксами `language-` или `lang-`. + +Чтобы отключить подсветку для какого-то блока, используйте класс `nohighlight`: + +```html +
    ...
    +``` + +## Инициализация вручную + +Чтобы иметь чуть больше контроля за инициализацией подсветки, вы можете +использовать функции [`highlightBlock`][3] и [`configure`][4]. Таким образом +можно управлять тем, *что* и *когда* подсвечивать. + +Вот пример инициализации, эквивалентной вызову [`initHighlightingOnLoad`][1], но +с использованием `document.addEventListener`: + +```js +document.addEventListener('DOMContentLoaded', (event) => { + document.querySelectorAll('pre code').forEach((block) => { + hljs.highlightBlock(block); + }); +}); +``` + +Вы можете использовать любые теги разметки вместо `
    `. Если
    +используете контейнер, не сохраняющий переводы строк, вам нужно сказать
    +highlight.js использовать для них тег `
    `: + +```js +hljs.configure({useBR: true}); + +document.querySelectorAll('div.code').forEach((block) => { + hljs.highlightBlock(block); +}); +``` + +Другие опции можно найти в документации функции [`configure`][4]. + + +## Web Workers + +Подсветку можно запустить внутри web worker'а, чтобы окно +браузера не подтормаживало при работе с большими кусками кода. + +В основном скрипте: + +```js +addEventListener('load', () => { + const code = document.querySelector('#code'); + const worker = new Worker('worker.js'); + worker.onmessage = (event) => { code.innerHTML = event.data; } + worker.postMessage(code.textContent); +}); +``` + +В worker.js: + +```js +onmessage = (event) => { + importScripts('/highlight.pack.js'); + const result = self.hljs.highlightAuto(event.data); + postMessage(result.value); +}; +``` + + +## Установка библиотеки + +Highlight.js можно использовать в браузере прямо с CDN хостинга или скачать +индивидуальную сборку, а также установив модуль на сервере. На +[странице загрузки][5] подробно описаны все варианты. + +**Не подключайте GitHub напрямую.** Библиотека не предназначена для +использования в виде исходного кода, а требует отдельной сборки. Если вам не +подходит ни один из готовых вариантов, читайте [документацию по сборке][6]. + +**Файл на CDN содержит не все языки.** Иначе он будет слишком большого размера. +Если нужного вам языка нет в [категории "Common"][5], можно дообавить его +вручную: + +```html + +``` + +**Про Almond.** Нужно задать имя модуля в оптимизаторе, например: + +``` +r.js -o name=hljs paths.hljs=/path/to/highlight out=highlight.js +``` + + +## Лицензия + +Highlight.js распространяется под лицензией BSD. Подробнее читайте файл +[LICENSE][7]. + + +## Ссылки + +Официальный сайт билиотеки расположен по адресу . + +Более подробная документация по API и другим темам расположена на +. + +Авторы и контрибьюторы перечислены в файле [AUTHORS.ru.txt][8] file. + +[1]: http://highlightjs.readthedocs.io/en/latest/api.html#inithighlightingonload +[2]: http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html +[3]: http://highlightjs.readthedocs.io/en/latest/api.html#highlightblock-block +[4]: http://highlightjs.readthedocs.io/en/latest/api.html#configure-options +[5]: https://highlightjs.org/download/ +[6]: http://highlightjs.readthedocs.io/en/latest/building-testing.html +[7]: https://github.com/highlightjs/highlight.js/blob/master/LICENSE +[8]: https://github.com/highlightjs/highlight.js/blob/master/AUTHORS.ru.txt diff --git a/vendor/highlight/highlight.pack.js b/vendor/highlight/highlight.pack.js new file mode 100644 index 00000000..f845efcb --- /dev/null +++ b/vendor/highlight/highlight.pack.js @@ -0,0 +1,2 @@ +/*! highlight.js v9.15.9 | BSD3 License | git.io/hljslicense */ +!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"==typeof exports||exports.nodeType?n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs})):e(exports)}(function(a){var f=[],u=Object.keys,N={},c={},n=/^(no-?highlight|plain|text)$/i,s=/\blang(?:uage)?-([\w-]+)\b/i,t=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,r={case_insensitive:"cI",lexemes:"l",contains:"c",keywords:"k",subLanguage:"sL",className:"cN",begin:"b",beginKeywords:"bK",end:"e",endsWithParent:"eW",illegal:"i",excludeBegin:"eB",excludeEnd:"eE",returnBegin:"rB",returnEnd:"rE",relevance:"r",variants:"v",IDENT_RE:"IR",UNDERSCORE_IDENT_RE:"UIR",NUMBER_RE:"NR",C_NUMBER_RE:"CNR",BINARY_NUMBER_RE:"BNR",RE_STARTERS_RE:"RSR",BACKSLASH_ESCAPE:"BE",APOS_STRING_MODE:"ASM",QUOTE_STRING_MODE:"QSM",PHRASAL_WORDS_MODE:"PWM",C_LINE_COMMENT_MODE:"CLCM",C_BLOCK_COMMENT_MODE:"CBCM",HASH_COMMENT_MODE:"HCM",NUMBER_MODE:"NM",C_NUMBER_MODE:"CNM",BINARY_NUMBER_MODE:"BNM",CSS_NUMBER_MODE:"CSSNM",REGEXP_MODE:"RM",TITLE_MODE:"TM",UNDERSCORE_TITLE_MODE:"UTM",COMMENT:"C",beginRe:"bR",endRe:"eR",illegalRe:"iR",lexemesRe:"lR",terminators:"t",terminator_end:"tE"},b="",h={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};function _(e){return e.replace(/&/g,"&").replace(//g,">")}function E(e){return e.nodeName.toLowerCase()}function v(e,n){var t=e&&e.exec(n);return t&&0===t.index}function l(e){return n.test(e)}function g(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function R(e){var a=[];return function e(n,t){for(var r=n.firstChild;r;r=r.nextSibling)3===r.nodeType?t+=r.nodeValue.length:1===r.nodeType&&(a.push({event:"start",offset:t,node:r}),t=e(r,t),E(r).match(/br|hr|img|input/)||a.push({event:"stop",offset:t,node:r}));return t}(e,0),a}function i(e){if(r&&!e.langApiRestored){for(var n in e.langApiRestored=!0,r)e[n]&&(e[r[n]]=e[n]);(e.c||[]).concat(e.v||[]).forEach(i)}}function m(o){function s(e){return e&&e.source||e}function c(e,n){return new RegExp(s(e),"m"+(o.cI?"i":"")+(n?"g":""))}!function n(t,e){if(!t.compiled){if(t.compiled=!0,t.k=t.k||t.bK,t.k){function r(t,e){o.cI&&(e=e.toLowerCase()),e.split(" ").forEach(function(e){var n=e.split("|");a[n[0]]=[t,n[1]?Number(n[1]):1]})}var a={};"string"==typeof t.k?r("keyword",t.k):u(t.k).forEach(function(e){r(e,t.k[e])}),t.k=a}t.lR=c(t.l||/\w+/,!0),e&&(t.bK&&(t.b="\\b("+t.bK.split(" ").join("|")+")\\b"),t.b||(t.b=/\B|\b/),t.bR=c(t.b),t.endSameAsBegin&&(t.e=t.b),t.e||t.eW||(t.e=/\B|\b/),t.e&&(t.eR=c(t.e)),t.tE=s(t.e)||"",t.eW&&e.tE&&(t.tE+=(t.e?"|":"")+e.tE)),t.i&&(t.iR=c(t.i)),null==t.r&&(t.r=1),t.c||(t.c=[]),t.c=Array.prototype.concat.apply([],t.c.map(function(e){return function(n){return n.v&&!n.cached_variants&&(n.cached_variants=n.v.map(function(e){return g(n,{v:null},e)})),n.cached_variants||n.eW&&[g(n)]||[n]}("self"===e?t:e)})),t.c.forEach(function(e){n(e,t)}),t.starts&&n(t.starts,e);var i=t.c.map(function(e){return e.bK?"\\.?(?:"+e.b+")\\.?":e.b}).concat([t.tE,t.i]).map(s).filter(Boolean);t.t=i.length?c(function(e,n){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i')+n+(t?"":b):n}function o(){E+=null!=l.sL?function(){var e="string"==typeof l.sL;if(e&&!N[l.sL])return _(g);var n=e?C(l.sL,g,!0,f[l.sL]):O(g,l.sL.length?l.sL:void 0);return 0")+'"');return g+=n,n.length||1}var s=B(e);if(!s)throw new Error('Unknown language: "'+e+'"');m(s);var a,l=t||s,f={},E="";for(a=l;a!==s;a=a.parent)a.cN&&(E=c(a.cN,"",!0)+E);var g="",R=0;try{for(var d,p,M=0;l.t.lastIndex=M,d=l.t.exec(n);)p=r(n.substring(M,d.index),d[0]),M=d.index+p;for(r(n.substr(M)),a=l;a.parent;a=a.parent)a.cN&&(E+=b);return{r:R,value:E,language:e,top:l}}catch(e){if(e.message&&-1!==e.message.indexOf("Illegal"))return{r:0,value:_(n)};throw e}}function O(t,e){e=e||h.languages||u(N);var r={r:0,value:_(t)},a=r;return e.filter(B).filter(M).forEach(function(e){var n=C(e,t,!1);n.language=e,n.r>a.r&&(a=n),n.r>r.r&&(a=r,r=n)}),a.language&&(r.second_best=a),r}function d(e){return h.tabReplace||h.useBR?e.replace(t,function(e,n){return h.useBR&&"\n"===e?"
    ":h.tabReplace?n.replace(/\t/g,h.tabReplace):""}):e}function o(e){var n,t,r,a,i,o=function(e){var n,t,r,a,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",t=s.exec(i))return B(t[1])?t[1]:"no-highlight";for(n=0,r=(i=i.split(/\s+/)).length;n/g,"\n"):n=e,i=n.textContent,r=o?C(o,i,!0):O(i),(t=R(n)).length&&((a=document.createElementNS("http://www.w3.org/1999/xhtml","div")).innerHTML=r.value,r.value=function(e,n,t){var r=0,a="",i=[];function o(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function u(e){a+=""}function s(e){("start"===e.event?c:u)(e.node)}for(;e.length||n.length;){var l=o();if(a+=_(t.substring(r,l[0].offset)),r=l[0].offset,l===e){for(i.reverse().forEach(u);s(l.splice(0,1)[0]),(l=o())===e&&l.length&&l[0].offset===r;);i.reverse().forEach(c)}else"start"===l[0].event?i.push(l[0].node):i.pop(),s(l.splice(0,1)[0])}return a+_(t.substr(r))}(t,R(a),i)),r.value=d(r.value),e.innerHTML=r.value,e.className=function(e,n,t){var r=n?c[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}(e.className,o,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function p(){if(!p.called){p.called=!0;var e=document.querySelectorAll("pre code");f.forEach.call(e,o)}}function B(e){return e=(e||"").toLowerCase(),N[e]||N[c[e]]}function M(e){var n=B(e);return n&&!n.disableAutodetect}return a.highlight=C,a.highlightAuto=O,a.fixMarkup=d,a.highlightBlock=o,a.configure=function(e){h=g(h,e)},a.initHighlighting=p,a.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",p,!1),addEventListener("load",p,!1)},a.registerLanguage=function(n,e){var t=N[n]=e(a);i(t),t.aliases&&t.aliases.forEach(function(e){c[e]=n})},a.listLanguages=function(){return u(N)},a.getLanguage=B,a.autoDetection=M,a.inherit=g,a.IR=a.IDENT_RE="[a-zA-Z]\\w*",a.UIR=a.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*",a.NR=a.NUMBER_RE="\\b\\d+(\\.\\d+)?",a.CNR=a.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",a.BNR=a.BINARY_NUMBER_RE="\\b(0b[01]+)",a.RSR=a.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",a.BE=a.BACKSLASH_ESCAPE={b:"\\\\[\\s\\S]",r:0},a.ASM=a.APOS_STRING_MODE={cN:"string",b:"'",e:"'",i:"\\n",c:[a.BE]},a.QSM=a.QUOTE_STRING_MODE={cN:"string",b:'"',e:'"',i:"\\n",c:[a.BE]},a.PWM=a.PHRASAL_WORDS_MODE={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},a.C=a.COMMENT=function(e,n,t){var r=a.inherit({cN:"comment",b:e,e:n,c:[]},t||{});return r.c.push(a.PWM),r.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),r},a.CLCM=a.C_LINE_COMMENT_MODE=a.C("//","$"),a.CBCM=a.C_BLOCK_COMMENT_MODE=a.C("/\\*","\\*/"),a.HCM=a.HASH_COMMENT_MODE=a.C("#","$"),a.NM=a.NUMBER_MODE={cN:"number",b:a.NR,r:0},a.CNM=a.C_NUMBER_MODE={cN:"number",b:a.CNR,r:0},a.BNM=a.BINARY_NUMBER_MODE={cN:"number",b:a.BNR,r:0},a.CSSNM=a.CSS_NUMBER_MODE={cN:"number",b:a.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},a.RM=a.REGEXP_MODE={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[a.BE,{b:/\[/,e:/\]/,r:0,c:[a.BE]}]},a.TM=a.TITLE_MODE={cN:"title",b:a.IR,r:0},a.UTM=a.UNDERSCORE_TITLE_MODE={cN:"title",b:a.UIR,r:0},a.METHOD_GUARD={b:"\\.\\s*"+a.UIR,r:0},a});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("php",function(e){var c={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},i={cN:"meta",b:/<\?(php)?|\?>/},t={cN:"string",c:[e.BE,i],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:["php","php3","php4","php5","php6","php7"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.HCM,e.C("//","$",{c:[i]}),e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},i,{cN:"keyword",b:/\$this\b/},c,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",c,e.CBCM,t,a]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},t,a]}});hljs.registerLanguage("python",function(e){var r={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10",built_in:"Ellipsis NotImplemented",literal:"False None True"},b={cN:"meta",b:/^(>>>|\.\.\.) /},c={cN:"subst",b:/\{/,e:/\}/,k:r,i:/#/},a={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[e.BE,b],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[e.BE,b],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[e.BE,b,c]},{b:/(fr|rf|f)"""/,e:/"""/,c:[e.BE,b,c]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)'/,e:/'/,c:[e.BE,c]},{b:/(fr|rf|f)"/,e:/"/,c:[e.BE,c]},e.ASM,e.QSM]},i={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},l={cN:"params",b:/\(/,e:/\)/,c:["self",b,i,a]};return c.c=[a,i,b],{aliases:["py","gyp","ipython"],k:r,i:/(<\/|->|\?)|=>/,c:[b,i,a,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,l,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("properties",function(r){var t="[ \\t\\f]*",e="("+t+"[:=]"+t+"|[ \\t\\f]+)",s="([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",n="([^\\\\:= \\t\\f\\n]|\\\\.)+",a={e:e,r:0,starts:{cN:"string",e:/$/,r:0,c:[{b:"\\\\\\n"}]}};return{cI:!0,i:/\S/,c:[r.C("^\\s*[!#]","$"),{b:s+e,rB:!0,c:[{cN:"attr",b:s,endsParent:!0,r:0}],starts:a},{b:n+e,rB:!0,r:0,c:[{cN:"meta",b:n,endsParent:!0,r:0}],starts:a},{cN:"attr",r:0,b:n+t+"$"}]}});hljs.registerLanguage("brainfuck",function(r){var n={cN:"literal",b:"[\\+\\-]",r:0};return{aliases:["bf"],c:[r.C("[^\\[\\]\\.,\\+\\-<> \r\n]","[\\[\\]\\.,\\+\\-<> \r\n]",{rE:!0,r:0}),{cN:"title",b:"[\\[\\]]",r:0},{cN:"string",b:"[\\.,]",r:0},{b:/\+\+|\-\-/,rB:!0,c:[n]},n]}});hljs.registerLanguage("nginx",function(e){var r={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},b={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,r],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[r]},{cN:"regexp",c:[e.BE,r],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},r]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:b}],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("plaintext",function(e){return{disableAutodetect:!0}});hljs.registerLanguage("objectivec",function(e){var t=/[a-zA-Z@][a-zA-Z0-9_]*/,_="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},l:t,i:""}]}]},{cN:"class",b:"("+_.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:_,l:t,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,{cN:"",b:/\\"/},{cN:"string",b:/'/,e:/'/},t]}});hljs.registerLanguage("shell",function(s){return{aliases:["console"],c:[{cN:"meta",b:"^\\s{0,3}[\\w\\d\\[\\]()@-]*[>%$#]",starts:{e:"$",sL:"bash"}}]}});hljs.registerLanguage("avrasm",function(r){return{cI:!0,l:"\\.?"+r.IR,k:{keyword:"adc add adiw and andi asr bclr bld brbc brbs brcc brcs break breq brge brhc brhs brid brie brlo brlt brmi brne brpl brsh brtc brts brvc brvs bset bst call cbi cbr clc clh cli cln clr cls clt clv clz com cp cpc cpi cpse dec eicall eijmp elpm eor fmul fmuls fmulsu icall ijmp in inc jmp ld ldd ldi lds lpm lsl lsr mov movw mul muls mulsu neg nop or ori out pop push rcall ret reti rjmp rol ror sbc sbr sbrc sbrs sec seh sbi sbci sbic sbis sbiw sei sen ser ses set sev sez sleep spm st std sts sub subi swap tst wdr",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 r16 r17 r18 r19 r20 r21 r22 r23 r24 r25 r26 r27 r28 r29 r30 r31 x|0 xh xl y|0 yh yl z|0 zh zl ucsr1c udr1 ucsr1a ucsr1b ubrr1l ubrr1h ucsr0c ubrr0h tccr3c tccr3a tccr3b tcnt3h tcnt3l ocr3ah ocr3al ocr3bh ocr3bl ocr3ch ocr3cl icr3h icr3l etimsk etifr tccr1c ocr1ch ocr1cl twcr twdr twar twsr twbr osccal xmcra xmcrb eicra spmcsr spmcr portg ddrg ping portf ddrf sreg sph spl xdiv rampz eicrb eimsk gimsk gicr eifr gifr timsk tifr mcucr mcucsr tccr0 tcnt0 ocr0 assr tccr1a tccr1b tcnt1h tcnt1l ocr1ah ocr1al ocr1bh ocr1bl icr1h icr1l tccr2 tcnt2 ocr2 ocdr wdtcr sfior eearh eearl eedr eecr porta ddra pina portb ddrb pinb portc ddrc pinc portd ddrd pind spdr spsr spcr udr0 ucsr0a ucsr0b ubrr0l acsr admux adcsr adch adcl porte ddre pine pinf",meta:".byte .cseg .db .def .device .dseg .dw .endmacro .equ .eseg .exit .include .list .listmac .macro .nolist .org .set"},c:[r.CBCM,r.C(";","$",{r:0}),r.CNM,r.BNM,{cN:"number",b:"\\b(\\$[a-zA-Z0-9]+|0o[0-7]+)"},r.QSM,{cN:"string",b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"},{cN:"symbol",b:"^[A-Za-z0-9_.$]+:"},{cN:"meta",b:"#",e:"$"},{cN:"subst",b:"@[0-9]+"}]}});hljs.registerLanguage("r",function(e){var r="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{c:[e.HCM,{b:r,l:r,k:{keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},r:0},{cN:"number",b:"0[xX][0-9a-fA-F]+[Li]?\\b",r:0},{cN:"number",b:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",r:0},{cN:"number",b:"\\d+\\.(?!\\d)(?:i\\b)?",r:0},{cN:"number",b:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{cN:"number",b:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{b:"`",e:"`",r:0},{cN:"string",c:[e.BE],v:[{b:'"',e:'"'},{b:"'",e:"'"}]}]}});hljs.registerLanguage("ruby",function(e){var b="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{b:/<<(-?)\w+$/,e:/^\s*\w+$/}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:b}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:b}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+"|unless)\\s*",k:"unless",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d;var l=[{b:/^\s*=>/,starts:{e:"$",c:i.c=d}},{cN:"meta",b:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:s.concat(l).concat(d)}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},i=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+n},{sL:"javascript",eB:!0,eE:!0,v:[{b:"```",e:"```"},{b:"`",e:"`"}]}];r.c=i;var s=e.inherit(e.TM,{b:n}),t="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(i)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:i.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+t,e:"[-=]>",rB:!0,c:[s,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:t,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[s]},s]},{b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("haskell",function(e){var i={v:[e.C("--","$"),e.C("{-","-}",{c:["self"]})]},a={cN:"meta",b:"{-#",e:"#-}"},l={cN:"meta",b:"^#",e:"$"},c={cN:"type",b:"\\b[A-Z][\\w']*",r:0},n={b:"\\(",e:"\\)",i:'"',c:[a,l,{cN:"type",b:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TM,{b:"[_a-z][\\w']*"}),i]};return{aliases:["hs"],k:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",c:[{bK:"module",e:"where",k:"module where",c:[n,i],i:"\\W\\.|;"},{b:"\\bimport\\b",e:"$",k:"import qualified as hiding",c:[n,i],i:"\\W\\.|;"},{cN:"class",b:"^(\\s*)?(class|instance)\\b",e:"where",k:"class family instance where",c:[c,n,i]},{cN:"class",b:"\\b(data|(new)?type)\\b",e:"$",k:"data family type newtype deriving",c:[a,c,n,{b:"{",e:"}",c:n.c},i]},{bK:"default",e:"$",c:[c,n,i]},{bK:"infix infixl infixr",e:"$",c:[e.CNM,i]},{b:"\\bforeign\\b",e:"$",k:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",c:[c,e.QSM,i]},{cN:"meta",b:"#!\\/usr\\/bin\\/env runhaskell",e:"$"},a,l,e.QSM,e.CNM,c,e.inherit(e.TM,{b:"^[_a-z][\\w']*"}),i,{b:"->|<-"}]}});hljs.registerLanguage("typescript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"},n={cN:"meta",b:"@"+r},a={b:"\\(",e:/\)/,k:t,c:["self",e.QSM,e.ASM,e.NM]},o={cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:[e.CLCM,e.CBCM,n,a]};return{aliases:["ts"],k:t,c:[{cN:"meta",b:/^\s*['"]use strict['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+e.IR+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:e.IR},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:["self",e.CLCM,e.CBCM]}]}]}],r:0},{cN:"function",b:"function",e:/[\{;]/,eE:!0,k:t,c:["self",e.inherit(e.TM,{b:r}),o],i:/%/,r:0},{bK:"constructor",e:/\{/,eE:!0,c:["self",o]},{b:/module\./,k:{built_in:"module"},r:0},{bK:"module",e:/\{/,eE:!0},{bK:"interface",e:/\{/,eE:!0,k:"interface extends"},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},n,a]}});hljs.registerLanguage("css",function(e){var c={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:"[a-zA-Z-][a-zA-Z0-9_-]*",r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,c]}]}});hljs.registerLanguage("cpp",function(t){var e={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[{b:'(u8?|U|L)?"',e:'"',i:"\\n",c:[t.BE]},{b:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\((?:.|\n)*?\)\1"/},{b:"'\\\\?.",e:"'",i:"."}]},s={cN:"number",v:[{b:"\\b(0b[01']+)"},{b:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],r:0},i={cN:"meta",b:/#\s*[a-z]+\b/,e:/$/,k:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},c:[{b:/\\\n/,r:0},t.inherit(r,{cN:"meta-string"}),{cN:"meta-string",b:/<[^\n>]*>/,e:/$/,i:"\\n"},t.CLCM,t.CBCM]},a=t.IR+"\\s*\\(",c={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",literal:"true false nullptr NULL"},n=[e,t.CLCM,t.CBCM,s,r];return{aliases:["c","cc","h","c++","h++","hpp","hh","hxx","cxx"],k:c,i:"",k:c,c:["self",e]},{b:t.IR+"::",k:c},{v:[{b:/=/,e:/;/},{b:/\(/,e:/\)/},{bK:"new throw return else",e:/;/}],k:c,c:n.concat([{b:/\(/,e:/\)/,k:c,c:n.concat(["self"]),r:0}]),r:0},{cN:"function",b:"("+t.IR+"[\\*&\\s]+)+"+a,rB:!0,e:/[{;=]/,eE:!0,k:c,i:/[^\w\s\*&]/,c:[{b:a,rB:!0,c:[t.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:c,r:0,c:[t.CLCM,t.CBCM,r,s,e,{b:/\(/,e:/\)/,k:c,r:0,c:["self",t.CLCM,t.CBCM,r,s,e]}]},t.CLCM,t.CBCM,i]},{cN:"class",bK:"class struct",e:/[{;:]/,c:[{b://,c:["self"]},t.TM]}]),exports:{preprocessor:i,strings:r,k:c}}});hljs.registerLanguage("arduino",function(e){var t=e.getLanguage("cpp").exports;return{k:{keyword:"boolean byte word string String array "+t.k.keyword,built_in:"setup loop while catch for if do goto try switch case else default break continue return KeyboardController MouseController SoftwareSerial EthernetServer EthernetClient LiquidCrystal RobotControl GSMVoiceCall EthernetUDP EsploraTFT HttpClient RobotMotor WiFiClient GSMScanner FileSystem Scheduler GSMServer YunClient YunServer IPAddress GSMClient GSMModem Keyboard Ethernet Console GSMBand Esplora Stepper Process WiFiUDP GSM_SMS Mailbox USBHost Firmata PImage Client Server GSMPIN FileIO Bridge Serial EEPROM Stream Mouse Audio Servo File Task GPRS WiFi Wire TFT GSM SPI SD runShellCommandAsynchronously analogWriteResolution retrieveCallingNumber printFirmwareVersion analogReadResolution sendDigitalPortPair noListenOnLocalhost readJoystickButton setFirmwareVersion readJoystickSwitch scrollDisplayRight getVoiceCallStatus scrollDisplayLeft writeMicroseconds delayMicroseconds beginTransmission getSignalStrength runAsynchronously getAsynchronously listenOnLocalhost getCurrentCarrier readAccelerometer messageAvailable sendDigitalPorts lineFollowConfig countryNameWrite runShellCommand readStringUntil rewindDirectory readTemperature setClockDivider readLightSensor endTransmission analogReference detachInterrupt countryNameRead attachInterrupt encryptionType readBytesUntil robotNameWrite readMicrophone robotNameRead cityNameWrite userNameWrite readJoystickY readJoystickX mouseReleased openNextFile scanNetworks noInterrupts digitalWrite beginSpeaker mousePressed isActionDone mouseDragged displayLogos noAutoscroll addParameter remoteNumber getModifiers keyboardRead userNameRead waitContinue processInput parseCommand printVersion readNetworks writeMessage blinkVersion cityNameRead readMessage setDataMode parsePacket isListening setBitOrder beginPacket isDirectory motorsWrite drawCompass digitalRead clearScreen serialEvent rightToLeft setTextSize leftToRight requestFrom keyReleased compassRead analogWrite interrupts WiFiServer disconnect playMelody parseFloat autoscroll getPINUsed setPINUsed setTimeout sendAnalog readSlider analogRead beginWrite createChar motorsStop keyPressed tempoWrite readButton subnetMask debugPrint macAddress writeGreen randomSeed attachGPRS readString sendString remotePort releaseAll mouseMoved background getXChange getYChange answerCall getResult voiceCall endPacket constrain getSocket writeJSON getButton available connected findUntil readBytes exitValue readGreen writeBlue startLoop IPAddress isPressed sendSysex pauseMode gatewayIP setCursor getOemKey tuneWrite noDisplay loadImage switchPIN onRequest onReceive changePIN playFile noBuffer parseInt overflow checkPIN knobRead beginTFT bitClear updateIR bitWrite position writeRGB highByte writeRed setSpeed readBlue noStroke remoteIP transfer shutdown hangCall beginSMS endWrite attached maintain noCursor checkReg checkPUK shiftOut isValid shiftIn pulseIn connect println localIP pinMode getIMEI display noBlink process getBand running beginSD drawBMP lowByte setBand release bitRead prepare pointTo readRed setMode noFill remove listen stroke detach attach noTone exists buffer height bitSet circle config cursor random IRread setDNS endSMS getKey micros millis begin print write ready flush width isPIN blink clear press mkdir rmdir close point yield image BSSID click delay read text move peek beep rect line open seek fill size turn stop home find step tone sqrt RSSI SSID end bit tan cos sin pow map abs max min get run put",literal:"DIGITAL_MESSAGE FIRMATA_STRING ANALOG_MESSAGE REPORT_DIGITAL REPORT_ANALOG INPUT_PULLUP SET_PIN_MODE INTERNAL2V56 SYSTEM_RESET LED_BUILTIN INTERNAL1V1 SYSEX_START INTERNAL EXTERNAL DEFAULT OUTPUT INPUT HIGH LOW"},c:[t.preprocessor,e.CLCM,e.CBCM,e.ASM,e.QSM,e.CNM]}});hljs.registerLanguage("go",function(e){var t={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],k:t,i:"{}*]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t,e.HCM]},e.CBCM,t,e.HCM]}});hljs.registerLanguage("actionscript",function(e){var a={cN:"rest_arg",b:"[.]{3}",e:"[a-zA-Z_$][a-zA-Z0-9_$]*",r:10};return{aliases:["as"],k:{keyword:"as break case catch class const continue default delete do dynamic each else extends final finally for function get if implements import in include instanceof interface internal is namespace native new override package private protected public return set static super switch this throw try typeof use var void while with",literal:"true false null undefined"},c:[e.ASM,e.QSM,e.CLCM,e.CBCM,e.CNM,{cN:"class",bK:"package",e:"{",c:[e.TM]},{cN:"class",bK:"class interface",e:"{",eE:!0,c:[{bK:"extends implements"},e.TM]},{cN:"meta",bK:"import include",e:";",k:{"meta-keyword":"import include"}},{cN:"function",bK:"function",e:"[{;]",eE:!0,i:"\\S",c:[e.TM,{cN:"params",b:"\\(",e:"\\)",c:[e.ASM,e.QSM,e.CLCM,e.CBCM,a]},{b:":\\s*([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)"}]},e.METHOD_GUARD],i:/#/}});hljs.registerLanguage("apache",function(e){var r={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:!0,c:[e.HCM,{cN:"section",b:""},{cN:"attribute",b:/\w+/,r:0,k:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"meta",b:"\\s\\[",e:"\\]$"},{cN:"variable",b:"[\\$%]\\{",e:"\\}",c:["self",r]},r,e.QSM]}}],i:/\S/}});hljs.registerLanguage("ini",function(e){var b={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_\.-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_\.-]+/},{b:/=/,eW:!0,r:0,c:[e.C(";","$"),e.HCM,{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},b,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}});hljs.registerLanguage("xml",function(s){var e={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("\x3c!--","--\x3e",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"meta",b:/<\?xml/,e:/\?>/,r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0},{b:'b"',e:'"',skip:!0},{b:"b'",e:"'",skip:!0},s.inherit(s.ASM,{i:null,cN:null,c:null,skip:!0}),s.inherit(s.QSM,{i:null,cN:null,c:null,skip:!0})]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[e],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[e],starts:{e:"<\/script>",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},e]}]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^\\s*([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}|\t)",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}});hljs.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},s={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],o=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),s,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=o,{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:s.c=o}});hljs.registerLanguage("cmake",function(e){return{aliases:["cmake.in"],cI:!0,k:{keyword:"break cmake_host_system_information cmake_minimum_required cmake_parse_arguments cmake_policy configure_file continue elseif else endforeach endfunction endif endmacro endwhile execute_process file find_file find_library find_package find_path find_program foreach function get_cmake_property get_directory_property get_filename_component get_property if include include_guard list macro mark_as_advanced math message option return separate_arguments set_directory_properties set_property set site_name string unset variable_watch while add_compile_definitions add_compile_options add_custom_command add_custom_target add_definitions add_dependencies add_executable add_library add_link_options add_subdirectory add_test aux_source_directory build_command create_test_sourcelist define_property enable_language enable_testing export fltk_wrap_ui get_source_file_property get_target_property get_test_property include_directories include_external_msproject include_regular_expression install link_directories link_libraries load_cache project qt_wrap_cpp qt_wrap_ui remove_definitions set_source_files_properties set_target_properties set_tests_properties source_group target_compile_definitions target_compile_features target_compile_options target_include_directories target_link_directories target_link_libraries target_link_options target_sources try_compile try_run ctest_build ctest_configure ctest_coverage ctest_empty_binary_directory ctest_memcheck ctest_read_custom_files ctest_run_script ctest_sleep ctest_start ctest_submit ctest_test ctest_update ctest_upload build_name exec_program export_library_dependencies install_files install_programs install_targets load_command make_directory output_required_files remove subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file qt5_use_modules qt5_use_package qt5_wrap_cpp on off true false and or not command policy target test exists is_newer_than is_directory is_symlink is_absolute matches less greater equal less_equal greater_equal strless strgreater strequal strless_equal strgreater_equal version_less version_greater version_equal version_less_equal version_greater_equal in_list defined"},c:[{cN:"variable",b:"\\${",e:"}"},e.HCM,e.QSM,e.NM]}});hljs.registerLanguage("cs",function(e){var i={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",literal:"null false true"},r={cN:"number",v:[{b:"\\b(0b[01']+)"},{b:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],r:0},t={cN:"string",b:'@"',e:'"',c:[{b:'""'}]},a=e.inherit(t,{i:/\n/}),c={cN:"subst",b:"{",e:"}",k:i},n=e.inherit(c,{i:/\n/}),s={cN:"string",b:/\$"/,e:'"',i:/\n/,c:[{b:"{{"},{b:"}}"},e.BE,n]},b={cN:"string",b:/\$@"/,e:'"',c:[{b:"{{"},{b:"}}"},{b:'""'},c]},l=e.inherit(b,{i:/\n/,c:[{b:"{{"},{b:"}}"},{b:'""'},n]});c.c=[b,s,t,e.ASM,e.QSM,r,e.CBCM],n.c=[l,s,a,e.ASM,e.QSM,r,e.inherit(e.CBCM,{i:/\n/})];var o={v:[b,s,t,e.ASM,e.QSM]},d=e.IR+"(<"+e.IR+"(\\s*,\\s*"+e.IR+")*>)?(\\[\\])?";return{aliases:["csharp","c#"],k:i,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:"\x3c!--|--\x3e"},{b:""}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},o,r,{bK:"class interface",e:/[{;=]/,i:/[^\s:,]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{cN:"meta",b:"^\\s*\\[",eB:!0,e:"\\]",eE:!0,c:[{cN:"meta-string",b:/"/,e:/"/}]},{bK:"new return throw await else",r:0},{cN:"function",b:"("+d+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/\s*[{;=]/,eE:!0,k:i,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:i,r:0,c:[o,r,e.CBCM]},e.CLCM,e.CBCM]}]}});hljs.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\-{3}/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+{3}/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",a={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},t={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:a,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,t,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:a,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,t,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:a,c:s}]}]},{cN:"",b:/\s/,e:/\s*/,skip:!0},{b://,sL:"xml",c:[{b:/<[A-Za-z0-9\\._:-]+\s*\/>/,skip:!0},{b:/<[A-Za-z0-9\\._:-]+/,e:/(\/[A-Za-z0-9\\._:-]+|[A-Za-z0-9\\._:-]+\/)>/,skip:!0,c:[{b:/<[A-Za-z0-9\\._:-]+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor get set",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("java",function(e){var a="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",t={cN:"number",b:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",r:0};return{aliases:["jsp"],k:a,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:a,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:a,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},t,{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("makefile",function(e){var i={cN:"variable",v:[{b:"\\$\\("+e.UIR+"\\)",c:[e.BE]},{b:/\$[@% + * ---------------------------------------------------- + * + * #ade5fc + * #a2fca2 + * #c6b4f0 + * #d36363 + * #fcc28c + * #fc9b9b + * #ffa + * #fff + * #333 + * #62c8f3 + * #888 + * + */ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #333; + color: white; +} + +.hljs-name, +.hljs-strong { + font-weight: bold; +} + +.hljs-code, +.hljs-emphasis { + font-style: italic; +} + +.hljs-tag { + color: #62c8f3; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-selector-id, +.hljs-selector-class { + color: #ade5fc; +} + +.hljs-string, +.hljs-bullet { + color: #a2fca2; +} + +.hljs-type, +.hljs-title, +.hljs-section, +.hljs-attribute, +.hljs-quote, +.hljs-built_in, +.hljs-builtin-name { + color: #ffa; +} + +.hljs-number, +.hljs-symbol, +.hljs-bullet { + color: #d36363; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal { + color: #fcc28c; +} + +.hljs-comment, +.hljs-deletion, +.hljs-code { + color: #888; +} + +.hljs-regexp, +.hljs-link { + color: #c6b4f0; +} + +.hljs-meta { + color: #fc9b9b; +} + +.hljs-deletion { + background-color: #fc9b9b; + color: #333; +} + +.hljs-addition { + background-color: #a2fca2; + color: #333; +} + +.hljs a { + color: inherit; +} + +.hljs a:focus, +.hljs a:hover { + color: inherit; + text-decoration: underline; +} diff --git a/vendor/highlight/styles/an-old-hope.css b/vendor/highlight/styles/an-old-hope.css new file mode 100644 index 00000000..a6d56f4b --- /dev/null +++ b/vendor/highlight/styles/an-old-hope.css @@ -0,0 +1,89 @@ +/* + +An Old Hope – Star Wars Syntax (c) Gustavo Costa +Original theme - Ocean Dark Theme – by https://github.com/gavsiu +Based on Jesse Leite's Atom syntax theme 'An Old Hope' – https://github.com/JesseLeite/an-old-hope-syntax-atom + +*/ + +/* Death Star Comment */ +.hljs-comment, +.hljs-quote +{ + color: #B6B18B; +} + +/* Darth Vader */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion +{ + color: #EB3C54; +} + +/* Threepio */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link +{ + color: #E7CE56; +} + +/* Luke Skywalker */ +.hljs-attribute +{ + color: #EE7C2B; +} + +/* Obi Wan Kenobi */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition +{ + color: #4FB4D7; +} + +/* Yoda */ +.hljs-title, +.hljs-section +{ + color: #78BB65; +} + +/* Mace Windu */ +.hljs-keyword, +.hljs-selector-tag +{ + color: #B45EA4; +} + +/* Millenium Falcon */ +.hljs +{ + display: block; + overflow-x: auto; + background: #1C1D21; + color: #c0c5ce; + padding: 0.5em; +} + +.hljs-emphasis +{ + font-style: italic; +} + +.hljs-strong +{ + font-weight: bold; +} diff --git a/vendor/highlight/styles/androidstudio.css b/vendor/highlight/styles/androidstudio.css new file mode 100644 index 00000000..bc8e473b --- /dev/null +++ b/vendor/highlight/styles/androidstudio.css @@ -0,0 +1,66 @@ +/* +Date: 24 Fev 2015 +Author: Pedro Oliveira +*/ + +.hljs { + color: #a9b7c6; + background: #282b2e; + display: block; + overflow-x: auto; + padding: 0.5em; +} + +.hljs-number, +.hljs-literal, +.hljs-symbol, +.hljs-bullet { + color: #6897BB; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-deletion { + color: #cc7832; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-link { + color: #629755; +} + +.hljs-comment, +.hljs-quote { + color: #808080; +} + +.hljs-meta { + color: #bbb529; +} + +.hljs-string, +.hljs-attribute, +.hljs-addition { + color: #6A8759; +} + +.hljs-section, +.hljs-title, +.hljs-type { + color: #ffc66d; +} + +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #e8bf6a; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/arduino-light.css b/vendor/highlight/styles/arduino-light.css new file mode 100644 index 00000000..4b8b7fd3 --- /dev/null +++ b/vendor/highlight/styles/arduino-light.css @@ -0,0 +1,88 @@ +/* + +Arduino® Light Theme - Stefania Mellai + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #FFFFFF; +} + +.hljs, +.hljs-subst { + color: #434f54; +} + +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-doctag, +.hljs-name { + color: #00979D; +} + +.hljs-built_in, +.hljs-literal, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #D35400; +} + +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #00979D; +} + +.hljs-type, +.hljs-string, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #005C5F; +} + +.hljs-title, +.hljs-section { + color: #880000; + font-weight: bold; +} + +.hljs-comment { + color: rgba(149,165,166,.8); +} + +.hljs-meta-keyword { + color: #728E00; +} + +.hljs-meta { + color: #728E00; + color: #434f54; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-function { + color: #728E00; +} + +.hljs-number { + color: #8A7B52; +} diff --git a/vendor/highlight/styles/arta.css b/vendor/highlight/styles/arta.css new file mode 100644 index 00000000..75ef3a9e --- /dev/null +++ b/vendor/highlight/styles/arta.css @@ -0,0 +1,73 @@ +/* +Date: 17.V.2011 +Author: pumbur +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #222; +} + +.hljs, +.hljs-subst { + color: #aaa; +} + +.hljs-section { + color: #fff; +} + +.hljs-comment, +.hljs-quote, +.hljs-meta { + color: #444; +} + +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-regexp { + color: #ffcc33; +} + +.hljs-number, +.hljs-addition { + color: #00cc66; +} + +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-template-variable, +.hljs-attribute, +.hljs-link { + color: #32aaee; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #6644aa; +} + +.hljs-title, +.hljs-variable, +.hljs-deletion, +.hljs-template-tag { + color: #bb1166; +} + +.hljs-section, +.hljs-doctag, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/ascetic.css b/vendor/highlight/styles/ascetic.css new file mode 100644 index 00000000..48397e88 --- /dev/null +++ b/vendor/highlight/styles/ascetic.css @@ -0,0 +1,45 @@ +/* + +Original style from softwaremaniacs.org (c) Ivan Sagalaev + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: white; + color: black; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-symbol, +.hljs-bullet, +.hljs-section, +.hljs-addition, +.hljs-attribute, +.hljs-link { + color: #888; +} + +.hljs-comment, +.hljs-quote, +.hljs-meta, +.hljs-deletion { + color: #ccc; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-section, +.hljs-name, +.hljs-type, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/atelier-cave-dark.css b/vendor/highlight/styles/atelier-cave-dark.css new file mode 100644 index 00000000..65428f3b --- /dev/null +++ b/vendor/highlight/styles/atelier-cave-dark.css @@ -0,0 +1,83 @@ +/* Base16 Atelier Cave Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Cave Comment */ +.hljs-comment, +.hljs-quote { + color: #7e7887; +} + +/* Atelier-Cave Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-regexp, +.hljs-link, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #be4678; +} + +/* Atelier-Cave Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #aa573c; +} + +/* Atelier-Cave Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #2a9292; +} + +/* Atelier-Cave Blue */ +.hljs-title, +.hljs-section { + color: #576ddb; +} + +/* Atelier-Cave Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #955ae7; +} + +.hljs-deletion, +.hljs-addition { + color: #19171c; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #be4678; +} + +.hljs-addition { + background-color: #2a9292; +} + +.hljs { + display: block; + overflow-x: auto; + background: #19171c; + color: #8b8792; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-cave-light.css b/vendor/highlight/styles/atelier-cave-light.css new file mode 100644 index 00000000..b419f9fd --- /dev/null +++ b/vendor/highlight/styles/atelier-cave-light.css @@ -0,0 +1,85 @@ +/* Base16 Atelier Cave Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Cave Comment */ +.hljs-comment, +.hljs-quote { + color: #655f6d; +} + +/* Atelier-Cave Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #be4678; +} + +/* Atelier-Cave Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #aa573c; +} + +/* Atelier-Cave Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #2a9292; +} + +/* Atelier-Cave Blue */ +.hljs-title, +.hljs-section { + color: #576ddb; +} + +/* Atelier-Cave Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #955ae7; +} + +.hljs-deletion, +.hljs-addition { + color: #19171c; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #be4678; +} + +.hljs-addition { + background-color: #2a9292; +} + +.hljs { + display: block; + overflow-x: auto; + background: #efecf4; + color: #585260; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-dune-dark.css b/vendor/highlight/styles/atelier-dune-dark.css new file mode 100644 index 00000000..1684f522 --- /dev/null +++ b/vendor/highlight/styles/atelier-dune-dark.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Dune Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Dune Comment */ +.hljs-comment, +.hljs-quote { + color: #999580; +} + +/* Atelier-Dune Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #d73737; +} + +/* Atelier-Dune Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #b65611; +} + +/* Atelier-Dune Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #60ac39; +} + +/* Atelier-Dune Blue */ +.hljs-title, +.hljs-section { + color: #6684e1; +} + +/* Atelier-Dune Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #b854d4; +} + +.hljs { + display: block; + overflow-x: auto; + background: #20201d; + color: #a6a28c; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-dune-light.css b/vendor/highlight/styles/atelier-dune-light.css new file mode 100644 index 00000000..547719de --- /dev/null +++ b/vendor/highlight/styles/atelier-dune-light.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Dune Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Dune Comment */ +.hljs-comment, +.hljs-quote { + color: #7d7a68; +} + +/* Atelier-Dune Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #d73737; +} + +/* Atelier-Dune Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #b65611; +} + +/* Atelier-Dune Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #60ac39; +} + +/* Atelier-Dune Blue */ +.hljs-title, +.hljs-section { + color: #6684e1; +} + +/* Atelier-Dune Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #b854d4; +} + +.hljs { + display: block; + overflow-x: auto; + background: #fefbec; + color: #6e6b5e; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-estuary-dark.css b/vendor/highlight/styles/atelier-estuary-dark.css new file mode 100644 index 00000000..a5e50718 --- /dev/null +++ b/vendor/highlight/styles/atelier-estuary-dark.css @@ -0,0 +1,84 @@ +/* Base16 Atelier Estuary Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Estuary Comment */ +.hljs-comment, +.hljs-quote { + color: #878573; +} + +/* Atelier-Estuary Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #ba6236; +} + +/* Atelier-Estuary Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #ae7313; +} + +/* Atelier-Estuary Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #7d9726; +} + +/* Atelier-Estuary Blue */ +.hljs-title, +.hljs-section { + color: #36a166; +} + +/* Atelier-Estuary Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #5f9182; +} + +.hljs-deletion, +.hljs-addition { + color: #22221b; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #ba6236; +} + +.hljs-addition { + background-color: #7d9726; +} + +.hljs { + display: block; + overflow-x: auto; + background: #22221b; + color: #929181; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-estuary-light.css b/vendor/highlight/styles/atelier-estuary-light.css new file mode 100644 index 00000000..1daee5d9 --- /dev/null +++ b/vendor/highlight/styles/atelier-estuary-light.css @@ -0,0 +1,84 @@ +/* Base16 Atelier Estuary Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Estuary Comment */ +.hljs-comment, +.hljs-quote { + color: #6c6b5a; +} + +/* Atelier-Estuary Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #ba6236; +} + +/* Atelier-Estuary Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #ae7313; +} + +/* Atelier-Estuary Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #7d9726; +} + +/* Atelier-Estuary Blue */ +.hljs-title, +.hljs-section { + color: #36a166; +} + +/* Atelier-Estuary Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #5f9182; +} + +.hljs-deletion, +.hljs-addition { + color: #22221b; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #ba6236; +} + +.hljs-addition { + background-color: #7d9726; +} + +.hljs { + display: block; + overflow-x: auto; + background: #f4f3ec; + color: #5f5e4e; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-forest-dark.css b/vendor/highlight/styles/atelier-forest-dark.css new file mode 100644 index 00000000..0ef4fae3 --- /dev/null +++ b/vendor/highlight/styles/atelier-forest-dark.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Forest Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Forest Comment */ +.hljs-comment, +.hljs-quote { + color: #9c9491; +} + +/* Atelier-Forest Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #f22c40; +} + +/* Atelier-Forest Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #df5320; +} + +/* Atelier-Forest Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #7b9726; +} + +/* Atelier-Forest Blue */ +.hljs-title, +.hljs-section { + color: #407ee7; +} + +/* Atelier-Forest Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #6666ea; +} + +.hljs { + display: block; + overflow-x: auto; + background: #1b1918; + color: #a8a19f; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-forest-light.css b/vendor/highlight/styles/atelier-forest-light.css new file mode 100644 index 00000000..bbedde18 --- /dev/null +++ b/vendor/highlight/styles/atelier-forest-light.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Forest Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Forest Comment */ +.hljs-comment, +.hljs-quote { + color: #766e6b; +} + +/* Atelier-Forest Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #f22c40; +} + +/* Atelier-Forest Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #df5320; +} + +/* Atelier-Forest Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #7b9726; +} + +/* Atelier-Forest Blue */ +.hljs-title, +.hljs-section { + color: #407ee7; +} + +/* Atelier-Forest Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #6666ea; +} + +.hljs { + display: block; + overflow-x: auto; + background: #f1efee; + color: #68615e; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-heath-dark.css b/vendor/highlight/styles/atelier-heath-dark.css new file mode 100644 index 00000000..fe01ff72 --- /dev/null +++ b/vendor/highlight/styles/atelier-heath-dark.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Heath Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Heath Comment */ +.hljs-comment, +.hljs-quote { + color: #9e8f9e; +} + +/* Atelier-Heath Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #ca402b; +} + +/* Atelier-Heath Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #a65926; +} + +/* Atelier-Heath Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #918b3b; +} + +/* Atelier-Heath Blue */ +.hljs-title, +.hljs-section { + color: #516aec; +} + +/* Atelier-Heath Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #7b59c0; +} + +.hljs { + display: block; + overflow-x: auto; + background: #1b181b; + color: #ab9bab; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-heath-light.css b/vendor/highlight/styles/atelier-heath-light.css new file mode 100644 index 00000000..ee43786d --- /dev/null +++ b/vendor/highlight/styles/atelier-heath-light.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Heath Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Heath Comment */ +.hljs-comment, +.hljs-quote { + color: #776977; +} + +/* Atelier-Heath Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #ca402b; +} + +/* Atelier-Heath Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #a65926; +} + +/* Atelier-Heath Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #918b3b; +} + +/* Atelier-Heath Blue */ +.hljs-title, +.hljs-section { + color: #516aec; +} + +/* Atelier-Heath Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #7b59c0; +} + +.hljs { + display: block; + overflow-x: auto; + background: #f7f3f7; + color: #695d69; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-lakeside-dark.css b/vendor/highlight/styles/atelier-lakeside-dark.css new file mode 100644 index 00000000..a937d3bf --- /dev/null +++ b/vendor/highlight/styles/atelier-lakeside-dark.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Lakeside Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Lakeside Comment */ +.hljs-comment, +.hljs-quote { + color: #7195a8; +} + +/* Atelier-Lakeside Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #d22d72; +} + +/* Atelier-Lakeside Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #935c25; +} + +/* Atelier-Lakeside Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #568c3b; +} + +/* Atelier-Lakeside Blue */ +.hljs-title, +.hljs-section { + color: #257fad; +} + +/* Atelier-Lakeside Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #6b6bb8; +} + +.hljs { + display: block; + overflow-x: auto; + background: #161b1d; + color: #7ea2b4; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-lakeside-light.css b/vendor/highlight/styles/atelier-lakeside-light.css new file mode 100644 index 00000000..6c7e8f9e --- /dev/null +++ b/vendor/highlight/styles/atelier-lakeside-light.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Lakeside Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Lakeside Comment */ +.hljs-comment, +.hljs-quote { + color: #5a7b8c; +} + +/* Atelier-Lakeside Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #d22d72; +} + +/* Atelier-Lakeside Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #935c25; +} + +/* Atelier-Lakeside Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #568c3b; +} + +/* Atelier-Lakeside Blue */ +.hljs-title, +.hljs-section { + color: #257fad; +} + +/* Atelier-Lakeside Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #6b6bb8; +} + +.hljs { + display: block; + overflow-x: auto; + background: #ebf8ff; + color: #516d7b; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-plateau-dark.css b/vendor/highlight/styles/atelier-plateau-dark.css new file mode 100644 index 00000000..3bb05269 --- /dev/null +++ b/vendor/highlight/styles/atelier-plateau-dark.css @@ -0,0 +1,84 @@ +/* Base16 Atelier Plateau Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Plateau Comment */ +.hljs-comment, +.hljs-quote { + color: #7e7777; +} + +/* Atelier-Plateau Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #ca4949; +} + +/* Atelier-Plateau Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #b45a3c; +} + +/* Atelier-Plateau Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #4b8b8b; +} + +/* Atelier-Plateau Blue */ +.hljs-title, +.hljs-section { + color: #7272ca; +} + +/* Atelier-Plateau Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #8464c4; +} + +.hljs-deletion, +.hljs-addition { + color: #1b1818; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #ca4949; +} + +.hljs-addition { + background-color: #4b8b8b; +} + +.hljs { + display: block; + overflow-x: auto; + background: #1b1818; + color: #8a8585; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-plateau-light.css b/vendor/highlight/styles/atelier-plateau-light.css new file mode 100644 index 00000000..5f0222be --- /dev/null +++ b/vendor/highlight/styles/atelier-plateau-light.css @@ -0,0 +1,84 @@ +/* Base16 Atelier Plateau Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Plateau Comment */ +.hljs-comment, +.hljs-quote { + color: #655d5d; +} + +/* Atelier-Plateau Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #ca4949; +} + +/* Atelier-Plateau Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #b45a3c; +} + +/* Atelier-Plateau Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #4b8b8b; +} + +/* Atelier-Plateau Blue */ +.hljs-title, +.hljs-section { + color: #7272ca; +} + +/* Atelier-Plateau Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #8464c4; +} + +.hljs-deletion, +.hljs-addition { + color: #1b1818; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #ca4949; +} + +.hljs-addition { + background-color: #4b8b8b; +} + +.hljs { + display: block; + overflow-x: auto; + background: #f4ecec; + color: #585050; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-savanna-dark.css b/vendor/highlight/styles/atelier-savanna-dark.css new file mode 100644 index 00000000..38f83143 --- /dev/null +++ b/vendor/highlight/styles/atelier-savanna-dark.css @@ -0,0 +1,84 @@ +/* Base16 Atelier Savanna Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Savanna Comment */ +.hljs-comment, +.hljs-quote { + color: #78877d; +} + +/* Atelier-Savanna Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #b16139; +} + +/* Atelier-Savanna Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #9f713c; +} + +/* Atelier-Savanna Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #489963; +} + +/* Atelier-Savanna Blue */ +.hljs-title, +.hljs-section { + color: #478c90; +} + +/* Atelier-Savanna Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #55859b; +} + +.hljs-deletion, +.hljs-addition { + color: #171c19; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #b16139; +} + +.hljs-addition { + background-color: #489963; +} + +.hljs { + display: block; + overflow-x: auto; + background: #171c19; + color: #87928a; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-savanna-light.css b/vendor/highlight/styles/atelier-savanna-light.css new file mode 100644 index 00000000..1ccd7c68 --- /dev/null +++ b/vendor/highlight/styles/atelier-savanna-light.css @@ -0,0 +1,84 @@ +/* Base16 Atelier Savanna Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Savanna Comment */ +.hljs-comment, +.hljs-quote { + color: #5f6d64; +} + +/* Atelier-Savanna Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #b16139; +} + +/* Atelier-Savanna Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #9f713c; +} + +/* Atelier-Savanna Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #489963; +} + +/* Atelier-Savanna Blue */ +.hljs-title, +.hljs-section { + color: #478c90; +} + +/* Atelier-Savanna Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #55859b; +} + +.hljs-deletion, +.hljs-addition { + color: #171c19; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #b16139; +} + +.hljs-addition { + background-color: #489963; +} + +.hljs { + display: block; + overflow-x: auto; + background: #ecf4ee; + color: #526057; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-seaside-dark.css b/vendor/highlight/styles/atelier-seaside-dark.css new file mode 100644 index 00000000..df29949c --- /dev/null +++ b/vendor/highlight/styles/atelier-seaside-dark.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Seaside Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Seaside Comment */ +.hljs-comment, +.hljs-quote { + color: #809980; +} + +/* Atelier-Seaside Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #e6193c; +} + +/* Atelier-Seaside Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #87711d; +} + +/* Atelier-Seaside Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #29a329; +} + +/* Atelier-Seaside Blue */ +.hljs-title, +.hljs-section { + color: #3d62f5; +} + +/* Atelier-Seaside Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #ad2bee; +} + +.hljs { + display: block; + overflow-x: auto; + background: #131513; + color: #8ca68c; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-seaside-light.css b/vendor/highlight/styles/atelier-seaside-light.css new file mode 100644 index 00000000..9d960f29 --- /dev/null +++ b/vendor/highlight/styles/atelier-seaside-light.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Seaside Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Seaside Comment */ +.hljs-comment, +.hljs-quote { + color: #687d68; +} + +/* Atelier-Seaside Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #e6193c; +} + +/* Atelier-Seaside Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #87711d; +} + +/* Atelier-Seaside Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #29a329; +} + +/* Atelier-Seaside Blue */ +.hljs-title, +.hljs-section { + color: #3d62f5; +} + +/* Atelier-Seaside Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #ad2bee; +} + +.hljs { + display: block; + overflow-x: auto; + background: #f4fbf4; + color: #5e6e5e; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-sulphurpool-dark.css b/vendor/highlight/styles/atelier-sulphurpool-dark.css new file mode 100644 index 00000000..c2ab7938 --- /dev/null +++ b/vendor/highlight/styles/atelier-sulphurpool-dark.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Sulphurpool Dark - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Sulphurpool Comment */ +.hljs-comment, +.hljs-quote { + color: #898ea4; +} + +/* Atelier-Sulphurpool Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #c94922; +} + +/* Atelier-Sulphurpool Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #c76b29; +} + +/* Atelier-Sulphurpool Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #ac9739; +} + +/* Atelier-Sulphurpool Blue */ +.hljs-title, +.hljs-section { + color: #3d8fd1; +} + +/* Atelier-Sulphurpool Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #6679cc; +} + +.hljs { + display: block; + overflow-x: auto; + background: #202746; + color: #979db4; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atelier-sulphurpool-light.css b/vendor/highlight/styles/atelier-sulphurpool-light.css new file mode 100644 index 00000000..96c47d08 --- /dev/null +++ b/vendor/highlight/styles/atelier-sulphurpool-light.css @@ -0,0 +1,69 @@ +/* Base16 Atelier Sulphurpool Light - Theme */ +/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool) */ +/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ + +/* Atelier-Sulphurpool Comment */ +.hljs-comment, +.hljs-quote { + color: #6b7394; +} + +/* Atelier-Sulphurpool Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #c94922; +} + +/* Atelier-Sulphurpool Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #c76b29; +} + +/* Atelier-Sulphurpool Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #ac9739; +} + +/* Atelier-Sulphurpool Blue */ +.hljs-title, +.hljs-section { + color: #3d8fd1; +} + +/* Atelier-Sulphurpool Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #6679cc; +} + +.hljs { + display: block; + overflow-x: auto; + background: #f5f7ff; + color: #5e6687; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/atom-one-dark-reasonable.css b/vendor/highlight/styles/atom-one-dark-reasonable.css new file mode 100644 index 00000000..fd41c996 --- /dev/null +++ b/vendor/highlight/styles/atom-one-dark-reasonable.css @@ -0,0 +1,77 @@ +/* + +Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage + +Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax + +*/ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + line-height: 1.3em; + color: #abb2bf; + background: #282c34; + border-radius: 5px; +} +.hljs-keyword, .hljs-operator { + color: #F92672; +} +.hljs-pattern-match { + color: #F92672; +} +.hljs-pattern-match .hljs-constructor { + color: #61aeee; +} +.hljs-function { + color: #61aeee; +} +.hljs-function .hljs-params { + color: #A6E22E; +} +.hljs-function .hljs-params .hljs-typing { + color: #FD971F; +} +.hljs-module-access .hljs-module { + color: #7e57c2; +} +.hljs-constructor { + color: #e2b93d; +} +.hljs-constructor .hljs-string { + color: #9CCC65; +} +.hljs-comment, .hljs-quote { + color: #b18eb1; + font-style: italic; +} +.hljs-doctag, .hljs-formula { + color: #c678dd; +} +.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { + color: #e06c75; +} +.hljs-literal { + color: #56b6c2; +} +.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { + color: #98c379; +} +.hljs-built_in, .hljs-class .hljs-title { + color: #e6c07b; +} +.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { + color: #d19a66; +} +.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { + color: #61aeee; +} +.hljs-emphasis { + font-style: italic; +} +.hljs-strong { + font-weight: bold; +} +.hljs-link { + text-decoration: underline; +} diff --git a/vendor/highlight/styles/atom-one-dark.css b/vendor/highlight/styles/atom-one-dark.css new file mode 100644 index 00000000..1616aafe --- /dev/null +++ b/vendor/highlight/styles/atom-one-dark.css @@ -0,0 +1,96 @@ +/* + +Atom One Dark by Daniel Gamage +Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax + +base: #282c34 +mono-1: #abb2bf +mono-2: #818896 +mono-3: #5c6370 +hue-1: #56b6c2 +hue-2: #61aeee +hue-3: #c678dd +hue-4: #98c379 +hue-5: #e06c75 +hue-5-2: #be5046 +hue-6: #d19a66 +hue-6-2: #e6c07b + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #abb2bf; + background: #282c34; +} + +.hljs-comment, +.hljs-quote { + color: #5c6370; + font-style: italic; +} + +.hljs-doctag, +.hljs-keyword, +.hljs-formula { + color: #c678dd; +} + +.hljs-section, +.hljs-name, +.hljs-selector-tag, +.hljs-deletion, +.hljs-subst { + color: #e06c75; +} + +.hljs-literal { + color: #56b6c2; +} + +.hljs-string, +.hljs-regexp, +.hljs-addition, +.hljs-attribute, +.hljs-meta-string { + color: #98c379; +} + +.hljs-built_in, +.hljs-class .hljs-title { + color: #e6c07b; +} + +.hljs-attr, +.hljs-variable, +.hljs-template-variable, +.hljs-type, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-number { + color: #d19a66; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-meta, +.hljs-selector-id, +.hljs-title { + color: #61aeee; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/vendor/highlight/styles/atom-one-light.css b/vendor/highlight/styles/atom-one-light.css new file mode 100644 index 00000000..d5bd1d2a --- /dev/null +++ b/vendor/highlight/styles/atom-one-light.css @@ -0,0 +1,96 @@ +/* + +Atom One Light by Daniel Gamage +Original One Light Syntax theme from https://github.com/atom/one-light-syntax + +base: #fafafa +mono-1: #383a42 +mono-2: #686b77 +mono-3: #a0a1a7 +hue-1: #0184bb +hue-2: #4078f2 +hue-3: #a626a4 +hue-4: #50a14f +hue-5: #e45649 +hue-5-2: #c91243 +hue-6: #986801 +hue-6-2: #c18401 + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #383a42; + background: #fafafa; +} + +.hljs-comment, +.hljs-quote { + color: #a0a1a7; + font-style: italic; +} + +.hljs-doctag, +.hljs-keyword, +.hljs-formula { + color: #a626a4; +} + +.hljs-section, +.hljs-name, +.hljs-selector-tag, +.hljs-deletion, +.hljs-subst { + color: #e45649; +} + +.hljs-literal { + color: #0184bb; +} + +.hljs-string, +.hljs-regexp, +.hljs-addition, +.hljs-attribute, +.hljs-meta-string { + color: #50a14f; +} + +.hljs-built_in, +.hljs-class .hljs-title { + color: #c18401; +} + +.hljs-attr, +.hljs-variable, +.hljs-template-variable, +.hljs-type, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-number { + color: #986801; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-meta, +.hljs-selector-id, +.hljs-title { + color: #4078f2; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/vendor/highlight/styles/brown-paper.css b/vendor/highlight/styles/brown-paper.css new file mode 100644 index 00000000..f0197b92 --- /dev/null +++ b/vendor/highlight/styles/brown-paper.css @@ -0,0 +1,64 @@ +/* + +Brown Paper style from goldblog.com.ua (c) Zaripov Yura + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background:#b7a68e url(./brown-papersq.png); +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal { + color:#005599; + font-weight:bold; +} + +.hljs, +.hljs-subst { + color: #363c69; +} + +.hljs-string, +.hljs-title, +.hljs-section, +.hljs-type, +.hljs-attribute, +.hljs-symbol, +.hljs-bullet, +.hljs-built_in, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable, +.hljs-link, +.hljs-name { + color: #2c009f; +} + +.hljs-comment, +.hljs-quote, +.hljs-meta, +.hljs-deletion { + color: #802022; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-doctag, +.hljs-title, +.hljs-section, +.hljs-type, +.hljs-name, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/brown-papersq.png b/vendor/highlight/styles/brown-papersq.png new file mode 100644 index 00000000..3813903d Binary files /dev/null and b/vendor/highlight/styles/brown-papersq.png differ diff --git a/vendor/highlight/styles/codepen-embed.css b/vendor/highlight/styles/codepen-embed.css new file mode 100644 index 00000000..195c4a07 --- /dev/null +++ b/vendor/highlight/styles/codepen-embed.css @@ -0,0 +1,60 @@ +/* + codepen.io Embed Theme + Author: Justin Perry + Original theme - https://github.com/chriskempson/tomorrow-theme +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #222; + color: #fff; +} + +.hljs-comment, +.hljs-quote { + color: #777; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-regexp, +.hljs-meta, +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-params, +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-deletion { + color: #ab875d; +} + +.hljs-section, +.hljs-title, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-type, +.hljs-attribute { + color: #9b869b; +} + +.hljs-string, +.hljs-keyword, +.hljs-selector-tag, +.hljs-addition { + color: #8f9c6c; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/color-brewer.css b/vendor/highlight/styles/color-brewer.css new file mode 100644 index 00000000..7934d986 --- /dev/null +++ b/vendor/highlight/styles/color-brewer.css @@ -0,0 +1,71 @@ +/* + +Colorbrewer theme +Original: https://github.com/mbostock/colorbrewer-theme (c) Mike Bostock +Ported by Fabrício Tavares de Oliveira + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #fff; +} + +.hljs, +.hljs-subst { + color: #000; +} + +.hljs-string, +.hljs-meta, +.hljs-symbol, +.hljs-template-tag, +.hljs-template-variable, +.hljs-addition { + color: #756bb1; +} + +.hljs-comment, +.hljs-quote { + color: #636363; +} + +.hljs-number, +.hljs-regexp, +.hljs-literal, +.hljs-bullet, +.hljs-link { + color: #31a354; +} + +.hljs-deletion, +.hljs-variable { + color: #88f; +} + + + +.hljs-keyword, +.hljs-selector-tag, +.hljs-title, +.hljs-section, +.hljs-built_in, +.hljs-doctag, +.hljs-type, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-strong { + color: #3182bd; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-attribute { + color: #e6550d; +} diff --git a/vendor/highlight/styles/darcula.css b/vendor/highlight/styles/darcula.css new file mode 100644 index 00000000..be182d0b --- /dev/null +++ b/vendor/highlight/styles/darcula.css @@ -0,0 +1,77 @@ +/* + +Darcula color scheme from the JetBrains family of IDEs + +*/ + + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #2b2b2b; +} + +.hljs { + color: #bababa; +} + +.hljs-strong, +.hljs-emphasis { + color: #a8a8a2; +} + +.hljs-bullet, +.hljs-quote, +.hljs-link, +.hljs-number, +.hljs-regexp, +.hljs-literal { + color: #6896ba; +} + +.hljs-code, +.hljs-selector-class { + color: #a6e22e; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-section, +.hljs-attribute, +.hljs-name, +.hljs-variable { + color: #cb7832; +} + +.hljs-params { + color: #b9b9b9; +} + +.hljs-string { + color: #6a8759; +} + +.hljs-subst, +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-symbol, +.hljs-selector-id, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-template-tag, +.hljs-template-variable, +.hljs-addition { + color: #e0c46c; +} + +.hljs-comment, +.hljs-deletion, +.hljs-meta { + color: #7f7f7f; +} diff --git a/vendor/highlight/styles/dark.css b/vendor/highlight/styles/dark.css new file mode 100644 index 00000000..b4724f5f --- /dev/null +++ b/vendor/highlight/styles/dark.css @@ -0,0 +1,63 @@ +/* + +Dark style from softwaremaniacs.org (c) Ivan Sagalaev + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #444; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-section, +.hljs-link { + color: white; +} + +.hljs, +.hljs-subst { + color: #ddd; +} + +.hljs-string, +.hljs-title, +.hljs-name, +.hljs-type, +.hljs-attribute, +.hljs-symbol, +.hljs-bullet, +.hljs-built_in, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #d88; +} + +.hljs-comment, +.hljs-quote, +.hljs-deletion, +.hljs-meta { + color: #777; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-title, +.hljs-section, +.hljs-doctag, +.hljs-type, +.hljs-name, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/darkula.css b/vendor/highlight/styles/darkula.css new file mode 100644 index 00000000..f4646c3c --- /dev/null +++ b/vendor/highlight/styles/darkula.css @@ -0,0 +1,6 @@ +/* + Deprecated due to a typo in the name and left here for compatibility purpose only. + Please use darcula.css instead. +*/ + +@import url('darcula.css'); diff --git a/vendor/highlight/styles/default.css b/vendor/highlight/styles/default.css new file mode 100644 index 00000000..f1bfade3 --- /dev/null +++ b/vendor/highlight/styles/default.css @@ -0,0 +1,99 @@ +/* + +Original highlight.js style (c) Ivan Sagalaev + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #F0F0F0; +} + + +/* Base color: saturation 0; */ + +.hljs, +.hljs-subst { + color: #444; +} + +.hljs-comment { + color: #888888; +} + +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold; +} + + +/* User color: hue: 0 */ + +.hljs-type, +.hljs-string, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #880000; +} + +.hljs-title, +.hljs-section { + color: #880000; + font-weight: bold; +} + +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #BC6060; +} + + +/* Language color: hue: 90; */ + +.hljs-literal { + color: #78A960; +} + +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #397300; +} + + +/* Meta color: hue: 200 */ + +.hljs-meta { + color: #1f7199; +} + +.hljs-meta-string { + color: #4d99bf; +} + + +/* Misc effects */ + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/docco.css b/vendor/highlight/styles/docco.css new file mode 100644 index 00000000..db366be3 --- /dev/null +++ b/vendor/highlight/styles/docco.css @@ -0,0 +1,97 @@ +/* +Docco style used in http://jashkenas.github.com/docco/ converted by Simon Madine (@thingsinjars) +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #000; + background: #f8f8ff; +} + +.hljs-comment, +.hljs-quote { + color: #408080; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-subst { + color: #954121; +} + +.hljs-number { + color: #40a070; +} + +.hljs-string, +.hljs-doctag { + color: #219161; +} + +.hljs-selector-id, +.hljs-selector-class, +.hljs-section, +.hljs-type { + color: #19469d; +} + +.hljs-params { + color: #00f; +} + +.hljs-title { + color: #458; + font-weight: bold; +} + +.hljs-tag, +.hljs-name, +.hljs-attribute { + color: #000080; + font-weight: normal; +} + +.hljs-variable, +.hljs-template-variable { + color: #008080; +} + +.hljs-regexp, +.hljs-link { + color: #b68; +} + +.hljs-symbol, +.hljs-bullet { + color: #990073; +} + +.hljs-built_in, +.hljs-builtin-name { + color: #0086b3; +} + +.hljs-meta { + color: #999; + font-weight: bold; +} + +.hljs-deletion { + background: #fdd; +} + +.hljs-addition { + background: #dfd; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/dracula.css b/vendor/highlight/styles/dracula.css new file mode 100644 index 00000000..d591db68 --- /dev/null +++ b/vendor/highlight/styles/dracula.css @@ -0,0 +1,76 @@ +/* + +Dracula Theme v1.2.0 + +https://github.com/zenorocha/dracula-theme + +Copyright 2015, All rights reserved + +Code licensed under the MIT license +http://zenorocha.mit-license.org + +@author Éverton Ribeiro +@author Zeno Rocha + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #282a36; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-section, +.hljs-link { + color: #8be9fd; +} + +.hljs-function .hljs-keyword { + color: #ff79c6; +} + +.hljs, +.hljs-subst { + color: #f8f8f2; +} + +.hljs-string, +.hljs-title, +.hljs-name, +.hljs-type, +.hljs-attribute, +.hljs-symbol, +.hljs-bullet, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #f1fa8c; +} + +.hljs-comment, +.hljs-quote, +.hljs-deletion, +.hljs-meta { + color: #6272a4; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-title, +.hljs-section, +.hljs-doctag, +.hljs-type, +.hljs-name, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/far.css b/vendor/highlight/styles/far.css new file mode 100644 index 00000000..2b3f87b5 --- /dev/null +++ b/vendor/highlight/styles/far.css @@ -0,0 +1,71 @@ +/* + +FAR Style (c) MajestiC + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #000080; +} + +.hljs, +.hljs-subst { + color: #0ff; +} + +.hljs-string, +.hljs-attribute, +.hljs-symbol, +.hljs-bullet, +.hljs-built_in, +.hljs-builtin-name, +.hljs-template-tag, +.hljs-template-variable, +.hljs-addition { + color: #ff0; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-section, +.hljs-type, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-variable { + color: #fff; +} + +.hljs-comment, +.hljs-quote, +.hljs-doctag, +.hljs-deletion { + color: #888; +} + +.hljs-number, +.hljs-regexp, +.hljs-literal, +.hljs-link { + color: #0f0; +} + +.hljs-meta { + color: #008080; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-title, +.hljs-section, +.hljs-name, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/foundation.css b/vendor/highlight/styles/foundation.css new file mode 100644 index 00000000..f1fe64b3 --- /dev/null +++ b/vendor/highlight/styles/foundation.css @@ -0,0 +1,88 @@ +/* +Description: Foundation 4 docs style for highlight.js +Author: Dan Allen +Website: http://foundation.zurb.com/docs/ +Version: 1.0 +Date: 2013-04-02 +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #eee; color: black; +} + +.hljs-link, +.hljs-emphasis, +.hljs-attribute, +.hljs-addition { + color: #070; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong, +.hljs-string, +.hljs-deletion { + color: #d14; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-quote, +.hljs-comment { + color: #998; + font-style: italic; +} + +.hljs-section, +.hljs-title { + color: #900; +} + +.hljs-class .hljs-title, +.hljs-type { + color: #458; +} + +.hljs-variable, +.hljs-template-variable { + color: #336699; +} + +.hljs-bullet { + color: #997700; +} + +.hljs-meta { + color: #3344bb; +} + +.hljs-code, +.hljs-number, +.hljs-literal, +.hljs-keyword, +.hljs-selector-tag { + color: #099; +} + +.hljs-regexp { + background-color: #fff0ff; + color: #880088; +} + +.hljs-symbol { + color: #990073; +} + +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #007700; +} diff --git a/vendor/highlight/styles/github-gist.css b/vendor/highlight/styles/github-gist.css new file mode 100644 index 00000000..155f0b91 --- /dev/null +++ b/vendor/highlight/styles/github-gist.css @@ -0,0 +1,71 @@ +/** + * GitHub Gist Theme + * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro + */ + +.hljs { + display: block; + background: white; + padding: 0.5em; + color: #333333; + overflow-x: auto; +} + +.hljs-comment, +.hljs-meta { + color: #969896; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-strong, +.hljs-emphasis, +.hljs-quote { + color: #df5000; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #a71d5d; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute { + color: #0086b3; +} + +.hljs-section, +.hljs-name { + color: #63a35c; +} + +.hljs-tag { + color: #333333; +} + +.hljs-title, +.hljs-attr, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #795da3; +} + +.hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/vendor/highlight/styles/github.css b/vendor/highlight/styles/github.css new file mode 100644 index 00000000..791932b8 --- /dev/null +++ b/vendor/highlight/styles/github.css @@ -0,0 +1,99 @@ +/* + +github.com style (c) Vasily Polovnyov + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #333; + background: #f8f8f8; +} + +.hljs-comment, +.hljs-quote { + color: #998; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-subst { + color: #333; + font-weight: bold; +} + +.hljs-number, +.hljs-literal, +.hljs-variable, +.hljs-template-variable, +.hljs-tag .hljs-attr { + color: #008080; +} + +.hljs-string, +.hljs-doctag { + color: #d14; +} + +.hljs-title, +.hljs-section, +.hljs-selector-id { + color: #900; + font-weight: bold; +} + +.hljs-subst { + font-weight: normal; +} + +.hljs-type, +.hljs-class .hljs-title { + color: #458; + font-weight: bold; +} + +.hljs-tag, +.hljs-name, +.hljs-attribute { + color: #000080; + font-weight: normal; +} + +.hljs-regexp, +.hljs-link { + color: #009926; +} + +.hljs-symbol, +.hljs-bullet { + color: #990073; +} + +.hljs-built_in, +.hljs-builtin-name { + color: #0086b3; +} + +.hljs-meta { + color: #999; + font-weight: bold; +} + +.hljs-deletion { + background: #fdd; +} + +.hljs-addition { + background: #dfd; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/gml.css b/vendor/highlight/styles/gml.css new file mode 100644 index 00000000..ffb5e474 --- /dev/null +++ b/vendor/highlight/styles/gml.css @@ -0,0 +1,78 @@ +/* + +GML Theme - Meseta + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #222222; + color: #C0C0C0; +} + +.hljs-keywords { + color: #FFB871; + font-weight: bold; +} + +.hljs-built_in { + color: #FFB871; +} + +.hljs-literal { + color: #FF8080; +} + +.hljs-symbol { + color: #58E55A; +} + +.hljs-comment { + color: #5B995B; +} + +.hljs-string { + color: #FFFF00; +} + +.hljs-number { + color: #FF8080; +} + +.hljs-attribute, +.hljs-selector-tag, +.hljs-doctag, +.hljs-name, +.hljs-bullet, +.hljs-code, +.hljs-addition, +.hljs-regexp, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-type, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion, +.hljs-title, +.hljs-section, +.hljs-function, +.hljs-meta-keyword, +.hljs-meta, +.hljs-subst { + color: #C0C0C0; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/googlecode.css b/vendor/highlight/styles/googlecode.css new file mode 100644 index 00000000..884ad635 --- /dev/null +++ b/vendor/highlight/styles/googlecode.css @@ -0,0 +1,89 @@ +/* + +Google Code style (c) Aahan Krish + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: white; + color: black; +} + +.hljs-comment, +.hljs-quote { + color: #800; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-section, +.hljs-title, +.hljs-name { + color: #008; +} + +.hljs-variable, +.hljs-template-variable { + color: #660; +} + +.hljs-string, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-regexp { + color: #080; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-meta, +.hljs-number, +.hljs-link { + color: #066; +} + +.hljs-title, +.hljs-doctag, +.hljs-type, +.hljs-attr, +.hljs-built_in, +.hljs-builtin-name, +.hljs-params { + color: #606; +} + +.hljs-attribute, +.hljs-subst { + color: #000; +} + +.hljs-formula { + background-color: #eee; + font-style: italic; +} + +.hljs-selector-id, +.hljs-selector-class { + color: #9B703F +} + +.hljs-addition { + background-color: #baeeba; +} + +.hljs-deletion { + background-color: #ffc8bd; +} + +.hljs-doctag, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/grayscale.css b/vendor/highlight/styles/grayscale.css new file mode 100644 index 00000000..5376f340 --- /dev/null +++ b/vendor/highlight/styles/grayscale.css @@ -0,0 +1,101 @@ +/* + +grayscale style (c) MY Sun + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #333; + background: #fff; +} + +.hljs-comment, +.hljs-quote { + color: #777; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-subst { + color: #333; + font-weight: bold; +} + +.hljs-number, +.hljs-literal { + color: #777; +} + +.hljs-string, +.hljs-doctag, +.hljs-formula { + color: #333; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAJ0lEQVQIW2O8e/fufwYGBgZBQUEQxcCIIfDu3Tuwivfv30NUoAsAALHpFMMLqZlPAAAAAElFTkSuQmCC) repeat; +} + +.hljs-title, +.hljs-section, +.hljs-selector-id { + color: #000; + font-weight: bold; +} + +.hljs-subst { + font-weight: normal; +} + +.hljs-class .hljs-title, +.hljs-type, +.hljs-name { + color: #333; + font-weight: bold; +} + +.hljs-tag { + color: #333; +} + +.hljs-regexp { + color: #333; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAICAYAAADA+m62AAAAPUlEQVQYV2NkQAN37979r6yszIgujiIAU4RNMVwhuiQ6H6wQl3XI4oy4FMHcCJPHcDS6J2A2EqUQpJhohQDexSef15DBCwAAAABJRU5ErkJggg==) repeat; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link { + color: #000; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAKElEQVQIW2NkQAO7d+/+z4gsBhJwdXVlhAvCBECKwIIwAbhKZBUwBQA6hBpm5efZsgAAAABJRU5ErkJggg==) repeat; +} + +.hljs-built_in, +.hljs-builtin-name { + color: #000; + text-decoration: underline; +} + +.hljs-meta { + color: #999; + font-weight: bold; +} + +.hljs-deletion { + color: #fff; + background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAE0lEQVQIW2MMDQ39zzhz5kwIAQAyxweWgUHd1AAAAABJRU5ErkJggg==) repeat; +} + +.hljs-addition { + color: #000; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAALUlEQVQYV2N89+7dfwYk8P79ewZBQUFkIQZGOiu6e/cuiptQHAPl0NtNxAQBAM97Oejj3Dg7AAAAAElFTkSuQmCC) repeat; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/gruvbox-dark.css b/vendor/highlight/styles/gruvbox-dark.css new file mode 100644 index 00000000..f563811a --- /dev/null +++ b/vendor/highlight/styles/gruvbox-dark.css @@ -0,0 +1,108 @@ +/* + +Gruvbox style (dark) (c) Pavel Pertsev (original style at https://github.com/morhetz/gruvbox) + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #282828; +} + +.hljs, +.hljs-subst { + color: #ebdbb2; +} + +/* Gruvbox Red */ +.hljs-deletion, +.hljs-formula, +.hljs-keyword, +.hljs-link, +.hljs-selector-tag { + color: #fb4934; +} + +/* Gruvbox Blue */ +.hljs-built_in, +.hljs-emphasis, +.hljs-name, +.hljs-quote, +.hljs-strong, +.hljs-title, +.hljs-variable { + color: #83a598; +} + +/* Gruvbox Yellow */ +.hljs-attr, +.hljs-params, +.hljs-template-tag, +.hljs-type { + color: #fabd2f; +} + +/* Gruvbox Purple */ +.hljs-builtin-name, +.hljs-doctag, +.hljs-literal, +.hljs-number { + color: #8f3f71; +} + +/* Gruvbox Orange */ +.hljs-code, +.hljs-meta, +.hljs-regexp, +.hljs-selector-id, +.hljs-template-variable { + color: #fe8019; +} + +/* Gruvbox Green */ +.hljs-addition, +.hljs-meta-string, +.hljs-section, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-string, +.hljs-symbol { + color: #b8bb26; +} + +/* Gruvbox Aqua */ +.hljs-attribute, +.hljs-bullet, +.hljs-class, +.hljs-function, +.hljs-function .hljs-keyword, +.hljs-meta-keyword, +.hljs-selector-pseudo, +.hljs-tag { + color: #8ec07c; +} + +/* Gruvbox Gray */ +.hljs-comment { + color: #928374; +} + +/* Gruvbox Purple */ +.hljs-link_label, +.hljs-literal, +.hljs-number { + color: #d3869b; +} + +.hljs-comment, +.hljs-emphasis { + font-style: italic; +} + +.hljs-section, +.hljs-strong, +.hljs-tag { + font-weight: bold; +} diff --git a/vendor/highlight/styles/gruvbox-light.css b/vendor/highlight/styles/gruvbox-light.css new file mode 100644 index 00000000..ff45468e --- /dev/null +++ b/vendor/highlight/styles/gruvbox-light.css @@ -0,0 +1,108 @@ +/* + +Gruvbox style (light) (c) Pavel Pertsev (original style at https://github.com/morhetz/gruvbox) + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #fbf1c7; +} + +.hljs, +.hljs-subst { + color: #3c3836; +} + +/* Gruvbox Red */ +.hljs-deletion, +.hljs-formula, +.hljs-keyword, +.hljs-link, +.hljs-selector-tag { + color: #9d0006; +} + +/* Gruvbox Blue */ +.hljs-built_in, +.hljs-emphasis, +.hljs-name, +.hljs-quote, +.hljs-strong, +.hljs-title, +.hljs-variable { + color: #076678; +} + +/* Gruvbox Yellow */ +.hljs-attr, +.hljs-params, +.hljs-template-tag, +.hljs-type { + color: #b57614; +} + +/* Gruvbox Purple */ +.hljs-builtin-name, +.hljs-doctag, +.hljs-literal, +.hljs-number { + color: #8f3f71; +} + +/* Gruvbox Orange */ +.hljs-code, +.hljs-meta, +.hljs-regexp, +.hljs-selector-id, +.hljs-template-variable { + color: #af3a03; +} + +/* Gruvbox Green */ +.hljs-addition, +.hljs-meta-string, +.hljs-section, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-string, +.hljs-symbol { + color: #79740e; +} + +/* Gruvbox Aqua */ +.hljs-attribute, +.hljs-bullet, +.hljs-class, +.hljs-function, +.hljs-function .hljs-keyword, +.hljs-meta-keyword, +.hljs-selector-pseudo, +.hljs-tag { + color: #427b58; +} + +/* Gruvbox Gray */ +.hljs-comment { + color: #928374; +} + +/* Gruvbox Purple */ +.hljs-link_label, +.hljs-literal, +.hljs-number { + color: #8f3f71; +} + +.hljs-comment, +.hljs-emphasis { + font-style: italic; +} + +.hljs-section, +.hljs-strong, +.hljs-tag { + font-weight: bold; +} diff --git a/vendor/highlight/styles/hopscotch.css b/vendor/highlight/styles/hopscotch.css new file mode 100644 index 00000000..32e60d23 --- /dev/null +++ b/vendor/highlight/styles/hopscotch.css @@ -0,0 +1,83 @@ +/* + * Hopscotch + * by Jan T. Sott + * https://github.com/idleberg/Hopscotch + * + * This work is licensed under the Creative Commons CC0 1.0 Universal License + */ + +/* Comment */ +.hljs-comment, +.hljs-quote { + color: #989498; +} + +/* Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-link, +.hljs-deletion { + color: #dd464c; +} + +/* Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #fd8b19; +} + +/* Yellow */ +.hljs-class .hljs-title { + color: #fdcc59; +} + +/* Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #8fc13e; +} + +/* Aqua */ +.hljs-meta { + color: #149b93; +} + +/* Blue */ +.hljs-function, +.hljs-section, +.hljs-title { + color: #1290bf; +} + +/* Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #c85e7c; +} + +.hljs { + display: block; + background: #322931; + color: #b9b5b8; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/hybrid.css b/vendor/highlight/styles/hybrid.css new file mode 100644 index 00000000..29735a18 --- /dev/null +++ b/vendor/highlight/styles/hybrid.css @@ -0,0 +1,102 @@ +/* + +vim-hybrid theme by w0ng (https://github.com/w0ng/vim-hybrid) + +*/ + +/*background color*/ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #1d1f21; +} + +/*selection color*/ +.hljs::selection, +.hljs span::selection { + background: #373b41; +} + +.hljs::-moz-selection, +.hljs span::-moz-selection { + background: #373b41; +} + +/*foreground color*/ +.hljs { + color: #c5c8c6; +} + +/*color: fg_yellow*/ +.hljs-title, +.hljs-name { + color: #f0c674; +} + +/*color: fg_comment*/ +.hljs-comment, +.hljs-meta, +.hljs-meta .hljs-keyword { + color: #707880; +} + +/*color: fg_red*/ +.hljs-number, +.hljs-symbol, +.hljs-literal, +.hljs-deletion, +.hljs-link { + color: #cc6666 +} + +/*color: fg_green*/ +.hljs-string, +.hljs-doctag, +.hljs-addition, +.hljs-regexp, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #b5bd68; +} + +/*color: fg_purple*/ +.hljs-attribute, +.hljs-code, +.hljs-selector-id { + color: #b294bb; +} + +/*color: fg_blue*/ +.hljs-keyword, +.hljs-selector-tag, +.hljs-bullet, +.hljs-tag { + color: #81a2be; +} + +/*color: fg_aqua*/ +.hljs-subst, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #8abeb7; +} + +/*color: fg_orange*/ +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-quote, +.hljs-section, +.hljs-selector-class { + color: #de935f; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/idea.css b/vendor/highlight/styles/idea.css new file mode 100644 index 00000000..3bf1892b --- /dev/null +++ b/vendor/highlight/styles/idea.css @@ -0,0 +1,97 @@ +/* + +Intellij Idea-like styling (c) Vasily Polovnyov + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #000; + background: #fff; +} + +.hljs-subst, +.hljs-title { + font-weight: normal; + color: #000; +} + +.hljs-comment, +.hljs-quote { + color: #808080; + font-style: italic; +} + +.hljs-meta { + color: #808000; +} + +.hljs-tag { + background: #efefef; +} + +.hljs-section, +.hljs-name, +.hljs-literal, +.hljs-keyword, +.hljs-selector-tag, +.hljs-type, +.hljs-selector-id, +.hljs-selector-class { + font-weight: bold; + color: #000080; +} + +.hljs-attribute, +.hljs-number, +.hljs-regexp, +.hljs-link { + font-weight: bold; + color: #0000ff; +} + +.hljs-number, +.hljs-regexp, +.hljs-link { + font-weight: normal; +} + +.hljs-string { + color: #008000; + font-weight: bold; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-formula { + color: #000; + background: #d0eded; + font-style: italic; +} + +.hljs-doctag { + text-decoration: underline; +} + +.hljs-variable, +.hljs-template-variable { + color: #660e7a; +} + +.hljs-addition { + background: #baeeba; +} + +.hljs-deletion { + background: #ffc8bd; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/ir-black.css b/vendor/highlight/styles/ir-black.css new file mode 100644 index 00000000..bd4c755e --- /dev/null +++ b/vendor/highlight/styles/ir-black.css @@ -0,0 +1,73 @@ +/* + IR_Black style (c) Vasily Mikhailitchenko +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #000; + color: #f8f8f8; +} + +.hljs-comment, +.hljs-quote, +.hljs-meta { + color: #7c7c7c; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-tag, +.hljs-name { + color: #96cbfe; +} + +.hljs-attribute, +.hljs-selector-id { + color: #ffffb6; +} + +.hljs-string, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-addition { + color: #a8ff60; +} + +.hljs-subst { + color: #daefa3; +} + +.hljs-regexp, +.hljs-link { + color: #e9c062; +} + +.hljs-title, +.hljs-section, +.hljs-type, +.hljs-doctag { + color: #ffffb6; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-variable, +.hljs-template-variable, +.hljs-literal { + color: #c6c5fe; +} + +.hljs-number, +.hljs-deletion { + color:#ff73fd; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/isbl-editor-dark.css b/vendor/highlight/styles/isbl-editor-dark.css new file mode 100644 index 00000000..2f1d95dd --- /dev/null +++ b/vendor/highlight/styles/isbl-editor-dark.css @@ -0,0 +1,112 @@ +/* + +ISBL Editor style dark color scheme (c) Dmitriy Tarasov + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #404040; + color: #f0f0f0; +} + +/* Base color: saturation 0; */ + +.hljs, +.hljs-subst { + color: #f0f0f0; +} + +.hljs-comment { + color: #b5b5b5; + font-style: italic; +} + +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + color: #f0f0f0; + font-weight: bold; +} + + +/* User color: hue: 0 */ + +.hljs-string { + color: #97bf0d; +} + +.hljs-type, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #f0f0f0; +} + +.hljs-title, +.hljs-section { + color: #df471e; +} + +.hljs-title>.hljs-built_in { + color: #81bce9; + font-weight: normal; +} + +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #e2c696; +} + +/* Language color: hue: 90; */ + +.hljs-built_in, +.hljs-literal { + color: #97bf0d; + font-weight: bold; +} + +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #397300; +} + +.hljs-class { + color: #ce9d4d; + font-weight: bold; +} + +/* Meta color: hue: 200 */ + +.hljs-meta { + color: #1f7199; +} + +.hljs-meta-string { + color: #4d99bf; +} + + +/* Misc effects */ + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/isbl-editor-light.css b/vendor/highlight/styles/isbl-editor-light.css new file mode 100644 index 00000000..633070dc --- /dev/null +++ b/vendor/highlight/styles/isbl-editor-light.css @@ -0,0 +1,112 @@ +/* + +ISBL Editor style light color schemec (c) Dmitriy Tarasov + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: white; + color: black; +} + +/* Base color: saturation 0; */ + +.hljs, +.hljs-subst { + color: #000000; +} + +.hljs-comment { + color: #555555; + font-style: italic; +} + +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + color: #000000; + font-weight: bold; +} + + +/* User color: hue: 0 */ + +.hljs-string { + color: #000080; +} + +.hljs-type, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #000000; +} + +.hljs-title, +.hljs-section { + color: #fb2c00; +} + +.hljs-title>.hljs-built_in { + color: #008080; + font-weight: normal; +} + +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #5e1700; +} + +/* Language color: hue: 90; */ + +.hljs-built_in, +.hljs-literal { + color: #000080; + font-weight: bold; +} + +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #397300; +} + +.hljs-class { + color: #6f1C00; + font-weight: bold; +} + +/* Meta color: hue: 200 */ + +.hljs-meta { + color: #1f7199; +} + +.hljs-meta-string { + color: #4d99bf; +} + + +/* Misc effects */ + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/kimbie.dark.css b/vendor/highlight/styles/kimbie.dark.css new file mode 100644 index 00000000..d139cb5d --- /dev/null +++ b/vendor/highlight/styles/kimbie.dark.css @@ -0,0 +1,74 @@ +/* + Name: Kimbie (dark) + Author: Jan T. Sott + License: Creative Commons Attribution-ShareAlike 4.0 Unported License + URL: https://github.com/idleberg/Kimbie-highlight.js +*/ + +/* Kimbie Comment */ +.hljs-comment, +.hljs-quote { + color: #d6baad; +} + +/* Kimbie Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-meta { + color: #dc3958; +} + +/* Kimbie Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-deletion, +.hljs-link { + color: #f79a32; +} + +/* Kimbie Yellow */ +.hljs-title, +.hljs-section, +.hljs-attribute { + color: #f06431; +} + +/* Kimbie Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #889b4a; +} + +/* Kimbie Purple */ +.hljs-keyword, +.hljs-selector-tag, +.hljs-function { + color: #98676a; +} + +.hljs { + display: block; + overflow-x: auto; + background: #221a0f; + color: #d3af86; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/kimbie.light.css b/vendor/highlight/styles/kimbie.light.css new file mode 100644 index 00000000..04ff6ed3 --- /dev/null +++ b/vendor/highlight/styles/kimbie.light.css @@ -0,0 +1,74 @@ +/* + Name: Kimbie (light) + Author: Jan T. Sott + License: Creative Commons Attribution-ShareAlike 4.0 Unported License + URL: https://github.com/idleberg/Kimbie-highlight.js +*/ + +/* Kimbie Comment */ +.hljs-comment, +.hljs-quote { + color: #a57a4c; +} + +/* Kimbie Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-meta { + color: #dc3958; +} + +/* Kimbie Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-deletion, +.hljs-link { + color: #f79a32; +} + +/* Kimbie Yellow */ +.hljs-title, +.hljs-section, +.hljs-attribute { + color: #f06431; +} + +/* Kimbie Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #889b4a; +} + +/* Kimbie Purple */ +.hljs-keyword, +.hljs-selector-tag, +.hljs-function { + color: #98676a; +} + +.hljs { + display: block; + overflow-x: auto; + background: #fbebd4; + color: #84613d; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/lightfair.css b/vendor/highlight/styles/lightfair.css new file mode 100644 index 00000000..a247c8eb --- /dev/null +++ b/vendor/highlight/styles/lightfair.css @@ -0,0 +1,87 @@ +/* + +Lightfair style (c) Tristian Kelly + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; +} + +.hljs-name { + color:#01a3a3; +} + +.hljs-tag,.hljs-meta { + color:#778899; +} + +.hljs, +.hljs-subst { + color: #444 +} + +.hljs-comment { + color: #888888 +} + +.hljs-keyword, +.hljs-attribute, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold +} + +.hljs-type, +.hljs-string, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #4286f4 +} + +.hljs-title, +.hljs-section { + color: #4286f4; + font-weight: bold +} + +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #BC6060 +} + +.hljs-literal { + color: #62bcbc +} + +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #25c6c6 +} + +.hljs-meta-string { + color: #4d99bf +} + +.hljs-emphasis { + font-style: italic +} + +.hljs-strong { + font-weight: bold +} diff --git a/vendor/highlight/styles/magula.css b/vendor/highlight/styles/magula.css new file mode 100644 index 00000000..44dee5e8 --- /dev/null +++ b/vendor/highlight/styles/magula.css @@ -0,0 +1,70 @@ +/* +Description: Magula style for highligh.js +Author: Ruslan Keba +Website: http://rukeba.com/ +Version: 1.0 +Date: 2009-01-03 +Music: Aphex Twin / Xtal +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background-color: #f4f4f4; +} + +.hljs, +.hljs-subst { + color: black; +} + +.hljs-string, +.hljs-title, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #050; +} + +.hljs-comment, +.hljs-quote { + color: #777; +} + +.hljs-number, +.hljs-regexp, +.hljs-literal, +.hljs-type, +.hljs-link { + color: #800; +} + +.hljs-deletion, +.hljs-meta { + color: #00e; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-doctag, +.hljs-title, +.hljs-section, +.hljs-built_in, +.hljs-tag, +.hljs-name { + font-weight: bold; + color: navy; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/mono-blue.css b/vendor/highlight/styles/mono-blue.css new file mode 100644 index 00000000..884c97c7 --- /dev/null +++ b/vendor/highlight/styles/mono-blue.css @@ -0,0 +1,59 @@ +/* + Five-color theme from a single blue hue. +*/ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #eaeef3; +} + +.hljs { + color: #00193a; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-title, +.hljs-section, +.hljs-doctag, +.hljs-name, +.hljs-strong { + font-weight: bold; +} + +.hljs-comment { + color: #738191; +} + +.hljs-string, +.hljs-title, +.hljs-section, +.hljs-built_in, +.hljs-literal, +.hljs-type, +.hljs-addition, +.hljs-tag, +.hljs-quote, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #0048ab; +} + +.hljs-meta, +.hljs-subst, +.hljs-symbol, +.hljs-regexp, +.hljs-attribute, +.hljs-deletion, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-bullet { + color: #4c81c9; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/monokai-sublime.css b/vendor/highlight/styles/monokai-sublime.css new file mode 100644 index 00000000..2864170d --- /dev/null +++ b/vendor/highlight/styles/monokai-sublime.css @@ -0,0 +1,83 @@ +/* + +Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #23241f; +} + +.hljs, +.hljs-tag, +.hljs-subst { + color: #f8f8f2; +} + +.hljs-strong, +.hljs-emphasis { + color: #a8a8a2; +} + +.hljs-bullet, +.hljs-quote, +.hljs-number, +.hljs-regexp, +.hljs-literal, +.hljs-link { + color: #ae81ff; +} + +.hljs-code, +.hljs-title, +.hljs-section, +.hljs-selector-class { + color: #a6e22e; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-name, +.hljs-attr { + color: #f92672; +} + +.hljs-symbol, +.hljs-attribute { + color: #66d9ef; +} + +.hljs-params, +.hljs-class .hljs-title { + color: #f8f8f2; +} + +.hljs-string, +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-selector-id, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-addition, +.hljs-variable, +.hljs-template-variable { + color: #e6db74; +} + +.hljs-comment, +.hljs-deletion, +.hljs-meta { + color: #75715e; +} diff --git a/vendor/highlight/styles/monokai.css b/vendor/highlight/styles/monokai.css new file mode 100644 index 00000000..775d53f9 --- /dev/null +++ b/vendor/highlight/styles/monokai.css @@ -0,0 +1,70 @@ +/* +Monokai style - ported by Luigi Maselli - http://grigio.org +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #272822; color: #ddd; +} + +.hljs-tag, +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-strong, +.hljs-name { + color: #f92672; +} + +.hljs-code { + color: #66d9ef; +} + +.hljs-class .hljs-title { + color: white; +} + +.hljs-attribute, +.hljs-symbol, +.hljs-regexp, +.hljs-link { + color: #bf79db; +} + +.hljs-string, +.hljs-bullet, +.hljs-subst, +.hljs-title, +.hljs-section, +.hljs-emphasis, +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #a6e22e; +} + +.hljs-comment, +.hljs-quote, +.hljs-deletion, +.hljs-meta { + color: #75715e; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-doctag, +.hljs-title, +.hljs-section, +.hljs-type, +.hljs-selector-id { + font-weight: bold; +} diff --git a/vendor/highlight/styles/nord.css b/vendor/highlight/styles/nord.css new file mode 100644 index 00000000..42403844 --- /dev/null +++ b/vendor/highlight/styles/nord.css @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2017-present Arctic Ice Studio + * Copyright (c) 2017-present Sven Greb + * + * Project: Nord highlight.js + * Version: 0.1.0 + * Repository: https://github.com/arcticicestudio/nord-highlightjs + * License: MIT + * References: + * https://github.com/arcticicestudio/nord + */ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #2E3440; +} + +.hljs, +.hljs-subst { + color: #D8DEE9; +} + +.hljs-selector-tag { + color: #81A1C1; +} + +.hljs-selector-id { + color: #8FBCBB; + font-weight: bold; +} + +.hljs-selector-class { + color: #8FBCBB; +} + +.hljs-selector-attr { + color: #8FBCBB; +} + +.hljs-selector-pseudo { + color: #88C0D0; +} + +.hljs-addition { + background-color: rgba(163, 190, 140, 0.5); +} + +.hljs-deletion { + background-color: rgba(191, 97, 106, 0.5); +} + +.hljs-built_in, +.hljs-type { + color: #8FBCBB; +} + +.hljs-class { + color: #8FBCBB; +} + +.hljs-function { + color: #88C0D0; +} + +.hljs-function > .hljs-title { + color: #88C0D0; +} + +.hljs-keyword, +.hljs-literal, +.hljs-symbol { + color: #81A1C1; +} + +.hljs-number { + color: #B48EAD; +} + +.hljs-regexp { + color: #EBCB8B; +} + +.hljs-string { + color: #A3BE8C; +} + +.hljs-title { + color: #8FBCBB; +} + +.hljs-params { + color: #D8DEE9; +} + +.hljs-bullet { + color: #81A1C1; +} + +.hljs-code { + color: #8FBCBB; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-formula { + color: #8FBCBB; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link:hover { + text-decoration: underline; +} + +.hljs-quote { + color: #4C566A; +} + +.hljs-comment { + color: #4C566A; +} + +.hljs-doctag { + color: #8FBCBB; +} + +.hljs-meta, +.hljs-meta-keyword { + color: #5E81AC; +} + +.hljs-meta-string { + color: #A3BE8C; +} + +.hljs-attr { + color: #8FBCBB; +} + +.hljs-attribute { + color: #D8DEE9; +} + +.hljs-builtin-name { + color: #81A1C1; +} + +.hljs-name { + color: #81A1C1; +} + +.hljs-section { + color: #88C0D0; +} + +.hljs-tag { + color: #81A1C1; +} + +.hljs-variable { + color: #D8DEE9; +} + +.hljs-template-variable { + color: #D8DEE9; +} + +.hljs-template-tag { + color: #5E81AC; +} + +.abnf .hljs-attribute { + color: #88C0D0; +} + +.abnf .hljs-symbol { + color: #EBCB8B; +} + +.apache .hljs-attribute { + color: #88C0D0; +} + +.apache .hljs-section { + color: #81A1C1; +} + +.arduino .hljs-built_in { + color: #88C0D0; +} + +.aspectj .hljs-meta { + color: #D08770; +} + +.aspectj > .hljs-title { + color: #88C0D0; +} + +.bnf .hljs-attribute { + color: #8FBCBB; +} + +.clojure .hljs-name { + color: #88C0D0; +} + +.clojure .hljs-symbol { + color: #EBCB8B; +} + +.coq .hljs-built_in { + color: #88C0D0; +} + +.cpp .hljs-meta-string { + color: #8FBCBB; +} + +.css .hljs-built_in { + color: #88C0D0; +} + +.css .hljs-keyword { + color: #D08770; +} + +.diff .hljs-meta { + color: #8FBCBB; +} + +.ebnf .hljs-attribute { + color: #8FBCBB; +} + +.glsl .hljs-built_in { + color: #88C0D0; +} + +.groovy .hljs-meta:not(:first-child) { + color: #D08770; +} + +.haxe .hljs-meta { + color: #D08770; +} + +.java .hljs-meta { + color: #D08770; +} + +.ldif .hljs-attribute { + color: #8FBCBB; +} + +.lisp .hljs-name { + color: #88C0D0; +} + +.lua .hljs-built_in { + color: #88C0D0; +} + +.moonscript .hljs-built_in { + color: #88C0D0; +} + +.nginx .hljs-attribute { + color: #88C0D0; +} + +.nginx .hljs-section { + color: #5E81AC; +} + +.pf .hljs-built_in { + color: #88C0D0; +} + +.processing .hljs-built_in { + color: #88C0D0; +} + +.scss .hljs-keyword { + color: #81A1C1; +} + +.stylus .hljs-keyword { + color: #81A1C1; +} + +.swift .hljs-meta { + color: #D08770; +} + +.vim .hljs-built_in { + color: #88C0D0; + font-style: italic; +} + +.yaml .hljs-meta { + color: #D08770; +} diff --git a/vendor/highlight/styles/obsidian.css b/vendor/highlight/styles/obsidian.css new file mode 100644 index 00000000..356630fa --- /dev/null +++ b/vendor/highlight/styles/obsidian.css @@ -0,0 +1,88 @@ +/** + * Obsidian style + * ported by Alexander Marenin (http://github.com/ioncreature) + */ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #282b2e; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-selector-id { + color: #93c763; +} + +.hljs-number { + color: #ffcd22; +} + +.hljs { + color: #e0e2e4; +} + +.hljs-attribute { + color: #668bb0; +} + +.hljs-code, +.hljs-class .hljs-title, +.hljs-section { + color: white; +} + +.hljs-regexp, +.hljs-link { + color: #d39745; +} + +.hljs-meta { + color: #557182; +} + +.hljs-tag, +.hljs-name, +.hljs-bullet, +.hljs-subst, +.hljs-emphasis, +.hljs-type, +.hljs-built_in, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #8cbbad; +} + +.hljs-string, +.hljs-symbol { + color: #ec7600; +} + +.hljs-comment, +.hljs-quote, +.hljs-deletion { + color: #818e96; +} + +.hljs-selector-class { + color: #A082BD +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-doctag, +.hljs-title, +.hljs-section, +.hljs-type, +.hljs-name, +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/ocean.css b/vendor/highlight/styles/ocean.css new file mode 100644 index 00000000..5901581b --- /dev/null +++ b/vendor/highlight/styles/ocean.css @@ -0,0 +1,74 @@ +/* Ocean Dark Theme */ +/* https://github.com/gavsiu */ +/* Original theme - https://github.com/chriskempson/base16 */ + +/* Ocean Comment */ +.hljs-comment, +.hljs-quote { + color: #65737e; +} + +/* Ocean Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion { + color: #bf616a; +} + +/* Ocean Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link { + color: #d08770; +} + +/* Ocean Yellow */ +.hljs-attribute { + color: #ebcb8b; +} + +/* Ocean Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #a3be8c; +} + +/* Ocean Blue */ +.hljs-title, +.hljs-section { + color: #8fa1b3; +} + +/* Ocean Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #b48ead; +} + +.hljs { + display: block; + overflow-x: auto; + background: #2b303b; + color: #c0c5ce; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/paraiso-dark.css b/vendor/highlight/styles/paraiso-dark.css new file mode 100644 index 00000000..e7292401 --- /dev/null +++ b/vendor/highlight/styles/paraiso-dark.css @@ -0,0 +1,72 @@ +/* + Paraíso (dark) + Created by Jan T. Sott (http://github.com/idleberg) + Inspired by the art of Rubens LP (http://www.rubenslp.com.br) +*/ + +/* Paraíso Comment */ +.hljs-comment, +.hljs-quote { + color: #8d8687; +} + +/* Paraíso Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-link, +.hljs-meta { + color: #ef6155; +} + +/* Paraíso Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-deletion { + color: #f99b15; +} + +/* Paraíso Yellow */ +.hljs-title, +.hljs-section, +.hljs-attribute { + color: #fec418; +} + +/* Paraíso Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #48b685; +} + +/* Paraíso Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #815ba4; +} + +.hljs { + display: block; + overflow-x: auto; + background: #2f1e2e; + color: #a39e9b; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/paraiso-light.css b/vendor/highlight/styles/paraiso-light.css new file mode 100644 index 00000000..944857cd --- /dev/null +++ b/vendor/highlight/styles/paraiso-light.css @@ -0,0 +1,72 @@ +/* + Paraíso (light) + Created by Jan T. Sott (http://github.com/idleberg) + Inspired by the art of Rubens LP (http://www.rubenslp.com.br) +*/ + +/* Paraíso Comment */ +.hljs-comment, +.hljs-quote { + color: #776e71; +} + +/* Paraíso Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-link, +.hljs-meta { + color: #ef6155; +} + +/* Paraíso Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-deletion { + color: #f99b15; +} + +/* Paraíso Yellow */ +.hljs-title, +.hljs-section, +.hljs-attribute { + color: #fec418; +} + +/* Paraíso Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #48b685; +} + +/* Paraíso Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #815ba4; +} + +.hljs { + display: block; + overflow-x: auto; + background: #e7e9db; + color: #4f424c; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/pojoaque.css b/vendor/highlight/styles/pojoaque.css new file mode 100644 index 00000000..2e07847b --- /dev/null +++ b/vendor/highlight/styles/pojoaque.css @@ -0,0 +1,83 @@ +/* + +Pojoaque Style by Jason Tate +http://web-cms-designs.com/ftopict-10-pojoaque-style-for-highlight-js-code-highlighter.html +Based on Solarized Style from http://ethanschoonover.com/solarized + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #dccf8f; + background: url(./pojoaque.jpg) repeat scroll left top #181914; +} + +.hljs-comment, +.hljs-quote { + color: #586e75; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-addition { + color: #b64926; +} + +.hljs-number, +.hljs-string, +.hljs-doctag, +.hljs-regexp { + color: #468966; +} + +.hljs-title, +.hljs-section, +.hljs-built_in, +.hljs-name { + color: #ffb03b; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-class .hljs-title, +.hljs-type, +.hljs-tag { + color: #b58900; +} + +.hljs-attribute { + color: #b89859; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-subst, +.hljs-meta { + color: #cb4b16; +} + +.hljs-deletion { + color: #dc322f; +} + +.hljs-selector-id, +.hljs-selector-class { + color: #d3a60c; +} + +.hljs-formula { + background: #073642; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/pojoaque.jpg b/vendor/highlight/styles/pojoaque.jpg new file mode 100644 index 00000000..9c07d4ab Binary files /dev/null and b/vendor/highlight/styles/pojoaque.jpg differ diff --git a/vendor/highlight/styles/purebasic.css b/vendor/highlight/styles/purebasic.css new file mode 100644 index 00000000..5ce9b9e0 --- /dev/null +++ b/vendor/highlight/styles/purebasic.css @@ -0,0 +1,96 @@ +/* + +PureBASIC native IDE style ( version 1.0 - April 2016 ) + +by Tristano Ajmone + +Public Domain + +NOTE_1: PureBASIC code syntax highlighting only applies the following classes: + .hljs-comment + .hljs-function + .hljs-keywords + .hljs-string + .hljs-symbol + + Other classes are added here for the benefit of styling other languages with the look and feel of PureBASIC native IDE style. + If you need to customize a stylesheet for PureBASIC only, remove all non-relevant classes -- PureBASIC-related classes are followed by + a "--- used for PureBASIC ... ---" comment on same line. + +NOTE_2: Color names provided in comments were derived using "Name that Color" online tool: + http://chir.ag/projects/name-that-color +*/ + +.hljs { /* Common set of rules required by highlight.js (don'r remove!) */ + display: block; + overflow-x: auto; + padding: 0.5em; + background: #FFFFDF; /* Half and Half (approx.) */ +/* --- Uncomment to add PureBASIC native IDE styled font! + font-family: Consolas; +*/ +} + +.hljs, /* --- used for PureBASIC base color --- */ +.hljs-type, /* --- used for PureBASIC Procedures return type --- */ +.hljs-function, /* --- used for wrapping PureBASIC Procedures definitions --- */ +.hljs-name, +.hljs-number, +.hljs-attr, +.hljs-params, +.hljs-subst { + color: #000000; /* Black */ +} + +.hljs-comment, /* --- used for PureBASIC Comments --- */ +.hljs-regexp, +.hljs-section, +.hljs-selector-pseudo, +.hljs-addition { + color: #00AAAA; /* Persian Green (approx.) */ +} + +.hljs-title, /* --- used for PureBASIC Procedures Names --- */ +.hljs-tag, +.hljs-variable, +.hljs-code { + color: #006666; /* Blue Stone (approx.) */ +} + +.hljs-keyword, /* --- used for PureBASIC Keywords --- */ +.hljs-class, +.hljs-meta-keyword, +.hljs-selector-class, +.hljs-built_in, +.hljs-builtin-name { + color: #006666; /* Blue Stone (approx.) */ + font-weight: bold; +} + +.hljs-string, /* --- used for PureBASIC Strings --- */ +.hljs-selector-attr { + color: #0080FF; /* Azure Radiance (approx.) */ +} + +.hljs-symbol, /* --- used for PureBASIC Constants --- */ +.hljs-link, +.hljs-deletion, +.hljs-attribute { + color: #924B72; /* Cannon Pink (approx.) */ +} + +.hljs-meta, +.hljs-literal, +.hljs-selector-id { + color: #924B72; /* Cannon Pink (approx.) */ + font-weight: bold; +} + +.hljs-strong, +.hljs-name { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/qtcreator_dark.css b/vendor/highlight/styles/qtcreator_dark.css new file mode 100644 index 00000000..7aa56a36 --- /dev/null +++ b/vendor/highlight/styles/qtcreator_dark.css @@ -0,0 +1,83 @@ +/* + +Qt Creator dark color scheme + +*/ + + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #000000; +} + +.hljs, +.hljs-subst, +.hljs-tag, +.hljs-title { + color: #aaaaaa; +} + +.hljs-strong, +.hljs-emphasis { + color: #a8a8a2; +} + +.hljs-bullet, +.hljs-quote, +.hljs-number, +.hljs-regexp, +.hljs-literal { + color: #ff55ff; +} + +.hljs-code +.hljs-selector-class { + color: #aaaaff; +} + +.hljs-emphasis, +.hljs-stronge, +.hljs-type { + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-function, +.hljs-section, +.hljs-symbol, +.hljs-name { + color: #ffff55; +} + +.hljs-attribute { + color: #ff5555; +} + +.hljs-variable, +.hljs-params, +.hljs-class .hljs-title { + color: #8888ff; +} + +.hljs-string, +.hljs-selector-id, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-template-tag, +.hljs-template-variable, +.hljs-addition, +.hljs-link { + color: #ff55ff; +} + +.hljs-comment, +.hljs-meta, +.hljs-deletion { + color: #55ffff; +} diff --git a/vendor/highlight/styles/qtcreator_light.css b/vendor/highlight/styles/qtcreator_light.css new file mode 100644 index 00000000..1efa2c66 --- /dev/null +++ b/vendor/highlight/styles/qtcreator_light.css @@ -0,0 +1,83 @@ +/* + +Qt Creator light color scheme + +*/ + + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #ffffff; +} + +.hljs, +.hljs-subst, +.hljs-tag, +.hljs-title { + color: #000000; +} + +.hljs-strong, +.hljs-emphasis { + color: #000000; +} + +.hljs-bullet, +.hljs-quote, +.hljs-number, +.hljs-regexp, +.hljs-literal { + color: #000080; +} + +.hljs-code +.hljs-selector-class { + color: #800080; +} + +.hljs-emphasis, +.hljs-stronge, +.hljs-type { + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-function, +.hljs-section, +.hljs-symbol, +.hljs-name { + color: #808000; +} + +.hljs-attribute { + color: #800000; +} + +.hljs-variable, +.hljs-params, +.hljs-class .hljs-title { + color: #0055AF; +} + +.hljs-string, +.hljs-selector-id, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-template-tag, +.hljs-template-variable, +.hljs-addition, +.hljs-link { + color: #008000; +} + +.hljs-comment, +.hljs-meta, +.hljs-deletion { + color: #008000; +} diff --git a/vendor/highlight/styles/railscasts.css b/vendor/highlight/styles/railscasts.css new file mode 100644 index 00000000..008cdc5b --- /dev/null +++ b/vendor/highlight/styles/railscasts.css @@ -0,0 +1,106 @@ +/* + +Railscasts-like style (c) Visoft, Inc. (Damien White) + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #232323; + color: #e6e1dc; +} + +.hljs-comment, +.hljs-quote { + color: #bc9458; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag { + color: #c26230; +} + +.hljs-string, +.hljs-number, +.hljs-regexp, +.hljs-variable, +.hljs-template-variable { + color: #a5c261; +} + +.hljs-subst { + color: #519f50; +} + +.hljs-tag, +.hljs-name { + color: #e8bf6a; +} + +.hljs-type { + color: #da4939; +} + + +.hljs-symbol, +.hljs-bullet, +.hljs-built_in, +.hljs-builtin-name, +.hljs-attr, +.hljs-link { + color: #6d9cbe; +} + +.hljs-params { + color: #d0d0ff; +} + +.hljs-attribute { + color: #cda869; +} + +.hljs-meta { + color: #9b859d; +} + +.hljs-title, +.hljs-section { + color: #ffc66d; +} + +.hljs-addition { + background-color: #144212; + color: #e6e1dc; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #600; + color: #e6e1dc; + display: inline-block; + width: 100%; +} + +.hljs-selector-class { + color: #9b703f; +} + +.hljs-selector-id { + color: #8b98ab; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/vendor/highlight/styles/rainbow.css b/vendor/highlight/styles/rainbow.css new file mode 100644 index 00000000..905eb8ef --- /dev/null +++ b/vendor/highlight/styles/rainbow.css @@ -0,0 +1,85 @@ +/* + +Style with support for rainbow parens + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #474949; + color: #d1d9e1; +} + + +.hljs-comment, +.hljs-quote { + color: #969896; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-type, +.hljs-addition { + color: #cc99cc; +} + +.hljs-number, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #f99157; +} + +.hljs-string, +.hljs-doctag, +.hljs-regexp { + color: #8abeb7; +} + +.hljs-title, +.hljs-name, +.hljs-section, +.hljs-built_in { + color: #b5bd68; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-selector-id, +.hljs-class .hljs-title { + color: #ffcc66; +} + +.hljs-section, +.hljs-name, +.hljs-strong { + font-weight: bold; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-subst, +.hljs-meta, +.hljs-link { + color: #f99157; +} + +.hljs-deletion { + color: #dc322f; +} + +.hljs-formula { + background: #eee8d5; +} + +.hljs-attr, +.hljs-attribute { + color: #81a2be; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/routeros.css b/vendor/highlight/styles/routeros.css new file mode 100644 index 00000000..ebe23990 --- /dev/null +++ b/vendor/highlight/styles/routeros.css @@ -0,0 +1,108 @@ +/* + + highlight.js style for Microtik RouterOS script + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #F0F0F0; +} + +/* Base color: saturation 0; */ + +.hljs, +.hljs-subst { + color: #444; +} + +.hljs-comment { + color: #888888; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-meta-keyword, +.hljs-doctag, +.hljs-name { + font-weight: bold; +} + +.hljs-attribute { + color: #0E9A00; +} + +.hljs-function { + color: #99069A; +} + +.hljs-builtin-name { + color: #99069A; +} + +/* User color: hue: 0 */ + +.hljs-type, +.hljs-string, +.hljs-number, +.hljs-selector-id, +.hljs-selector-class, +.hljs-quote, +.hljs-template-tag, +.hljs-deletion { + color: #880000; +} + +.hljs-title, +.hljs-section { + color: #880000; + font-weight: bold; +} + +.hljs-regexp, +.hljs-symbol, +.hljs-variable, +.hljs-template-variable, +.hljs-link, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #BC6060; +} + + +/* Language color: hue: 90; */ + +.hljs-literal { + color: #78A960; +} + +.hljs-built_in, +.hljs-bullet, +.hljs-code, +.hljs-addition { + color: #0C9A9A; +} + + +/* Meta color: hue: 200 */ + +.hljs-meta { + color: #1f7199; +} + +.hljs-meta-string { + color: #4d99bf; +} + + +/* Misc effects */ + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/school-book.css b/vendor/highlight/styles/school-book.css new file mode 100644 index 00000000..94838513 --- /dev/null +++ b/vendor/highlight/styles/school-book.css @@ -0,0 +1,69 @@ +/* + +School Book style from goldblog.com.ua (c) Zaripov Yura + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 15px 0.5em 0.5em 30px; + font-size: 11px; + line-height:16px; + background:#f6f6ae url(./school-book.png); + border-top: solid 2px #d2e8b9; + border-bottom: solid 1px #d2e8b9; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal { + color:#005599; + font-weight:bold; +} + +.hljs, +.hljs-subst { + color: #3e5915; +} + +.hljs-string, +.hljs-title, +.hljs-section, +.hljs-type, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute, +.hljs-built_in, +.hljs-builtin-name, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable, +.hljs-link { + color: #2c009f; +} + +.hljs-comment, +.hljs-quote, +.hljs-deletion, +.hljs-meta { + color: #e60415; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-doctag, +.hljs-title, +.hljs-section, +.hljs-type, +.hljs-name, +.hljs-selector-id, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/school-book.png b/vendor/highlight/styles/school-book.png new file mode 100644 index 00000000..956e9790 Binary files /dev/null and b/vendor/highlight/styles/school-book.png differ diff --git a/vendor/highlight/styles/shades-of-purple.css b/vendor/highlight/styles/shades-of-purple.css new file mode 100644 index 00000000..c0e899e0 --- /dev/null +++ b/vendor/highlight/styles/shades-of-purple.css @@ -0,0 +1,97 @@ +/** + * Shades of Purple Theme — for Highlightjs. + * + * @author (c) Ahmad Awais + * @link GitHub Repo → https://github.com/ahmadawais/Shades-of-Purple-HighlightJS + * @version 1.5.0 + */ + +.hljs { + display: block; + overflow-x: auto; + /* Custom font is optional */ + /* font-family: 'Operator Mono', 'Fira Code', 'Menlo', 'Monaco', 'Courier New', 'monospace'; */ + line-height: 1.45; + padding: 2rem; + background: #2d2b57; + font-weight: normal; +} + +.hljs-title { + color: #fad000; + font-weight: normal; +} + +.hljs-name { + color: #a1feff; +} + +.hljs-tag { + color: #ffffff; +} + +.hljs-attr { + color: #f8d000; + font-style: italic; +} + +.hljs-built_in, +.hljs-selector-tag, +.hljs-section { + color: #fb9e00; +} + +.hljs-keyword { + color: #fb9e00; +} + +.hljs, +.hljs-subst { + color: #e3dfff; +} + +.hljs-string, +.hljs-attribute, +.hljs-symbol, +.hljs-bullet, +.hljs-addition, +.hljs-code, +.hljs-regexp, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-template-tag, +.hljs-quote, +.hljs-deletion { + color: #4cd213; +} + +.hljs-meta, +.hljs-meta-string { + color: #fb9e00; +} + +.hljs-comment { + color: #ac65ff; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-name, +.hljs-strong { + font-weight: normal; +} + +.hljs-literal, +.hljs-number { + color: #fa658d; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/solarized-dark.css b/vendor/highlight/styles/solarized-dark.css new file mode 100644 index 00000000..b4c0da1f --- /dev/null +++ b/vendor/highlight/styles/solarized-dark.css @@ -0,0 +1,84 @@ +/* + +Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #002b36; + color: #839496; +} + +.hljs-comment, +.hljs-quote { + color: #586e75; +} + +/* Solarized Green */ +.hljs-keyword, +.hljs-selector-tag, +.hljs-addition { + color: #859900; +} + +/* Solarized Cyan */ +.hljs-number, +.hljs-string, +.hljs-meta .hljs-meta-string, +.hljs-literal, +.hljs-doctag, +.hljs-regexp { + color: #2aa198; +} + +/* Solarized Blue */ +.hljs-title, +.hljs-section, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #268bd2; +} + +/* Solarized Yellow */ +.hljs-attribute, +.hljs-attr, +.hljs-variable, +.hljs-template-variable, +.hljs-class .hljs-title, +.hljs-type { + color: #b58900; +} + +/* Solarized Orange */ +.hljs-symbol, +.hljs-bullet, +.hljs-subst, +.hljs-meta, +.hljs-meta .hljs-keyword, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-link { + color: #cb4b16; +} + +/* Solarized Red */ +.hljs-built_in, +.hljs-deletion { + color: #dc322f; +} + +.hljs-formula { + background: #073642; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/solarized-light.css b/vendor/highlight/styles/solarized-light.css new file mode 100644 index 00000000..fdcfcc72 --- /dev/null +++ b/vendor/highlight/styles/solarized-light.css @@ -0,0 +1,84 @@ +/* + +Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #fdf6e3; + color: #657b83; +} + +.hljs-comment, +.hljs-quote { + color: #93a1a1; +} + +/* Solarized Green */ +.hljs-keyword, +.hljs-selector-tag, +.hljs-addition { + color: #859900; +} + +/* Solarized Cyan */ +.hljs-number, +.hljs-string, +.hljs-meta .hljs-meta-string, +.hljs-literal, +.hljs-doctag, +.hljs-regexp { + color: #2aa198; +} + +/* Solarized Blue */ +.hljs-title, +.hljs-section, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #268bd2; +} + +/* Solarized Yellow */ +.hljs-attribute, +.hljs-attr, +.hljs-variable, +.hljs-template-variable, +.hljs-class .hljs-title, +.hljs-type { + color: #b58900; +} + +/* Solarized Orange */ +.hljs-symbol, +.hljs-bullet, +.hljs-subst, +.hljs-meta, +.hljs-meta .hljs-keyword, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-link { + color: #cb4b16; +} + +/* Solarized Red */ +.hljs-built_in, +.hljs-deletion { + color: #dc322f; +} + +.hljs-formula { + background: #eee8d5; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/sunburst.css b/vendor/highlight/styles/sunburst.css new file mode 100644 index 00000000..f56dd5e9 --- /dev/null +++ b/vendor/highlight/styles/sunburst.css @@ -0,0 +1,102 @@ +/* + +Sunburst-like style (c) Vasily Polovnyov + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #000; + color: #f8f8f8; +} + +.hljs-comment, +.hljs-quote { + color: #aeaeae; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #e28964; +} + +.hljs-string { + color: #65b042; +} + +.hljs-subst { + color: #daefa3; +} + +.hljs-regexp, +.hljs-link { + color: #e9c062; +} + +.hljs-title, +.hljs-section, +.hljs-tag, +.hljs-name { + color: #89bdff; +} + +.hljs-class .hljs-title, +.hljs-doctag { + text-decoration: underline; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-number { + color: #3387cc; +} + +.hljs-params, +.hljs-variable, +.hljs-template-variable { + color: #3e87e3; +} + +.hljs-attribute { + color: #cda869; +} + +.hljs-meta { + color: #8996a8; +} + +.hljs-formula { + background-color: #0e2231; + color: #f8f8f8; + font-style: italic; +} + +.hljs-addition { + background-color: #253b22; + color: #f8f8f8; +} + +.hljs-deletion { + background-color: #420e09; + color: #f8f8f8; +} + +.hljs-selector-class { + color: #9b703f; +} + +.hljs-selector-id { + color: #8b98ab; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/tomorrow-night-blue.css b/vendor/highlight/styles/tomorrow-night-blue.css new file mode 100644 index 00000000..78e59cc8 --- /dev/null +++ b/vendor/highlight/styles/tomorrow-night-blue.css @@ -0,0 +1,75 @@ +/* Tomorrow Night Blue Theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment, +.hljs-quote { + color: #7285b7; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion { + color: #ff9da4; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link { + color: #ffc58f; +} + +/* Tomorrow Yellow */ +.hljs-attribute { + color: #ffeead; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #d1f1a9; +} + +/* Tomorrow Blue */ +.hljs-title, +.hljs-section { + color: #bbdaff; +} + +/* Tomorrow Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #ebbbff; +} + +.hljs { + display: block; + overflow-x: auto; + background: #002451; + color: white; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/tomorrow-night-bright.css b/vendor/highlight/styles/tomorrow-night-bright.css new file mode 100644 index 00000000..e05af8ae --- /dev/null +++ b/vendor/highlight/styles/tomorrow-night-bright.css @@ -0,0 +1,74 @@ +/* Tomorrow Night Bright Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment, +.hljs-quote { + color: #969896; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion { + color: #d54e53; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link { + color: #e78c45; +} + +/* Tomorrow Yellow */ +.hljs-attribute { + color: #e7c547; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #b9ca4a; +} + +/* Tomorrow Blue */ +.hljs-title, +.hljs-section { + color: #7aa6da; +} + +/* Tomorrow Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #c397d8; +} + +.hljs { + display: block; + overflow-x: auto; + background: black; + color: #eaeaea; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/tomorrow-night-eighties.css b/vendor/highlight/styles/tomorrow-night-eighties.css new file mode 100644 index 00000000..08fd51c7 --- /dev/null +++ b/vendor/highlight/styles/tomorrow-night-eighties.css @@ -0,0 +1,74 @@ +/* Tomorrow Night Eighties Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment, +.hljs-quote { + color: #999999; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion { + color: #f2777a; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link { + color: #f99157; +} + +/* Tomorrow Yellow */ +.hljs-attribute { + color: #ffcc66; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #99cc99; +} + +/* Tomorrow Blue */ +.hljs-title, +.hljs-section { + color: #6699cc; +} + +/* Tomorrow Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #cc99cc; +} + +.hljs { + display: block; + overflow-x: auto; + background: #2d2d2d; + color: #cccccc; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/tomorrow-night.css b/vendor/highlight/styles/tomorrow-night.css new file mode 100644 index 00000000..ddd270a4 --- /dev/null +++ b/vendor/highlight/styles/tomorrow-night.css @@ -0,0 +1,75 @@ +/* Tomorrow Night Theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment, +.hljs-quote { + color: #969896; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion { + color: #cc6666; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link { + color: #de935f; +} + +/* Tomorrow Yellow */ +.hljs-attribute { + color: #f0c674; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #b5bd68; +} + +/* Tomorrow Blue */ +.hljs-title, +.hljs-section { + color: #81a2be; +} + +/* Tomorrow Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #b294bb; +} + +.hljs { + display: block; + overflow-x: auto; + background: #1d1f21; + color: #c5c8c6; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/tomorrow.css b/vendor/highlight/styles/tomorrow.css new file mode 100644 index 00000000..026a62fe --- /dev/null +++ b/vendor/highlight/styles/tomorrow.css @@ -0,0 +1,72 @@ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment, +.hljs-quote { + color: #8e908c; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion { + color: #c82829; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link { + color: #f5871f; +} + +/* Tomorrow Yellow */ +.hljs-attribute { + color: #eab700; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #718c00; +} + +/* Tomorrow Blue */ +.hljs-title, +.hljs-section { + color: #4271ae; +} + +/* Tomorrow Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #8959a8; +} + +.hljs { + display: block; + overflow-x: auto; + background: white; + color: #4d4d4c; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/vs.css b/vendor/highlight/styles/vs.css new file mode 100644 index 00000000..c5d07d31 --- /dev/null +++ b/vendor/highlight/styles/vs.css @@ -0,0 +1,68 @@ +/* + +Visual Studio-like style based on original C# coloring by Jason Diamond + +*/ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: white; + color: black; +} + +.hljs-comment, +.hljs-quote, +.hljs-variable { + color: #008000; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-built_in, +.hljs-name, +.hljs-tag { + color: #00f; +} + +.hljs-string, +.hljs-title, +.hljs-section, +.hljs-attribute, +.hljs-literal, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-addition { + color: #a31515; +} + +.hljs-deletion, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-meta { + color: #2b91af; +} + +.hljs-doctag { + color: #808080; +} + +.hljs-attr { + color: #f00; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link { + color: #00b0e8; +} + + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/highlight/styles/vs2015.css b/vendor/highlight/styles/vs2015.css new file mode 100644 index 00000000..d1d9be3c --- /dev/null +++ b/vendor/highlight/styles/vs2015.css @@ -0,0 +1,115 @@ +/* + * Visual Studio 2015 dark style + * Author: Nicolas LLOBERA + */ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #1E1E1E; + color: #DCDCDC; +} + +.hljs-keyword, +.hljs-literal, +.hljs-symbol, +.hljs-name { + color: #569CD6; +} +.hljs-link { + color: #569CD6; + text-decoration: underline; +} + +.hljs-built_in, +.hljs-type { + color: #4EC9B0; +} + +.hljs-number, +.hljs-class { + color: #B8D7A3; +} + +.hljs-string, +.hljs-meta-string { + color: #D69D85; +} + +.hljs-regexp, +.hljs-template-tag { + color: #9A5334; +} + +.hljs-subst, +.hljs-function, +.hljs-title, +.hljs-params, +.hljs-formula { + color: #DCDCDC; +} + +.hljs-comment, +.hljs-quote { + color: #57A64A; + font-style: italic; +} + +.hljs-doctag { + color: #608B4E; +} + +.hljs-meta, +.hljs-meta-keyword, +.hljs-tag { + color: #9B9B9B; +} + +.hljs-variable, +.hljs-template-variable { + color: #BD63C5; +} + +.hljs-attr, +.hljs-attribute, +.hljs-builtin-name { + color: #9CDCFE; +} + +.hljs-section { + color: gold; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +/*.hljs-code { + font-family:'Monospace'; +}*/ + +.hljs-bullet, +.hljs-selector-tag, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #D7BA7D; +} + +.hljs-addition { + background-color: #144212; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #600; + display: inline-block; + width: 100%; +} diff --git a/vendor/highlight/styles/xcode.css b/vendor/highlight/styles/xcode.css new file mode 100644 index 00000000..b3056655 --- /dev/null +++ b/vendor/highlight/styles/xcode.css @@ -0,0 +1,104 @@ +/* + +XCode style (c) Angel Garcia + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #fff; + color: black; +} + +/* Gray DOCTYPE selectors like WebKit */ +.xml .hljs-meta { + color: #c0c0c0; +} + +.hljs-comment, +.hljs-quote { + color: #007400; +} + +.hljs-tag, +.hljs-attribute, +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-name { + color: #aa0d91; +} + +.hljs-variable, +.hljs-template-variable { + color: #3F6E74; +} + +.hljs-code, +.hljs-string, +.hljs-meta-string { + color: #c41a16; +} + +.hljs-regexp, +.hljs-link { + color: #0E0EFF; +} + +.hljs-title, +.hljs-symbol, +.hljs-bullet, +.hljs-number { + color: #1c00cf; +} + +.hljs-section, +.hljs-meta { + color: #643820; +} + + +.hljs-class .hljs-title, +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-params { + color: #5c2699; +} + +.hljs-attr { + color: #836C28; +} + +.hljs-subst { + color: #000; +} + +.hljs-formula { + background-color: #eee; + font-style: italic; +} + +.hljs-addition { + background-color: #baeeba; +} + +.hljs-deletion { + background-color: #ffc8bd; +} + +.hljs-selector-id, +.hljs-selector-class { + color: #9b703f; +} + +.hljs-doctag, +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} diff --git a/vendor/highlight/styles/xt256.css b/vendor/highlight/styles/xt256.css new file mode 100644 index 00000000..3e35ad2d --- /dev/null +++ b/vendor/highlight/styles/xt256.css @@ -0,0 +1,92 @@ + +/* + xt256.css + + Contact: initbar [at] protonmail [dot] ch + : github.com/initbar +*/ + +.hljs { + display: block; + overflow-x: auto; + color: #eaeaea; + background: #000; + padding: 0.5em; +} + +.hljs-subst { + color: #eaeaea; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-builtin-name, +.hljs-type { + color: #eaeaea; +} + +.hljs-params { + color: #da0000; +} + +.hljs-literal, +.hljs-number, +.hljs-name { + color: #ff0000; + font-weight: bolder; +} + +.hljs-comment { + color: #969896; +} + +.hljs-selector-id, +.hljs-quote { + color: #00ffff; +} + +.hljs-template-variable, +.hljs-variable, +.hljs-title { + color: #00ffff; + font-weight: bold; +} + +.hljs-selector-class, +.hljs-keyword, +.hljs-symbol { + color: #fff000; +} + +.hljs-string, +.hljs-bullet { + color: #00ff00; +} + +.hljs-tag, +.hljs-section { + color: #000fff; +} + +.hljs-selector-tag { + color: #000fff; + font-weight: bold; +} + +.hljs-attribute, +.hljs-built_in, +.hljs-regexp, +.hljs-link { + color: #ff00ff; +} + +.hljs-meta { + color: #fff; + font-weight: bolder; +} diff --git a/vendor/highlight/styles/zenburn.css b/vendor/highlight/styles/zenburn.css new file mode 100644 index 00000000..07be5020 --- /dev/null +++ b/vendor/highlight/styles/zenburn.css @@ -0,0 +1,80 @@ +/* + +Zenburn style from voldmar.ru (c) Vladimir Epifanov +based on dark.css by Ivan Sagalaev + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #3f3f3f; + color: #dcdcdc; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-tag { + color: #e3ceab; +} + +.hljs-template-tag { + color: #dcdcdc; +} + +.hljs-number { + color: #8cd0d3; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-attribute { + color: #efdcbc; +} + +.hljs-literal { + color: #efefaf; +} + +.hljs-subst { + color: #8f8f8f; +} + +.hljs-title, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-section, +.hljs-type { + color: #efef8f; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link { + color: #dca3a3; +} + +.hljs-deletion, +.hljs-string, +.hljs-built_in, +.hljs-builtin-name { + color: #cc9393; +} + +.hljs-addition, +.hljs-comment, +.hljs-quote, +.hljs-meta { + color: #7f9f7f; +} + + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/vendor/popper/popper.js b/vendor/popper/popper.js deleted file mode 100644 index 82cf3830..00000000 --- a/vendor/popper/popper.js +++ /dev/null @@ -1,2442 +0,0 @@ -/**! - * @fileOverview Kickass library to create and place poppers near their reference elements. - * @version 1.12.6 - * @license - * Copyright (c) 2016 Federico Zivolo and contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.Popper = factory()); -}(this, (function () { 'use strict'; - - var isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; - var longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox']; - var timeoutDuration = 0; - for (var i = 0; i < longerTimeoutBrowsers.length; i += 1) { - if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) { - timeoutDuration = 1; - break; - } - } - - function microtaskDebounce(fn) { - var called = false; - return function () { - if (called) { - return; - } - called = true; - Promise.resolve().then(function () { - called = false; - fn(); - }); - }; - } - - function taskDebounce(fn) { - var scheduled = false; - return function () { - if (!scheduled) { - scheduled = true; - setTimeout(function () { - scheduled = false; - fn(); - }, timeoutDuration); - } - }; - } - - var supportsMicroTasks = isBrowser && window.Promise; - - /** - * Create a debounced version of a method, that's asynchronously deferred - * but called in the minimum time possible. - * - * @method - * @memberof Popper.Utils - * @argument {Function} fn - * @returns {Function} - */ - var debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce; - - /** - * Check if the given variable is a function - * @method - * @memberof Popper.Utils - * @argument {Any} functionToCheck - variable to check - * @returns {Boolean} answer to: is a function? - */ - function isFunction(functionToCheck) { - var getType = {}; - return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; - } - - /** - * Get CSS computed property of the given element - * @method - * @memberof Popper.Utils - * @argument {Eement} element - * @argument {String} property - */ - function getStyleComputedProperty(element, property) { - if (element.nodeType !== 1) { - return []; - } - // NOTE: 1 DOM access here - var css = window.getComputedStyle(element, null); - return property ? css[property] : css; - } - - /** - * Returns the parentNode or the host of the element - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Element} parent - */ - function getParentNode(element) { - if (element.nodeName === 'HTML') { - return element; - } - return element.parentNode || element.host; - } - - /** - * Returns the scrolling parent of the given element - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Element} scroll parent - */ - function getScrollParent(element) { - // Return body, `getScroll` will take care to get the correct `scrollTop` from it - if (!element) { - return window.document.body; - } - - switch (element.nodeName) { - case 'HTML': - case 'BODY': - return element.ownerDocument.body; - case '#document': - return element.body; - } - - // Firefox want us to check `-x` and `-y` variations as well - - var _getStyleComputedProp = getStyleComputedProperty(element), - overflow = _getStyleComputedProp.overflow, - overflowX = _getStyleComputedProp.overflowX, - overflowY = _getStyleComputedProp.overflowY; - - if (/(auto|scroll)/.test(overflow + overflowY + overflowX)) { - return element; - } - - return getScrollParent(getParentNode(element)); - } - - /** - * Returns the offset parent of the given element - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Element} offset parent - */ - function getOffsetParent(element) { - // NOTE: 1 DOM access here - var offsetParent = element && element.offsetParent; - var nodeName = offsetParent && offsetParent.nodeName; - - if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') { - if (element) { - return element.ownerDocument.documentElement; - } - - return window.document.documentElement; - } - - // .offsetParent will return the closest TD or TABLE in case - // no offsetParent is present, I hate this job... - if (['TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') { - return getOffsetParent(offsetParent); - } - - return offsetParent; - } - - function isOffsetContainer(element) { - var nodeName = element.nodeName; - - if (nodeName === 'BODY') { - return false; - } - return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element; - } - - /** - * Finds the root node (document, shadowDOM root) of the given element - * @method - * @memberof Popper.Utils - * @argument {Element} node - * @returns {Element} root node - */ - function getRoot(node) { - if (node.parentNode !== null) { - return getRoot(node.parentNode); - } - - return node; - } - - /** - * Finds the offset parent common to the two provided nodes - * @method - * @memberof Popper.Utils - * @argument {Element} element1 - * @argument {Element} element2 - * @returns {Element} common offset parent - */ - function findCommonOffsetParent(element1, element2) { - // This check is needed to avoid errors in case one of the elements isn't defined for any reason - if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) { - return window.document.documentElement; - } - - // Here we make sure to give as "start" the element that comes first in the DOM - var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING; - var start = order ? element1 : element2; - var end = order ? element2 : element1; - - // Get common ancestor container - var range = document.createRange(); - range.setStart(start, 0); - range.setEnd(end, 0); - var commonAncestorContainer = range.commonAncestorContainer; - - // Both nodes are inside #document - - if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) { - if (isOffsetContainer(commonAncestorContainer)) { - return commonAncestorContainer; - } - - return getOffsetParent(commonAncestorContainer); - } - - // one of the nodes is inside shadowDOM, find which one - var element1root = getRoot(element1); - if (element1root.host) { - return findCommonOffsetParent(element1root.host, element2); - } else { - return findCommonOffsetParent(element1, getRoot(element2).host); - } - } - - /** - * Gets the scroll value of the given element in the given side (top and left) - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @argument {String} side `top` or `left` - * @returns {number} amount of scrolled pixels - */ - function getScroll(element) { - var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top'; - - var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft'; - var nodeName = element.nodeName; - - if (nodeName === 'BODY' || nodeName === 'HTML') { - var html = element.ownerDocument.documentElement; - var scrollingElement = element.ownerDocument.scrollingElement || html; - return scrollingElement[upperSide]; - } - - return element[upperSide]; - } - - /* - * Sum or subtract the element scroll values (left and top) from a given rect object - * @method - * @memberof Popper.Utils - * @param {Object} rect - Rect object you want to change - * @param {HTMLElement} element - The element from the function reads the scroll values - * @param {Boolean} subtract - set to true if you want to subtract the scroll values - * @return {Object} rect - The modifier rect object - */ - function includeScroll(rect, element) { - var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - - var scrollTop = getScroll(element, 'top'); - var scrollLeft = getScroll(element, 'left'); - var modifier = subtract ? -1 : 1; - rect.top += scrollTop * modifier; - rect.bottom += scrollTop * modifier; - rect.left += scrollLeft * modifier; - rect.right += scrollLeft * modifier; - return rect; - } - - /* - * Helper to detect borders of a given element - * @method - * @memberof Popper.Utils - * @param {CSSStyleDeclaration} styles - * Result of `getStyleComputedProperty` on the given element - * @param {String} axis - `x` or `y` - * @return {number} borders - The borders size of the given axis - */ - - function getBordersSize(styles, axis) { - var sideA = axis === 'x' ? 'Left' : 'Top'; - var sideB = sideA === 'Left' ? 'Right' : 'Bottom'; - - return +styles['border' + sideA + 'Width'].split('px')[0] + +styles['border' + sideB + 'Width'].split('px')[0]; - } - - /** - * Tells if you are running Internet Explorer 10 - * @method - * @memberof Popper.Utils - * @returns {Boolean} isIE10 - */ - var isIE10 = undefined; - - var isIE10$1 = function () { - if (isIE10 === undefined) { - isIE10 = navigator.appVersion.indexOf('MSIE 10') !== -1; - } - return isIE10; - }; - - function getSize(axis, body, html, computedStyle) { - return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE10$1() ? html['offset' + axis] + computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')] + computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')] : 0); - } - - function getWindowSizes() { - var body = window.document.body; - var html = window.document.documentElement; - var computedStyle = isIE10$1() && window.getComputedStyle(html); - - return { - height: getSize('Height', body, html, computedStyle), - width: getSize('Width', body, html, computedStyle) - }; - } - - var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - }; - - var createClass = function () { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; - }(); - - - - - - var defineProperty = function (obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - - return obj; - }; - - var _extends = Object.assign || function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target; - }; - - /** - * Given element offsets, generate an output similar to getBoundingClientRect - * @method - * @memberof Popper.Utils - * @argument {Object} offsets - * @returns {Object} ClientRect like output - */ - function getClientRect(offsets) { - return _extends({}, offsets, { - right: offsets.left + offsets.width, - bottom: offsets.top + offsets.height - }); - } - - /** - * Get bounding client rect of given element - * @method - * @memberof Popper.Utils - * @param {HTMLElement} element - * @return {Object} client rect - */ - function getBoundingClientRect(element) { - var rect = {}; - - // IE10 10 FIX: Please, don't ask, the element isn't - // considered in DOM in some circumstances... - // This isn't reproducible in IE10 compatibility mode of IE11 - if (isIE10$1()) { - try { - rect = element.getBoundingClientRect(); - var scrollTop = getScroll(element, 'top'); - var scrollLeft = getScroll(element, 'left'); - rect.top += scrollTop; - rect.left += scrollLeft; - rect.bottom += scrollTop; - rect.right += scrollLeft; - } catch (err) {} - } else { - rect = element.getBoundingClientRect(); - } - - var result = { - left: rect.left, - top: rect.top, - width: rect.right - rect.left, - height: rect.bottom - rect.top - }; - - // subtract scrollbar size from sizes - var sizes = element.nodeName === 'HTML' ? getWindowSizes() : {}; - var width = sizes.width || element.clientWidth || result.right - result.left; - var height = sizes.height || element.clientHeight || result.bottom - result.top; - - var horizScrollbar = element.offsetWidth - width; - var vertScrollbar = element.offsetHeight - height; - - // if an hypothetical scrollbar is detected, we must be sure it's not a `border` - // we make this check conditional for performance reasons - if (horizScrollbar || vertScrollbar) { - var styles = getStyleComputedProperty(element); - horizScrollbar -= getBordersSize(styles, 'x'); - vertScrollbar -= getBordersSize(styles, 'y'); - - result.width -= horizScrollbar; - result.height -= vertScrollbar; - } - - return getClientRect(result); - } - - function getOffsetRectRelativeToArbitraryNode(children, parent) { - var isIE10 = isIE10$1(); - var isHTML = parent.nodeName === 'HTML'; - var childrenRect = getBoundingClientRect(children); - var parentRect = getBoundingClientRect(parent); - var scrollParent = getScrollParent(children); - - var styles = getStyleComputedProperty(parent); - var borderTopWidth = +styles.borderTopWidth.split('px')[0]; - var borderLeftWidth = +styles.borderLeftWidth.split('px')[0]; - - var offsets = getClientRect({ - top: childrenRect.top - parentRect.top - borderTopWidth, - left: childrenRect.left - parentRect.left - borderLeftWidth, - width: childrenRect.width, - height: childrenRect.height - }); - offsets.marginTop = 0; - offsets.marginLeft = 0; - - // Subtract margins of documentElement in case it's being used as parent - // we do this only on HTML because it's the only element that behaves - // differently when margins are applied to it. The margins are included in - // the box of the documentElement, in the other cases not. - if (!isIE10 && isHTML) { - var marginTop = +styles.marginTop.split('px')[0]; - var marginLeft = +styles.marginLeft.split('px')[0]; - - offsets.top -= borderTopWidth - marginTop; - offsets.bottom -= borderTopWidth - marginTop; - offsets.left -= borderLeftWidth - marginLeft; - offsets.right -= borderLeftWidth - marginLeft; - - // Attach marginTop and marginLeft because in some circumstances we may need them - offsets.marginTop = marginTop; - offsets.marginLeft = marginLeft; - } - - if (isIE10 ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') { - offsets = includeScroll(offsets, parent); - } - - return offsets; - } - - function getViewportOffsetRectRelativeToArtbitraryNode(element) { - var html = element.ownerDocument.documentElement; - var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html); - var width = Math.max(html.clientWidth, window.innerWidth || 0); - var height = Math.max(html.clientHeight, window.innerHeight || 0); - - var scrollTop = getScroll(html); - var scrollLeft = getScroll(html, 'left'); - - var offset = { - top: scrollTop - relativeOffset.top + relativeOffset.marginTop, - left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft, - width: width, - height: height - }; - - return getClientRect(offset); - } - - /** - * Check if the given element is fixed or is inside a fixed parent - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @argument {Element} customContainer - * @returns {Boolean} answer to "isFixed?" - */ - function isFixed(element) { - var nodeName = element.nodeName; - if (nodeName === 'BODY' || nodeName === 'HTML') { - return false; - } - if (getStyleComputedProperty(element, 'position') === 'fixed') { - return true; - } - return isFixed(getParentNode(element)); - } - - /** - * Computed the boundaries limits and return them - * @method - * @memberof Popper.Utils - * @param {HTMLElement} popper - * @param {HTMLElement} reference - * @param {number} padding - * @param {HTMLElement} boundariesElement - Element used to define the boundaries - * @returns {Object} Coordinates of the boundaries - */ - function getBoundaries(popper, reference, padding, boundariesElement) { - // NOTE: 1 DOM access here - var boundaries = { top: 0, left: 0 }; - var offsetParent = findCommonOffsetParent(popper, reference); - - // Handle viewport case - if (boundariesElement === 'viewport') { - boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent); - } else { - // Handle other cases based on DOM element used as boundaries - var boundariesNode = void 0; - if (boundariesElement === 'scrollParent') { - boundariesNode = getScrollParent(getParentNode(popper)); - if (boundariesNode.nodeName === 'BODY') { - boundariesNode = popper.ownerDocument.documentElement; - } - } else if (boundariesElement === 'window') { - boundariesNode = popper.ownerDocument.documentElement; - } else { - boundariesNode = boundariesElement; - } - - var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent); - - // In case of HTML, we need a different computation - if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) { - var _getWindowSizes = getWindowSizes(), - height = _getWindowSizes.height, - width = _getWindowSizes.width; - - boundaries.top += offsets.top - offsets.marginTop; - boundaries.bottom = height + offsets.top; - boundaries.left += offsets.left - offsets.marginLeft; - boundaries.right = width + offsets.left; - } else { - // for all the other DOM elements, this one is good - boundaries = offsets; - } - } - - // Add paddings - boundaries.left += padding; - boundaries.top += padding; - boundaries.right -= padding; - boundaries.bottom -= padding; - - return boundaries; - } - - function getArea(_ref) { - var width = _ref.width, - height = _ref.height; - - return width * height; - } - - /** - * Utility used to transform the `auto` placement to the placement with more - * available space. - * @method - * @memberof Popper.Utils - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) { - var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0; - - if (placement.indexOf('auto') === -1) { - return placement; - } - - var boundaries = getBoundaries(popper, reference, padding, boundariesElement); - - var rects = { - top: { - width: boundaries.width, - height: refRect.top - boundaries.top - }, - right: { - width: boundaries.right - refRect.right, - height: boundaries.height - }, - bottom: { - width: boundaries.width, - height: boundaries.bottom - refRect.bottom - }, - left: { - width: refRect.left - boundaries.left, - height: boundaries.height - } - }; - - var sortedAreas = Object.keys(rects).map(function (key) { - return _extends({ - key: key - }, rects[key], { - area: getArea(rects[key]) - }); - }).sort(function (a, b) { - return b.area - a.area; - }); - - var filteredAreas = sortedAreas.filter(function (_ref2) { - var width = _ref2.width, - height = _ref2.height; - return width >= popper.clientWidth && height >= popper.clientHeight; - }); - - var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key; - - var variation = placement.split('-')[1]; - - return computedPlacement + (variation ? '-' + variation : ''); - } - - /** - * Get offsets to the reference element - * @method - * @memberof Popper.Utils - * @param {Object} state - * @param {Element} popper - the popper element - * @param {Element} reference - the reference element (the popper will be relative to this) - * @returns {Object} An object containing the offsets which will be applied to the popper - */ - function getReferenceOffsets(state, popper, reference) { - var commonOffsetParent = findCommonOffsetParent(popper, reference); - return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent); - } - - /** - * Get the outer sizes of the given element (offset size + margins) - * @method - * @memberof Popper.Utils - * @argument {Element} element - * @returns {Object} object containing width and height properties - */ - function getOuterSizes(element) { - var styles = window.getComputedStyle(element); - var x = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom); - var y = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight); - var result = { - width: element.offsetWidth + y, - height: element.offsetHeight + x - }; - return result; - } - - /** - * Get the opposite placement of the given one - * @method - * @memberof Popper.Utils - * @argument {String} placement - * @returns {String} flipped placement - */ - function getOppositePlacement(placement) { - var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; - return placement.replace(/left|right|bottom|top/g, function (matched) { - return hash[matched]; - }); - } - - /** - * Get offsets to the popper - * @method - * @memberof Popper.Utils - * @param {Object} position - CSS position the Popper will get applied - * @param {HTMLElement} popper - the popper element - * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this) - * @param {String} placement - one of the valid placement options - * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper - */ - function getPopperOffsets(popper, referenceOffsets, placement) { - placement = placement.split('-')[0]; - - // Get popper node sizes - var popperRect = getOuterSizes(popper); - - // Add position, width and height to our offsets object - var popperOffsets = { - width: popperRect.width, - height: popperRect.height - }; - - // depending by the popper placement we have to compute its offsets slightly differently - var isHoriz = ['right', 'left'].indexOf(placement) !== -1; - var mainSide = isHoriz ? 'top' : 'left'; - var secondarySide = isHoriz ? 'left' : 'top'; - var measurement = isHoriz ? 'height' : 'width'; - var secondaryMeasurement = !isHoriz ? 'height' : 'width'; - - popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2; - if (placement === secondarySide) { - popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement]; - } else { - popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)]; - } - - return popperOffsets; - } - - /** - * Mimics the `find` method of Array - * @method - * @memberof Popper.Utils - * @argument {Array} arr - * @argument prop - * @argument value - * @returns index or -1 - */ - function find(arr, check) { - // use native find if supported - if (Array.prototype.find) { - return arr.find(check); - } - - // use `filter` to obtain the same behavior of `find` - return arr.filter(check)[0]; - } - - /** - * Return the index of the matching object - * @method - * @memberof Popper.Utils - * @argument {Array} arr - * @argument prop - * @argument value - * @returns index or -1 - */ - function findIndex(arr, prop, value) { - // use native findIndex if supported - if (Array.prototype.findIndex) { - return arr.findIndex(function (cur) { - return cur[prop] === value; - }); - } - - // use `find` + `indexOf` if `findIndex` isn't supported - var match = find(arr, function (obj) { - return obj[prop] === value; - }); - return arr.indexOf(match); - } - - /** - * Loop trough the list of modifiers and run them in order, - * each of them will then edit the data object. - * @method - * @memberof Popper.Utils - * @param {dataObject} data - * @param {Array} modifiers - * @param {String} ends - Optional modifier name used as stopper - * @returns {dataObject} - */ - function runModifiers(modifiers, data, ends) { - var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends)); - - modifiersToRun.forEach(function (modifier) { - if (modifier['function']) { - // eslint-disable-line dot-notation - console.warn('`modifier.function` is deprecated, use `modifier.fn`!'); - } - var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation - if (modifier.enabled && isFunction(fn)) { - // Add properties to offsets to make them a complete clientRect object - // we do this before each modifier to make sure the previous one doesn't - // mess with these values - data.offsets.popper = getClientRect(data.offsets.popper); - data.offsets.reference = getClientRect(data.offsets.reference); - - data = fn(data, modifier); - } - }); - - return data; - } - - /** - * Updates the position of the popper, computing the new offsets and applying - * the new style.
    - * Prefer `scheduleUpdate` over `update` because of performance reasons. - * @method - * @memberof Popper - */ - function update() { - // if popper is destroyed, don't perform any further update - if (this.state.isDestroyed) { - return; - } - - var data = { - instance: this, - styles: {}, - arrowStyles: {}, - attributes: {}, - flipped: false, - offsets: {} - }; - - // compute reference element offsets - data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference); - - // compute auto placement, store placement inside the data object, - // modifiers will be able to edit `placement` if needed - // and refer to originalPlacement to know the original value - data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding); - - // store the computed placement inside `originalPlacement` - data.originalPlacement = data.placement; - - // compute the popper offsets - data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement); - data.offsets.popper.position = 'absolute'; - - // run the modifiers - data = runModifiers(this.modifiers, data); - - // the first `update` will call `onCreate` callback - // the other ones will call `onUpdate` callback - if (!this.state.isCreated) { - this.state.isCreated = true; - this.options.onCreate(data); - } else { - this.options.onUpdate(data); - } - } - - /** - * Helper used to know if the given modifier is enabled. - * @method - * @memberof Popper.Utils - * @returns {Boolean} - */ - function isModifierEnabled(modifiers, modifierName) { - return modifiers.some(function (_ref) { - var name = _ref.name, - enabled = _ref.enabled; - return enabled && name === modifierName; - }); - } - - /** - * Get the prefixed supported property name - * @method - * @memberof Popper.Utils - * @argument {String} property (camelCase) - * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix) - */ - function getSupportedPropertyName(property) { - var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O']; - var upperProp = property.charAt(0).toUpperCase() + property.slice(1); - - for (var i = 0; i < prefixes.length - 1; i++) { - var prefix = prefixes[i]; - var toCheck = prefix ? '' + prefix + upperProp : property; - if (typeof window.document.body.style[toCheck] !== 'undefined') { - return toCheck; - } - } - return null; - } - - /** - * Destroy the popper - * @method - * @memberof Popper - */ - function destroy() { - this.state.isDestroyed = true; - - // touch DOM only if `applyStyle` modifier is enabled - if (isModifierEnabled(this.modifiers, 'applyStyle')) { - this.popper.removeAttribute('x-placement'); - this.popper.style.left = ''; - this.popper.style.position = ''; - this.popper.style.top = ''; - this.popper.style[getSupportedPropertyName('transform')] = ''; - } - - this.disableEventListeners(); - - // remove the popper if user explicity asked for the deletion on destroy - // do not use `remove` because IE11 doesn't support it - if (this.options.removeOnDestroy) { - this.popper.parentNode.removeChild(this.popper); - } - return this; - } - - /** - * Get the window associated with the element - * @argument {Element} element - * @returns {Window} - */ - function getWindow(element) { - var ownerDocument = element.ownerDocument; - return ownerDocument ? ownerDocument.defaultView : window; - } - - function attachToScrollParents(scrollParent, event, callback, scrollParents) { - var isBody = scrollParent.nodeName === 'BODY'; - var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent; - target.addEventListener(event, callback, { passive: true }); - - if (!isBody) { - attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents); - } - scrollParents.push(target); - } - - /** - * Setup needed event listeners used to update the popper position - * @method - * @memberof Popper.Utils - * @private - */ - function setupEventListeners(reference, options, state, updateBound) { - // Resize event listener on window - state.updateBound = updateBound; - getWindow(reference).addEventListener('resize', state.updateBound, { passive: true }); - - // Scroll event listener on scroll parents - var scrollElement = getScrollParent(reference); - attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents); - state.scrollElement = scrollElement; - state.eventsEnabled = true; - - return state; - } - - /** - * It will add resize/scroll events and start recalculating - * position of the popper element when they are triggered. - * @method - * @memberof Popper - */ - function enableEventListeners() { - if (!this.state.eventsEnabled) { - this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate); - } - } - - /** - * Remove event listeners used to update the popper position - * @method - * @memberof Popper.Utils - * @private - */ - function removeEventListeners(reference, state) { - // Remove resize event listener on window - getWindow(reference).removeEventListener('resize', state.updateBound); - - // Remove scroll event listener on scroll parents - state.scrollParents.forEach(function (target) { - target.removeEventListener('scroll', state.updateBound); - }); - - // Reset state - state.updateBound = null; - state.scrollParents = []; - state.scrollElement = null; - state.eventsEnabled = false; - return state; - } - - /** - * It will remove resize/scroll events and won't recalculate popper position - * when they are triggered. It also won't trigger onUpdate callback anymore, - * unless you call `update` method manually. - * @method - * @memberof Popper - */ - function disableEventListeners() { - if (this.state.eventsEnabled) { - window.cancelAnimationFrame(this.scheduleUpdate); - this.state = removeEventListeners(this.reference, this.state); - } - } - - /** - * Tells if a given input is a number - * @method - * @memberof Popper.Utils - * @param {*} input to check - * @return {Boolean} - */ - function isNumeric(n) { - return n !== '' && !isNaN(parseFloat(n)) && isFinite(n); - } - - /** - * Set the style to the given popper - * @method - * @memberof Popper.Utils - * @argument {Element} element - Element to apply the style to - * @argument {Object} styles - * Object with a list of properties and values which will be applied to the element - */ - function setStyles(element, styles) { - Object.keys(styles).forEach(function (prop) { - var unit = ''; - // add unit if the value is numeric and is one of the following - if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) { - unit = 'px'; - } - element.style[prop] = styles[prop] + unit; - }); - } - - /** - * Set the attributes to the given popper - * @method - * @memberof Popper.Utils - * @argument {Element} element - Element to apply the attributes to - * @argument {Object} styles - * Object with a list of properties and values which will be applied to the element - */ - function setAttributes(element, attributes) { - Object.keys(attributes).forEach(function (prop) { - var value = attributes[prop]; - if (value !== false) { - element.setAttribute(prop, attributes[prop]); - } else { - element.removeAttribute(prop); - } - }); - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} data.styles - List of style properties - values to apply to popper element - * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The same data object - */ - function applyStyle(data) { - // any property present in `data.styles` will be applied to the popper, - // in this way we can make the 3rd party modifiers add custom styles to it - // Be aware, modifiers could override the properties defined in the previous - // lines of this modifier! - setStyles(data.instance.popper, data.styles); - - // any property present in `data.attributes` will be applied to the popper, - // they will be set as HTML attributes of the element - setAttributes(data.instance.popper, data.attributes); - - // if arrowElement is defined and arrowStyles has some properties - if (data.arrowElement && Object.keys(data.arrowStyles).length) { - setStyles(data.arrowElement, data.arrowStyles); - } - - return data; - } - - /** - * Set the x-placement attribute before everything else because it could be used - * to add margins to the popper margins needs to be calculated to get the - * correct popper offsets. - * @method - * @memberof Popper.modifiers - * @param {HTMLElement} reference - The reference element used to position the popper - * @param {HTMLElement} popper - The HTML element used as popper. - * @param {Object} options - Popper.js options - */ - function applyStyleOnLoad(reference, popper, options, modifierOptions, state) { - // compute reference element offsets - var referenceOffsets = getReferenceOffsets(state, popper, reference); - - // compute auto placement, store placement inside the data object, - // modifiers will be able to edit `placement` if needed - // and refer to originalPlacement to know the original value - var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding); - - popper.setAttribute('x-placement', placement); - - // Apply `position` to popper before anything else because - // without the position applied we can't guarantee correct computations - setStyles(popper, { position: 'absolute' }); - - return options; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function computeStyle(data, options) { - var x = options.x, - y = options.y; - var popper = data.offsets.popper; - - // Remove this legacy support in Popper.js v2 - - var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) { - return modifier.name === 'applyStyle'; - }).gpuAcceleration; - if (legacyGpuAccelerationOption !== undefined) { - console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!'); - } - var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration; - - var offsetParent = getOffsetParent(data.instance.popper); - var offsetParentRect = getBoundingClientRect(offsetParent); - - // Styles - var styles = { - position: popper.position - }; - - // floor sides to avoid blurry text - var offsets = { - left: Math.floor(popper.left), - top: Math.floor(popper.top), - bottom: Math.floor(popper.bottom), - right: Math.floor(popper.right) - }; - - var sideA = x === 'bottom' ? 'top' : 'bottom'; - var sideB = y === 'right' ? 'left' : 'right'; - - // if gpuAcceleration is set to `true` and transform is supported, - // we use `translate3d` to apply the position to the popper we - // automatically use the supported prefixed version if needed - var prefixedProperty = getSupportedPropertyName('transform'); - - // now, let's make a step back and look at this code closely (wtf?) - // If the content of the popper grows once it's been positioned, it - // may happen that the popper gets misplaced because of the new content - // overflowing its reference element - // To avoid this problem, we provide two options (x and y), which allow - // the consumer to define the offset origin. - // If we position a popper on top of a reference element, we can set - // `x` to `top` to make the popper grow towards its top instead of - // its bottom. - var left = void 0, - top = void 0; - if (sideA === 'bottom') { - top = -offsetParentRect.height + offsets.bottom; - } else { - top = offsets.top; - } - if (sideB === 'right') { - left = -offsetParentRect.width + offsets.right; - } else { - left = offsets.left; - } - if (gpuAcceleration && prefixedProperty) { - styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)'; - styles[sideA] = 0; - styles[sideB] = 0; - styles.willChange = 'transform'; - } else { - // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties - var invertTop = sideA === 'bottom' ? -1 : 1; - var invertLeft = sideB === 'right' ? -1 : 1; - styles[sideA] = top * invertTop; - styles[sideB] = left * invertLeft; - styles.willChange = sideA + ', ' + sideB; - } - - // Attributes - var attributes = { - 'x-placement': data.placement - }; - - // Update `data` attributes, styles and arrowStyles - data.attributes = _extends({}, attributes, data.attributes); - data.styles = _extends({}, styles, data.styles); - data.arrowStyles = _extends({}, data.offsets.arrow, data.arrowStyles); - - return data; - } - - /** - * Helper used to know if the given modifier depends from another one.
    - * It checks if the needed modifier is listed and enabled. - * @method - * @memberof Popper.Utils - * @param {Array} modifiers - list of modifiers - * @param {String} requestingName - name of requesting modifier - * @param {String} requestedName - name of requested modifier - * @returns {Boolean} - */ - function isModifierRequired(modifiers, requestingName, requestedName) { - var requesting = find(modifiers, function (_ref) { - var name = _ref.name; - return name === requestingName; - }); - - var isRequired = !!requesting && modifiers.some(function (modifier) { - return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order; - }); - - if (!isRequired) { - var _requesting = '`' + requestingName + '`'; - var requested = '`' + requestedName + '`'; - console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!'); - } - return isRequired; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function arrow(data, options) { - // arrow depends on keepTogether in order to work - if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) { - return data; - } - - var arrowElement = options.element; - - // if arrowElement is a string, suppose it's a CSS selector - if (typeof arrowElement === 'string') { - arrowElement = data.instance.popper.querySelector(arrowElement); - - // if arrowElement is not found, don't run the modifier - if (!arrowElement) { - return data; - } - } else { - // if the arrowElement isn't a query selector we must check that the - // provided DOM node is child of its popper node - if (!data.instance.popper.contains(arrowElement)) { - console.warn('WARNING: `arrow.element` must be child of its popper element!'); - return data; - } - } - - var placement = data.placement.split('-')[0]; - var _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - - var isVertical = ['left', 'right'].indexOf(placement) !== -1; - - var len = isVertical ? 'height' : 'width'; - var sideCapitalized = isVertical ? 'Top' : 'Left'; - var side = sideCapitalized.toLowerCase(); - var altSide = isVertical ? 'left' : 'top'; - var opSide = isVertical ? 'bottom' : 'right'; - var arrowElementSize = getOuterSizes(arrowElement)[len]; - - // - // extends keepTogether behavior making sure the popper and its - // reference have enough pixels in conjuction - // - - // top/left side - if (reference[opSide] - arrowElementSize < popper[side]) { - data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize); - } - // bottom/right side - if (reference[side] + arrowElementSize > popper[opSide]) { - data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide]; - } - - // compute center of the popper - var center = reference[side] + reference[len] / 2 - arrowElementSize / 2; - - // Compute the sideValue using the updated popper offsets - // take popper margin in account because we don't have this info available - var popperMarginSide = getStyleComputedProperty(data.instance.popper, 'margin' + sideCapitalized).replace('px', ''); - var sideValue = center - getClientRect(data.offsets.popper)[side] - popperMarginSide; - - // prevent arrowElement from being placed not contiguously to its popper - sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0); - - data.arrowElement = arrowElement; - data.offsets.arrow = {}; - data.offsets.arrow[side] = Math.round(sideValue); - data.offsets.arrow[altSide] = ''; // make sure to unset any eventual altSide value from the DOM node - - return data; - } - - /** - * Get the opposite placement variation of the given one - * @method - * @memberof Popper.Utils - * @argument {String} placement variation - * @returns {String} flipped placement variation - */ - function getOppositeVariation(variation) { - if (variation === 'end') { - return 'start'; - } else if (variation === 'start') { - return 'end'; - } - return variation; - } - - /** - * List of accepted placements to use as values of the `placement` option.
    - * Valid placements are: - * - `auto` - * - `top` - * - `right` - * - `bottom` - * - `left` - * - * Each placement can have a variation from this list: - * - `-start` - * - `-end` - * - * Variations are interpreted easily if you think of them as the left to right - * written languages. Horizontally (`top` and `bottom`), `start` is left and `end` - * is right.
    - * Vertically (`left` and `right`), `start` is top and `end` is bottom. - * - * Some valid examples are: - * - `top-end` (on top of reference, right aligned) - * - `right-start` (on right of reference, top aligned) - * - `bottom` (on bottom, centered) - * - `auto-right` (on the side with more space available, alignment depends by placement) - * - * @static - * @type {Array} - * @enum {String} - * @readonly - * @method placements - * @memberof Popper - */ - var placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start']; - -// Get rid of `auto` `auto-start` and `auto-end` - var validPlacements = placements.slice(3); - - /** - * Given an initial placement, returns all the subsequent placements - * clockwise (or counter-clockwise). - * - * @method - * @memberof Popper.Utils - * @argument {String} placement - A valid placement (it accepts variations) - * @argument {Boolean} counter - Set to true to walk the placements counterclockwise - * @returns {Array} placements including their variations - */ - function clockwise(placement) { - var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - var index = validPlacements.indexOf(placement); - var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index)); - return counter ? arr.reverse() : arr; - } - - var BEHAVIORS = { - FLIP: 'flip', - CLOCKWISE: 'clockwise', - COUNTERCLOCKWISE: 'counterclockwise' - }; - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function flip(data, options) { - // if `inner` modifier is enabled, we can't use the `flip` modifier - if (isModifierEnabled(data.instance.modifiers, 'inner')) { - return data; - } - - if (data.flipped && data.placement === data.originalPlacement) { - // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides - return data; - } - - var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement); - - var placement = data.placement.split('-')[0]; - var placementOpposite = getOppositePlacement(placement); - var variation = data.placement.split('-')[1] || ''; - - var flipOrder = []; - - switch (options.behavior) { - case BEHAVIORS.FLIP: - flipOrder = [placement, placementOpposite]; - break; - case BEHAVIORS.CLOCKWISE: - flipOrder = clockwise(placement); - break; - case BEHAVIORS.COUNTERCLOCKWISE: - flipOrder = clockwise(placement, true); - break; - default: - flipOrder = options.behavior; - } - - flipOrder.forEach(function (step, index) { - if (placement !== step || flipOrder.length === index + 1) { - return data; - } - - placement = data.placement.split('-')[0]; - placementOpposite = getOppositePlacement(placement); - - var popperOffsets = data.offsets.popper; - var refOffsets = data.offsets.reference; - - // using floor because the reference offsets may contain decimals we are not going to consider here - var floor = Math.floor; - var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom); - - var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left); - var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right); - var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top); - var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom); - - var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom; - - // flip the variation if required - var isVertical = ['top', 'bottom'].indexOf(placement) !== -1; - var flippedVariation = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom); - - if (overlapsRef || overflowsBoundaries || flippedVariation) { - // this boolean to detect any flip loop - data.flipped = true; - - if (overlapsRef || overflowsBoundaries) { - placement = flipOrder[index + 1]; - } - - if (flippedVariation) { - variation = getOppositeVariation(variation); - } - - data.placement = placement + (variation ? '-' + variation : ''); - - // this object contains `position`, we want to preserve it along with - // any additional property we may add in the future - data.offsets.popper = _extends({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement)); - - data = runModifiers(data.instance.modifiers, data, 'flip'); - } - }); - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function keepTogether(data) { - var _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - - var placement = data.placement.split('-')[0]; - var floor = Math.floor; - var isVertical = ['top', 'bottom'].indexOf(placement) !== -1; - var side = isVertical ? 'right' : 'bottom'; - var opSide = isVertical ? 'left' : 'top'; - var measurement = isVertical ? 'width' : 'height'; - - if (popper[side] < floor(reference[opSide])) { - data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement]; - } - if (popper[opSide] > floor(reference[side])) { - data.offsets.popper[opSide] = floor(reference[side]); - } - - return data; - } - - /** - * Converts a string containing value + unit into a px value number - * @function - * @memberof {modifiers~offset} - * @private - * @argument {String} str - Value + unit string - * @argument {String} measurement - `height` or `width` - * @argument {Object} popperOffsets - * @argument {Object} referenceOffsets - * @returns {Number|String} - * Value in pixels, or original string if no values were extracted - */ - function toValue(str, measurement, popperOffsets, referenceOffsets) { - // separate value from unit - var split = str.match(/((?:\-|\+)?\d*\.?\d*)(.*)/); - var value = +split[1]; - var unit = split[2]; - - // If it's not a number it's an operator, I guess - if (!value) { - return str; - } - - if (unit.indexOf('%') === 0) { - var element = void 0; - switch (unit) { - case '%p': - element = popperOffsets; - break; - case '%': - case '%r': - default: - element = referenceOffsets; - } - - var rect = getClientRect(element); - return rect[measurement] / 100 * value; - } else if (unit === 'vh' || unit === 'vw') { - // if is a vh or vw, we calculate the size based on the viewport - var size = void 0; - if (unit === 'vh') { - size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); - } else { - size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); - } - return size / 100 * value; - } else { - // if is an explicit pixel unit, we get rid of the unit and keep the value - // if is an implicit unit, it's px, and we return just the value - return value; - } - } - - /** - * Parse an `offset` string to extrapolate `x` and `y` numeric offsets. - * @function - * @memberof {modifiers~offset} - * @private - * @argument {String} offset - * @argument {Object} popperOffsets - * @argument {Object} referenceOffsets - * @argument {String} basePlacement - * @returns {Array} a two cells array with x and y offsets in numbers - */ - function parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) { - var offsets = [0, 0]; - - // Use height if placement is left or right and index is 0 otherwise use width - // in this way the first offset will use an axis and the second one - // will use the other one - var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1; - - // Split the offset string to obtain a list of values and operands - // The regex addresses values with the plus or minus sign in front (+10, -20, etc) - var fragments = offset.split(/(\+|\-)/).map(function (frag) { - return frag.trim(); - }); - - // Detect if the offset string contains a pair of values or a single one - // they could be separated by comma or space - var divider = fragments.indexOf(find(fragments, function (frag) { - return frag.search(/,|\s/) !== -1; - })); - - if (fragments[divider] && fragments[divider].indexOf(',') === -1) { - console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.'); - } - - // If divider is found, we divide the list of values and operands to divide - // them by ofset X and Y. - var splitRegex = /\s*,\s*|\s+/; - var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments]; - - // Convert the values with units to absolute pixels to allow our computations - ops = ops.map(function (op, index) { - // Most of the units rely on the orientation of the popper - var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width'; - var mergeWithPrevious = false; - return op - // This aggregates any `+` or `-` sign that aren't considered operators - // e.g.: 10 + +5 => [10, +, +5] - .reduce(function (a, b) { - if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) { - a[a.length - 1] = b; - mergeWithPrevious = true; - return a; - } else if (mergeWithPrevious) { - a[a.length - 1] += b; - mergeWithPrevious = false; - return a; - } else { - return a.concat(b); - } - }, []) - // Here we convert the string values into number values (in px) - .map(function (str) { - return toValue(str, measurement, popperOffsets, referenceOffsets); - }); - }); - - // Loop trough the offsets arrays and execute the operations - ops.forEach(function (op, index) { - op.forEach(function (frag, index2) { - if (isNumeric(frag)) { - offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1); - } - }); - }); - return offsets; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @argument {Number|String} options.offset=0 - * The offset value as described in the modifier description - * @returns {Object} The data object, properly modified - */ - function offset(data, _ref) { - var offset = _ref.offset; - var placement = data.placement, - _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - - var basePlacement = placement.split('-')[0]; - - var offsets = void 0; - if (isNumeric(+offset)) { - offsets = [+offset, 0]; - } else { - offsets = parseOffset(offset, popper, reference, basePlacement); - } - - if (basePlacement === 'left') { - popper.top += offsets[0]; - popper.left -= offsets[1]; - } else if (basePlacement === 'right') { - popper.top += offsets[0]; - popper.left += offsets[1]; - } else if (basePlacement === 'top') { - popper.left += offsets[0]; - popper.top -= offsets[1]; - } else if (basePlacement === 'bottom') { - popper.left += offsets[0]; - popper.top += offsets[1]; - } - - data.popper = popper; - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function preventOverflow(data, options) { - var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper); - - // If offsetParent is the reference element, we really want to - // go one step up and use the next offsetParent as reference to - // avoid to make this modifier completely useless and look like broken - if (data.instance.reference === boundariesElement) { - boundariesElement = getOffsetParent(boundariesElement); - } - - var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement); - options.boundaries = boundaries; - - var order = options.priority; - var popper = data.offsets.popper; - - var check = { - primary: function primary(placement) { - var value = popper[placement]; - if (popper[placement] < boundaries[placement] && !options.escapeWithReference) { - value = Math.max(popper[placement], boundaries[placement]); - } - return defineProperty({}, placement, value); - }, - secondary: function secondary(placement) { - var mainSide = placement === 'right' ? 'left' : 'top'; - var value = popper[mainSide]; - if (popper[placement] > boundaries[placement] && !options.escapeWithReference) { - value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height)); - } - return defineProperty({}, mainSide, value); - } - }; - - order.forEach(function (placement) { - var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary'; - popper = _extends({}, popper, check[side](placement)); - }); - - data.offsets.popper = popper; - - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function shift(data) { - var placement = data.placement; - var basePlacement = placement.split('-')[0]; - var shiftvariation = placement.split('-')[1]; - - // if shift shiftvariation is specified, run the modifier - if (shiftvariation) { - var _data$offsets = data.offsets, - reference = _data$offsets.reference, - popper = _data$offsets.popper; - - var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1; - var side = isVertical ? 'left' : 'top'; - var measurement = isVertical ? 'width' : 'height'; - - var shiftOffsets = { - start: defineProperty({}, side, reference[side]), - end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement]) - }; - - data.offsets.popper = _extends({}, popper, shiftOffsets[shiftvariation]); - } - - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by update method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function hide(data) { - if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) { - return data; - } - - var refRect = data.offsets.reference; - var bound = find(data.instance.modifiers, function (modifier) { - return modifier.name === 'preventOverflow'; - }).boundaries; - - if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) { - // Avoid unnecessary DOM access if visibility hasn't changed - if (data.hide === true) { - return data; - } - - data.hide = true; - data.attributes['x-out-of-boundaries'] = ''; - } else { - // Avoid unnecessary DOM access if visibility hasn't changed - if (data.hide === false) { - return data; - } - - data.hide = false; - data.attributes['x-out-of-boundaries'] = false; - } - - return data; - } - - /** - * @function - * @memberof Modifiers - * @argument {Object} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {Object} The data object, properly modified - */ - function inner(data) { - var placement = data.placement; - var basePlacement = placement.split('-')[0]; - var _data$offsets = data.offsets, - popper = _data$offsets.popper, - reference = _data$offsets.reference; - - var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1; - - var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1; - - popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0); - - data.placement = getOppositePlacement(placement); - data.offsets.popper = getClientRect(popper); - - return data; - } - - /** - * Modifier function, each modifier can have a function of this type assigned - * to its `fn` property.
    - * These functions will be called on each update, this means that you must - * make sure they are performant enough to avoid performance bottlenecks. - * - * @function ModifierFn - * @argument {dataObject} data - The data object generated by `update` method - * @argument {Object} options - Modifiers configuration and options - * @returns {dataObject} The data object, properly modified - */ - - /** - * Modifiers are plugins used to alter the behavior of your poppers.
    - * Popper.js uses a set of 9 modifiers to provide all the basic functionalities - * needed by the library. - * - * Usually you don't want to override the `order`, `fn` and `onLoad` props. - * All the other properties are configurations that could be tweaked. - * @namespace modifiers - */ - var modifiers = { - /** - * Modifier used to shift the popper on the start or end of its reference - * element.
    - * It will read the variation of the `placement` property.
    - * It can be one either `-end` or `-start`. - * @memberof modifiers - * @inner - */ - shift: { - /** @prop {number} order=100 - Index used to define the order of execution */ - order: 100, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: shift - }, - - /** - * The `offset` modifier can shift your popper on both its axis. - * - * It accepts the following units: - * - `px` or unitless, interpreted as pixels - * - `%` or `%r`, percentage relative to the length of the reference element - * - `%p`, percentage relative to the length of the popper element - * - `vw`, CSS viewport width unit - * - `vh`, CSS viewport height unit - * - * For length is intended the main axis relative to the placement of the popper.
    - * This means that if the placement is `top` or `bottom`, the length will be the - * `width`. In case of `left` or `right`, it will be the height. - * - * You can provide a single value (as `Number` or `String`), or a pair of values - * as `String` divided by a comma or one (or more) white spaces.
    - * The latter is a deprecated method because it leads to confusion and will be - * removed in v2.
    - * Additionally, it accepts additions and subtractions between different units. - * Note that multiplications and divisions aren't supported. - * - * Valid examples are: - * ``` - * 10 - * '10%' - * '10, 10' - * '10%, 10' - * '10 + 10%' - * '10 - 5vh + 3%' - * '-10px + 5vh, 5px - 6%' - * ``` - * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap - * > with their reference element, unfortunately, you will have to disable the `flip` modifier. - * > More on this [reading this issue](https://github.com/FezVrasta/popper.js/issues/373) - * - * @memberof modifiers - * @inner - */ - offset: { - /** @prop {number} order=200 - Index used to define the order of execution */ - order: 200, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: offset, - /** @prop {Number|String} offset=0 - * The offset value as described in the modifier description - */ - offset: 0 - }, - - /** - * Modifier used to prevent the popper from being positioned outside the boundary. - * - * An scenario exists where the reference itself is not within the boundaries.
    - * We can say it has "escaped the boundaries" — or just "escaped".
    - * In this case we need to decide whether the popper should either: - * - * - detach from the reference and remain "trapped" in the boundaries, or - * - if it should ignore the boundary and "escape with its reference" - * - * When `escapeWithReference` is set to`true` and reference is completely - * outside its boundaries, the popper will overflow (or completely leave) - * the boundaries in order to remain attached to the edge of the reference. - * - * @memberof modifiers - * @inner - */ - preventOverflow: { - /** @prop {number} order=300 - Index used to define the order of execution */ - order: 300, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: preventOverflow, - /** - * @prop {Array} [priority=['left','right','top','bottom']] - * Popper will try to prevent overflow following these priorities by default, - * then, it could overflow on the left and on top of the `boundariesElement` - */ - priority: ['left', 'right', 'top', 'bottom'], - /** - * @prop {number} padding=5 - * Amount of pixel used to define a minimum distance between the boundaries - * and the popper this makes sure the popper has always a little padding - * between the edges of its container - */ - padding: 5, - /** - * @prop {String|HTMLElement} boundariesElement='scrollParent' - * Boundaries used by the modifier, can be `scrollParent`, `window`, - * `viewport` or any DOM element. - */ - boundariesElement: 'scrollParent' - }, - - /** - * Modifier used to make sure the reference and its popper stay near eachothers - * without leaving any gap between the two. Expecially useful when the arrow is - * enabled and you want to assure it to point to its reference element. - * It cares only about the first axis, you can still have poppers with margin - * between the popper and its reference element. - * @memberof modifiers - * @inner - */ - keepTogether: { - /** @prop {number} order=400 - Index used to define the order of execution */ - order: 400, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: keepTogether - }, - - /** - * This modifier is used to move the `arrowElement` of the popper to make - * sure it is positioned between the reference element and its popper element. - * It will read the outer size of the `arrowElement` node to detect how many - * pixels of conjuction are needed. - * - * It has no effect if no `arrowElement` is provided. - * @memberof modifiers - * @inner - */ - arrow: { - /** @prop {number} order=500 - Index used to define the order of execution */ - order: 500, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: arrow, - /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */ - element: '[x-arrow]' - }, - - /** - * Modifier used to flip the popper's placement when it starts to overlap its - * reference element. - * - * Requires the `preventOverflow` modifier before it in order to work. - * - * **NOTE:** this modifier will interrupt the current update cycle and will - * restart it if it detects the need to flip the placement. - * @memberof modifiers - * @inner - */ - flip: { - /** @prop {number} order=600 - Index used to define the order of execution */ - order: 600, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: flip, - /** - * @prop {String|Array} behavior='flip' - * The behavior used to change the popper's placement. It can be one of - * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid - * placements (with optional variations). - */ - behavior: 'flip', - /** - * @prop {number} padding=5 - * The popper will flip if it hits the edges of the `boundariesElement` - */ - padding: 5, - /** - * @prop {String|HTMLElement} boundariesElement='viewport' - * The element which will define the boundaries of the popper position, - * the popper will never be placed outside of the defined boundaries - * (except if keepTogether is enabled) - */ - boundariesElement: 'viewport' - }, - - /** - * Modifier used to make the popper flow toward the inner of the reference element. - * By default, when this modifier is disabled, the popper will be placed outside - * the reference element. - * @memberof modifiers - * @inner - */ - inner: { - /** @prop {number} order=700 - Index used to define the order of execution */ - order: 700, - /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */ - enabled: false, - /** @prop {ModifierFn} */ - fn: inner - }, - - /** - * Modifier used to hide the popper when its reference element is outside of the - * popper boundaries. It will set a `x-out-of-boundaries` attribute which can - * be used to hide with a CSS selector the popper when its reference is - * out of boundaries. - * - * Requires the `preventOverflow` modifier before it in order to work. - * @memberof modifiers - * @inner - */ - hide: { - /** @prop {number} order=800 - Index used to define the order of execution */ - order: 800, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: hide - }, - - /** - * Computes the style that will be applied to the popper element to gets - * properly positioned. - * - * Note that this modifier will not touch the DOM, it just prepares the styles - * so that `applyStyle` modifier can apply it. This separation is useful - * in case you need to replace `applyStyle` with a custom implementation. - * - * This modifier has `850` as `order` value to maintain backward compatibility - * with previous versions of Popper.js. Expect the modifiers ordering method - * to change in future major versions of the library. - * - * @memberof modifiers - * @inner - */ - computeStyle: { - /** @prop {number} order=850 - Index used to define the order of execution */ - order: 850, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: computeStyle, - /** - * @prop {Boolean} gpuAcceleration=true - * If true, it uses the CSS 3d transformation to position the popper. - * Otherwise, it will use the `top` and `left` properties. - */ - gpuAcceleration: true, - /** - * @prop {string} [x='bottom'] - * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin. - * Change this if your popper should grow in a direction different from `bottom` - */ - x: 'bottom', - /** - * @prop {string} [x='left'] - * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin. - * Change this if your popper should grow in a direction different from `right` - */ - y: 'right' - }, - - /** - * Applies the computed styles to the popper element. - * - * All the DOM manipulations are limited to this modifier. This is useful in case - * you want to integrate Popper.js inside a framework or view library and you - * want to delegate all the DOM manipulations to it. - * - * Note that if you disable this modifier, you must make sure the popper element - * has its position set to `absolute` before Popper.js can do its work! - * - * Just disable this modifier and define you own to achieve the desired effect. - * - * @memberof modifiers - * @inner - */ - applyStyle: { - /** @prop {number} order=900 - Index used to define the order of execution */ - order: 900, - /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ - enabled: true, - /** @prop {ModifierFn} */ - fn: applyStyle, - /** @prop {Function} */ - onLoad: applyStyleOnLoad, - /** - * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier - * @prop {Boolean} gpuAcceleration=true - * If true, it uses the CSS 3d transformation to position the popper. - * Otherwise, it will use the `top` and `left` properties. - */ - gpuAcceleration: undefined - } - }; - - /** - * The `dataObject` is an object containing all the informations used by Popper.js - * this object get passed to modifiers and to the `onCreate` and `onUpdate` callbacks. - * @name dataObject - * @property {Object} data.instance The Popper.js instance - * @property {String} data.placement Placement applied to popper - * @property {String} data.originalPlacement Placement originally defined on init - * @property {Boolean} data.flipped True if popper has been flipped by flip modifier - * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper. - * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier - * @property {Object} data.styles Any CSS property defined here will be applied to the popper, it expects the JavaScript nomenclature (eg. `marginBottom`) - * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow, it expects the JavaScript nomenclature (eg. `marginBottom`) - * @property {Object} data.boundaries Offsets of the popper boundaries - * @property {Object} data.offsets The measurements of popper, reference and arrow elements. - * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values - * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values - * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0 - */ - - /** - * Default options provided to Popper.js constructor.
    - * These can be overriden using the `options` argument of Popper.js.
    - * To override an option, simply pass as 3rd argument an object with the same - * structure of this object, example: - * ``` - * new Popper(ref, pop, { - * modifiers: { - * preventOverflow: { enabled: false } - * } - * }) - * ``` - * @type {Object} - * @static - * @memberof Popper - */ - var Defaults = { - /** - * Popper's placement - * @prop {Popper.placements} placement='bottom' - */ - placement: 'bottom', - - /** - * Whether events (resize, scroll) are initially enabled - * @prop {Boolean} eventsEnabled=true - */ - eventsEnabled: true, - - /** - * Set to true if you want to automatically remove the popper when - * you call the `destroy` method. - * @prop {Boolean} removeOnDestroy=false - */ - removeOnDestroy: false, - - /** - * Callback called when the popper is created.
    - * By default, is set to no-op.
    - * Access Popper.js instance with `data.instance`. - * @prop {onCreate} - */ - onCreate: function onCreate() {}, - - /** - * Callback called when the popper is updated, this callback is not called - * on the initialization/creation of the popper, but only on subsequent - * updates.
    - * By default, is set to no-op.
    - * Access Popper.js instance with `data.instance`. - * @prop {onUpdate} - */ - onUpdate: function onUpdate() {}, - - /** - * List of modifiers used to modify the offsets before they are applied to the popper. - * They provide most of the functionalities of Popper.js - * @prop {modifiers} - */ - modifiers: modifiers - }; - - /** - * @callback onCreate - * @param {dataObject} data - */ - - /** - * @callback onUpdate - * @param {dataObject} data - */ - -// Utils -// Methods - var Popper = function () { - /** - * Create a new Popper.js instance - * @class Popper - * @param {HTMLElement|referenceObject} reference - The reference element used to position the popper - * @param {HTMLElement} popper - The HTML element used as popper. - * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults) - * @return {Object} instance - The generated Popper.js instance - */ - function Popper(reference, popper) { - var _this = this; - - var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - classCallCheck(this, Popper); - - this.scheduleUpdate = function () { - return requestAnimationFrame(_this.update); - }; - - // make update() debounced, so that it only runs at most once-per-tick - this.update = debounce(this.update.bind(this)); - - // with {} we create a new object with the options inside it - this.options = _extends({}, Popper.Defaults, options); - - // init state - this.state = { - isDestroyed: false, - isCreated: false, - scrollParents: [] - }; - - // get reference and popper elements (allow jQuery wrappers) - this.reference = reference && reference.jquery ? reference[0] : reference; - this.popper = popper && popper.jquery ? popper[0] : popper; - - // Deep merge modifiers options - this.options.modifiers = {}; - Object.keys(_extends({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) { - _this.options.modifiers[name] = _extends({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {}); - }); - - // Refactoring modifiers' list (Object => Array) - this.modifiers = Object.keys(this.options.modifiers).map(function (name) { - return _extends({ - name: name - }, _this.options.modifiers[name]); - }) - // sort the modifiers by order - .sort(function (a, b) { - return a.order - b.order; - }); - - // modifiers have the ability to execute arbitrary code when Popper.js get inited - // such code is executed in the same order of its modifier - // they could add new properties to their options configuration - // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`! - this.modifiers.forEach(function (modifierOptions) { - if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) { - modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state); - } - }); - - // fire the first update to position the popper in the right place - this.update(); - - var eventsEnabled = this.options.eventsEnabled; - if (eventsEnabled) { - // setup event listeners, they will take care of update the position in specific situations - this.enableEventListeners(); - } - - this.state.eventsEnabled = eventsEnabled; - } - - // We can't use class properties because they don't get listed in the - // class prototype and break stuff like Sinon stubs - - - createClass(Popper, [{ - key: 'update', - value: function update$$1() { - return update.call(this); - } - }, { - key: 'destroy', - value: function destroy$$1() { - return destroy.call(this); - } - }, { - key: 'enableEventListeners', - value: function enableEventListeners$$1() { - return enableEventListeners.call(this); - } - }, { - key: 'disableEventListeners', - value: function disableEventListeners$$1() { - return disableEventListeners.call(this); - } - - /** - * Schedule an update, it will run on the next UI update available - * @method scheduleUpdate - * @memberof Popper - */ - - - /** - * Collection of utilities useful when writing custom modifiers. - * Starting from version 1.7, this method is available only if you - * include `popper-utils.js` before `popper.js`. - * - * **DEPRECATION**: This way to access PopperUtils is deprecated - * and will be removed in v2! Use the PopperUtils module directly instead. - * Due to the high instability of the methods contained in Utils, we can't - * guarantee them to follow semver. Use them at your own risk! - * @static - * @private - * @type {Object} - * @deprecated since version 1.8 - * @member Utils - * @memberof Popper - */ - - }]); - return Popper; - }(); - - /** - * The `referenceObject` is an object that provides an interface compatible with Popper.js - * and lets you use it as replacement of a real DOM node.
    - * You can use this method to position a popper relatively to a set of coordinates - * in case you don't have a DOM node to use as reference. - * - * ``` - * new Popper(referenceObject, popperNode); - * ``` - * - * NB: This feature isn't supported in Internet Explorer 10 - * @name referenceObject - * @property {Function} data.getBoundingClientRect - * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method. - * @property {number} data.clientWidth - * An ES6 getter that will return the width of the virtual reference element. - * @property {number} data.clientHeight - * An ES6 getter that will return the height of the virtual reference element. - */ - - - Popper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils; - Popper.placements = placements; - Popper.Defaults = Defaults; - - return Popper; - -}))); -//# sourceMappingURL=popper.js.map \ No newline at end of file diff --git a/vendor/remarkable/remarkable.min.js b/vendor/remarkable/remarkable.min.js new file mode 100644 index 00000000..e18d66c0 --- /dev/null +++ b/vendor/remarkable/remarkable.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).remarkable={})}(this,function(e){"use strict";var t={Aacute:"Á",aacute:"á",Abreve:"Ă",abreve:"ă",ac:"∾",acd:"∿",acE:"∾̳",Acirc:"Â",acirc:"â",acute:"´",Acy:"А",acy:"а",AElig:"Æ",aelig:"æ",af:"⁡",Afr:"𝔄",afr:"𝔞",Agrave:"À",agrave:"à",alefsym:"ℵ",aleph:"ℵ",Alpha:"Α",alpha:"α",Amacr:"Ā",amacr:"ā",amalg:"⨿",AMP:"&",amp:"&",And:"⩓",and:"∧",andand:"⩕",andd:"⩜",andslope:"⩘",andv:"⩚",ang:"∠",ange:"⦤",angle:"∠",angmsd:"∡",angmsdaa:"⦨",angmsdab:"⦩",angmsdac:"⦪",angmsdad:"⦫",angmsdae:"⦬",angmsdaf:"⦭",angmsdag:"⦮",angmsdah:"⦯",angrt:"∟",angrtvb:"⊾",angrtvbd:"⦝",angsph:"∢",angst:"Å",angzarr:"⍼",Aogon:"Ą",aogon:"ą",Aopf:"𝔸",aopf:"𝕒",ap:"≈",apacir:"⩯",apE:"⩰",ape:"≊",apid:"≋",apos:"'",ApplyFunction:"⁡",approx:"≈",approxeq:"≊",Aring:"Å",aring:"å",Ascr:"𝒜",ascr:"𝒶",Assign:"≔",ast:"*",asymp:"≈",asympeq:"≍",Atilde:"Ã",atilde:"ã",Auml:"Ä",auml:"ä",awconint:"∳",awint:"⨑",backcong:"≌",backepsilon:"϶",backprime:"‵",backsim:"∽",backsimeq:"⋍",Backslash:"∖",Barv:"⫧",barvee:"⊽",Barwed:"⌆",barwed:"⌅",barwedge:"⌅",bbrk:"⎵",bbrktbrk:"⎶",bcong:"≌",Bcy:"Б",bcy:"б",bdquo:"„",becaus:"∵",Because:"∵",because:"∵",bemptyv:"⦰",bepsi:"϶",bernou:"ℬ",Bernoullis:"ℬ",Beta:"Β",beta:"β",beth:"ℶ",between:"≬",Bfr:"𝔅",bfr:"𝔟",bigcap:"⋂",bigcirc:"◯",bigcup:"⋃",bigodot:"⨀",bigoplus:"⨁",bigotimes:"⨂",bigsqcup:"⨆",bigstar:"★",bigtriangledown:"▽",bigtriangleup:"△",biguplus:"⨄",bigvee:"⋁",bigwedge:"⋀",bkarow:"⤍",blacklozenge:"⧫",blacksquare:"▪",blacktriangle:"▴",blacktriangledown:"▾",blacktriangleleft:"◂",blacktriangleright:"▸",blank:"␣",blk12:"▒",blk14:"░",blk34:"▓",block:"█",bne:"=⃥",bnequiv:"≡⃥",bNot:"⫭",bnot:"⌐",Bopf:"𝔹",bopf:"𝕓",bot:"⊥",bottom:"⊥",bowtie:"⋈",boxbox:"⧉",boxDL:"╗",boxDl:"╖",boxdL:"╕",boxdl:"┐",boxDR:"╔",boxDr:"╓",boxdR:"╒",boxdr:"┌",boxH:"═",boxh:"─",boxHD:"╦",boxHd:"╤",boxhD:"╥",boxhd:"┬",boxHU:"╩",boxHu:"╧",boxhU:"╨",boxhu:"┴",boxminus:"⊟",boxplus:"⊞",boxtimes:"⊠",boxUL:"╝",boxUl:"╜",boxuL:"╛",boxul:"┘",boxUR:"╚",boxUr:"╙",boxuR:"╘",boxur:"└",boxV:"║",boxv:"│",boxVH:"╬",boxVh:"╫",boxvH:"╪",boxvh:"┼",boxVL:"╣",boxVl:"╢",boxvL:"╡",boxvl:"┤",boxVR:"╠",boxVr:"╟",boxvR:"╞",boxvr:"├",bprime:"‵",Breve:"˘",breve:"˘",brvbar:"¦",Bscr:"ℬ",bscr:"𝒷",bsemi:"⁏",bsim:"∽",bsime:"⋍",bsol:"\\",bsolb:"⧅",bsolhsub:"⟈",bull:"•",bullet:"•",bump:"≎",bumpE:"⪮",bumpe:"≏",Bumpeq:"≎",bumpeq:"≏",Cacute:"Ć",cacute:"ć",Cap:"⋒",cap:"∩",capand:"⩄",capbrcup:"⩉",capcap:"⩋",capcup:"⩇",capdot:"⩀",CapitalDifferentialD:"ⅅ",caps:"∩︀",caret:"⁁",caron:"ˇ",Cayleys:"ℭ",ccaps:"⩍",Ccaron:"Č",ccaron:"č",Ccedil:"Ç",ccedil:"ç",Ccirc:"Ĉ",ccirc:"ĉ",Cconint:"∰",ccups:"⩌",ccupssm:"⩐",Cdot:"Ċ",cdot:"ċ",cedil:"¸",Cedilla:"¸",cemptyv:"⦲",cent:"¢",CenterDot:"·",centerdot:"·",Cfr:"ℭ",cfr:"𝔠",CHcy:"Ч",chcy:"ч",check:"✓",checkmark:"✓",Chi:"Χ",chi:"χ",cir:"○",circ:"ˆ",circeq:"≗",circlearrowleft:"↺",circlearrowright:"↻",circledast:"⊛",circledcirc:"⊚",circleddash:"⊝",CircleDot:"⊙",circledR:"®",circledS:"Ⓢ",CircleMinus:"⊖",CirclePlus:"⊕",CircleTimes:"⊗",cirE:"⧃",cire:"≗",cirfnint:"⨐",cirmid:"⫯",cirscir:"⧂",ClockwiseContourIntegral:"∲",CloseCurlyDoubleQuote:"”",CloseCurlyQuote:"’",clubs:"♣",clubsuit:"♣",Colon:"∷",colon:":",Colone:"⩴",colone:"≔",coloneq:"≔",comma:",",commat:"@",comp:"∁",compfn:"∘",complement:"∁",complexes:"ℂ",cong:"≅",congdot:"⩭",Congruent:"≡",Conint:"∯",conint:"∮",ContourIntegral:"∮",Copf:"ℂ",copf:"𝕔",coprod:"∐",Coproduct:"∐",COPY:"©",copy:"©",copysr:"℗",CounterClockwiseContourIntegral:"∳",crarr:"↵",Cross:"⨯",cross:"✗",Cscr:"𝒞",cscr:"𝒸",csub:"⫏",csube:"⫑",csup:"⫐",csupe:"⫒",ctdot:"⋯",cudarrl:"⤸",cudarrr:"⤵",cuepr:"⋞",cuesc:"⋟",cularr:"↶",cularrp:"⤽",Cup:"⋓",cup:"∪",cupbrcap:"⩈",CupCap:"≍",cupcap:"⩆",cupcup:"⩊",cupdot:"⊍",cupor:"⩅",cups:"∪︀",curarr:"↷",curarrm:"⤼",curlyeqprec:"⋞",curlyeqsucc:"⋟",curlyvee:"⋎",curlywedge:"⋏",curren:"¤",curvearrowleft:"↶",curvearrowright:"↷",cuvee:"⋎",cuwed:"⋏",cwconint:"∲",cwint:"∱",cylcty:"⌭",Dagger:"‡",dagger:"†",daleth:"ℸ",Darr:"↡",dArr:"⇓",darr:"↓",dash:"‐",Dashv:"⫤",dashv:"⊣",dbkarow:"⤏",dblac:"˝",Dcaron:"Ď",dcaron:"ď",Dcy:"Д",dcy:"д",DD:"ⅅ",dd:"ⅆ",ddagger:"‡",ddarr:"⇊",DDotrahd:"⤑",ddotseq:"⩷",deg:"°",Del:"∇",Delta:"Δ",delta:"δ",demptyv:"⦱",dfisht:"⥿",Dfr:"𝔇",dfr:"𝔡",dHar:"⥥",dharl:"⇃",dharr:"⇂",DiacriticalAcute:"´",DiacriticalDot:"˙",DiacriticalDoubleAcute:"˝",DiacriticalGrave:"`",DiacriticalTilde:"˜",diam:"⋄",Diamond:"⋄",diamond:"⋄",diamondsuit:"♦",diams:"♦",die:"¨",DifferentialD:"ⅆ",digamma:"ϝ",disin:"⋲",div:"÷",divide:"÷",divideontimes:"⋇",divonx:"⋇",DJcy:"Ђ",djcy:"ђ",dlcorn:"⌞",dlcrop:"⌍",dollar:"$",Dopf:"𝔻",dopf:"𝕕",Dot:"¨",dot:"˙",DotDot:"⃜",doteq:"≐",doteqdot:"≑",DotEqual:"≐",dotminus:"∸",dotplus:"∔",dotsquare:"⊡",doublebarwedge:"⌆",DoubleContourIntegral:"∯",DoubleDot:"¨",DoubleDownArrow:"⇓",DoubleLeftArrow:"⇐",DoubleLeftRightArrow:"⇔",DoubleLeftTee:"⫤",DoubleLongLeftArrow:"⟸",DoubleLongLeftRightArrow:"⟺",DoubleLongRightArrow:"⟹",DoubleRightArrow:"⇒",DoubleRightTee:"⊨",DoubleUpArrow:"⇑",DoubleUpDownArrow:"⇕",DoubleVerticalBar:"∥",DownArrow:"↓",Downarrow:"⇓",downarrow:"↓",DownArrowBar:"⤓",DownArrowUpArrow:"⇵",DownBreve:"̑",downdownarrows:"⇊",downharpoonleft:"⇃",downharpoonright:"⇂",DownLeftRightVector:"⥐",DownLeftTeeVector:"⥞",DownLeftVector:"↽",DownLeftVectorBar:"⥖",DownRightTeeVector:"⥟",DownRightVector:"⇁",DownRightVectorBar:"⥗",DownTee:"⊤",DownTeeArrow:"↧",drbkarow:"⤐",drcorn:"⌟",drcrop:"⌌",Dscr:"𝒟",dscr:"𝒹",DScy:"Ѕ",dscy:"ѕ",dsol:"⧶",Dstrok:"Đ",dstrok:"đ",dtdot:"⋱",dtri:"▿",dtrif:"▾",duarr:"⇵",duhar:"⥯",dwangle:"⦦",DZcy:"Џ",dzcy:"џ",dzigrarr:"⟿",Eacute:"É",eacute:"é",easter:"⩮",Ecaron:"Ě",ecaron:"ě",ecir:"≖",Ecirc:"Ê",ecirc:"ê",ecolon:"≕",Ecy:"Э",ecy:"э",eDDot:"⩷",Edot:"Ė",eDot:"≑",edot:"ė",ee:"ⅇ",efDot:"≒",Efr:"𝔈",efr:"𝔢",eg:"⪚",Egrave:"È",egrave:"è",egs:"⪖",egsdot:"⪘",el:"⪙",Element:"∈",elinters:"⏧",ell:"ℓ",els:"⪕",elsdot:"⪗",Emacr:"Ē",emacr:"ē",empty:"∅",emptyset:"∅",EmptySmallSquare:"◻",emptyv:"∅",EmptyVerySmallSquare:"▫",emsp:" ",emsp13:" ",emsp14:" ",ENG:"Ŋ",eng:"ŋ",ensp:" ",Eogon:"Ę",eogon:"ę",Eopf:"𝔼",eopf:"𝕖",epar:"⋕",eparsl:"⧣",eplus:"⩱",epsi:"ε",Epsilon:"Ε",epsilon:"ε",epsiv:"ϵ",eqcirc:"≖",eqcolon:"≕",eqsim:"≂",eqslantgtr:"⪖",eqslantless:"⪕",Equal:"⩵",equals:"=",EqualTilde:"≂",equest:"≟",Equilibrium:"⇌",equiv:"≡",equivDD:"⩸",eqvparsl:"⧥",erarr:"⥱",erDot:"≓",Escr:"ℰ",escr:"ℯ",esdot:"≐",Esim:"⩳",esim:"≂",Eta:"Η",eta:"η",ETH:"Ð",eth:"ð",Euml:"Ë",euml:"ë",euro:"€",excl:"!",exist:"∃",Exists:"∃",expectation:"ℰ",ExponentialE:"ⅇ",exponentiale:"ⅇ",fallingdotseq:"≒",Fcy:"Ф",fcy:"ф",female:"♀",ffilig:"ffi",fflig:"ff",ffllig:"ffl",Ffr:"𝔉",ffr:"𝔣",filig:"fi",FilledSmallSquare:"◼",FilledVerySmallSquare:"▪",fjlig:"fj",flat:"♭",fllig:"fl",fltns:"▱",fnof:"ƒ",Fopf:"𝔽",fopf:"𝕗",ForAll:"∀",forall:"∀",fork:"⋔",forkv:"⫙",Fouriertrf:"ℱ",fpartint:"⨍",frac12:"½",frac13:"⅓",frac14:"¼",frac15:"⅕",frac16:"⅙",frac18:"⅛",frac23:"⅔",frac25:"⅖",frac34:"¾",frac35:"⅗",frac38:"⅜",frac45:"⅘",frac56:"⅚",frac58:"⅝",frac78:"⅞",frasl:"⁄",frown:"⌢",Fscr:"ℱ",fscr:"𝒻",gacute:"ǵ",Gamma:"Γ",gamma:"γ",Gammad:"Ϝ",gammad:"ϝ",gap:"⪆",Gbreve:"Ğ",gbreve:"ğ",Gcedil:"Ģ",Gcirc:"Ĝ",gcirc:"ĝ",Gcy:"Г",gcy:"г",Gdot:"Ġ",gdot:"ġ",gE:"≧",ge:"≥",gEl:"⪌",gel:"⋛",geq:"≥",geqq:"≧",geqslant:"⩾",ges:"⩾",gescc:"⪩",gesdot:"⪀",gesdoto:"⪂",gesdotol:"⪄",gesl:"⋛︀",gesles:"⪔",Gfr:"𝔊",gfr:"𝔤",Gg:"⋙",gg:"≫",ggg:"⋙",gimel:"ℷ",GJcy:"Ѓ",gjcy:"ѓ",gl:"≷",gla:"⪥",glE:"⪒",glj:"⪤",gnap:"⪊",gnapprox:"⪊",gnE:"≩",gne:"⪈",gneq:"⪈",gneqq:"≩",gnsim:"⋧",Gopf:"𝔾",gopf:"𝕘",grave:"`",GreaterEqual:"≥",GreaterEqualLess:"⋛",GreaterFullEqual:"≧",GreaterGreater:"⪢",GreaterLess:"≷",GreaterSlantEqual:"⩾",GreaterTilde:"≳",Gscr:"𝒢",gscr:"ℊ",gsim:"≳",gsime:"⪎",gsiml:"⪐",GT:">",Gt:"≫",gt:">",gtcc:"⪧",gtcir:"⩺",gtdot:"⋗",gtlPar:"⦕",gtquest:"⩼",gtrapprox:"⪆",gtrarr:"⥸",gtrdot:"⋗",gtreqless:"⋛",gtreqqless:"⪌",gtrless:"≷",gtrsim:"≳",gvertneqq:"≩︀",gvnE:"≩︀",Hacek:"ˇ",hairsp:" ",half:"½",hamilt:"ℋ",HARDcy:"Ъ",hardcy:"ъ",hArr:"⇔",harr:"↔",harrcir:"⥈",harrw:"↭",Hat:"^",hbar:"ℏ",Hcirc:"Ĥ",hcirc:"ĥ",hearts:"♥",heartsuit:"♥",hellip:"…",hercon:"⊹",Hfr:"ℌ",hfr:"𝔥",HilbertSpace:"ℋ",hksearow:"⤥",hkswarow:"⤦",hoarr:"⇿",homtht:"∻",hookleftarrow:"↩",hookrightarrow:"↪",Hopf:"ℍ",hopf:"𝕙",horbar:"―",HorizontalLine:"─",Hscr:"ℋ",hscr:"𝒽",hslash:"ℏ",Hstrok:"Ħ",hstrok:"ħ",HumpDownHump:"≎",HumpEqual:"≏",hybull:"⁃",hyphen:"‐",Iacute:"Í",iacute:"í",ic:"⁣",Icirc:"Î",icirc:"î",Icy:"И",icy:"и",Idot:"İ",IEcy:"Е",iecy:"е",iexcl:"¡",iff:"⇔",Ifr:"ℑ",ifr:"𝔦",Igrave:"Ì",igrave:"ì",ii:"ⅈ",iiiint:"⨌",iiint:"∭",iinfin:"⧜",iiota:"℩",IJlig:"IJ",ijlig:"ij",Im:"ℑ",Imacr:"Ī",imacr:"ī",image:"ℑ",ImaginaryI:"ⅈ",imagline:"ℐ",imagpart:"ℑ",imath:"ı",imof:"⊷",imped:"Ƶ",Implies:"⇒",in:"∈",incare:"℅",infin:"∞",infintie:"⧝",inodot:"ı",Int:"∬",int:"∫",intcal:"⊺",integers:"ℤ",Integral:"∫",intercal:"⊺",Intersection:"⋂",intlarhk:"⨗",intprod:"⨼",InvisibleComma:"⁣",InvisibleTimes:"⁢",IOcy:"Ё",iocy:"ё",Iogon:"Į",iogon:"į",Iopf:"𝕀",iopf:"𝕚",Iota:"Ι",iota:"ι",iprod:"⨼",iquest:"¿",Iscr:"ℐ",iscr:"𝒾",isin:"∈",isindot:"⋵",isinE:"⋹",isins:"⋴",isinsv:"⋳",isinv:"∈",it:"⁢",Itilde:"Ĩ",itilde:"ĩ",Iukcy:"І",iukcy:"і",Iuml:"Ï",iuml:"ï",Jcirc:"Ĵ",jcirc:"ĵ",Jcy:"Й",jcy:"й",Jfr:"𝔍",jfr:"𝔧",jmath:"ȷ",Jopf:"𝕁",jopf:"𝕛",Jscr:"𝒥",jscr:"𝒿",Jsercy:"Ј",jsercy:"ј",Jukcy:"Є",jukcy:"є",Kappa:"Κ",kappa:"κ",kappav:"ϰ",Kcedil:"Ķ",kcedil:"ķ",Kcy:"К",kcy:"к",Kfr:"𝔎",kfr:"𝔨",kgreen:"ĸ",KHcy:"Х",khcy:"х",KJcy:"Ќ",kjcy:"ќ",Kopf:"𝕂",kopf:"𝕜",Kscr:"𝒦",kscr:"𝓀",lAarr:"⇚",Lacute:"Ĺ",lacute:"ĺ",laemptyv:"⦴",lagran:"ℒ",Lambda:"Λ",lambda:"λ",Lang:"⟪",lang:"⟨",langd:"⦑",langle:"⟨",lap:"⪅",Laplacetrf:"ℒ",laquo:"«",Larr:"↞",lArr:"⇐",larr:"←",larrb:"⇤",larrbfs:"⤟",larrfs:"⤝",larrhk:"↩",larrlp:"↫",larrpl:"⤹",larrsim:"⥳",larrtl:"↢",lat:"⪫",lAtail:"⤛",latail:"⤙",late:"⪭",lates:"⪭︀",lBarr:"⤎",lbarr:"⤌",lbbrk:"❲",lbrace:"{",lbrack:"[",lbrke:"⦋",lbrksld:"⦏",lbrkslu:"⦍",Lcaron:"Ľ",lcaron:"ľ",Lcedil:"Ļ",lcedil:"ļ",lceil:"⌈",lcub:"{",Lcy:"Л",lcy:"л",ldca:"⤶",ldquo:"“",ldquor:"„",ldrdhar:"⥧",ldrushar:"⥋",ldsh:"↲",lE:"≦",le:"≤",LeftAngleBracket:"⟨",LeftArrow:"←",Leftarrow:"⇐",leftarrow:"←",LeftArrowBar:"⇤",LeftArrowRightArrow:"⇆",leftarrowtail:"↢",LeftCeiling:"⌈",LeftDoubleBracket:"⟦",LeftDownTeeVector:"⥡",LeftDownVector:"⇃",LeftDownVectorBar:"⥙",LeftFloor:"⌊",leftharpoondown:"↽",leftharpoonup:"↼",leftleftarrows:"⇇",LeftRightArrow:"↔",Leftrightarrow:"⇔",leftrightarrow:"↔",leftrightarrows:"⇆",leftrightharpoons:"⇋",leftrightsquigarrow:"↭",LeftRightVector:"⥎",LeftTee:"⊣",LeftTeeArrow:"↤",LeftTeeVector:"⥚",leftthreetimes:"⋋",LeftTriangle:"⊲",LeftTriangleBar:"⧏",LeftTriangleEqual:"⊴",LeftUpDownVector:"⥑",LeftUpTeeVector:"⥠",LeftUpVector:"↿",LeftUpVectorBar:"⥘",LeftVector:"↼",LeftVectorBar:"⥒",lEg:"⪋",leg:"⋚",leq:"≤",leqq:"≦",leqslant:"⩽",les:"⩽",lescc:"⪨",lesdot:"⩿",lesdoto:"⪁",lesdotor:"⪃",lesg:"⋚︀",lesges:"⪓",lessapprox:"⪅",lessdot:"⋖",lesseqgtr:"⋚",lesseqqgtr:"⪋",LessEqualGreater:"⋚",LessFullEqual:"≦",LessGreater:"≶",lessgtr:"≶",LessLess:"⪡",lesssim:"≲",LessSlantEqual:"⩽",LessTilde:"≲",lfisht:"⥼",lfloor:"⌊",Lfr:"𝔏",lfr:"𝔩",lg:"≶",lgE:"⪑",lHar:"⥢",lhard:"↽",lharu:"↼",lharul:"⥪",lhblk:"▄",LJcy:"Љ",ljcy:"љ",Ll:"⋘",ll:"≪",llarr:"⇇",llcorner:"⌞",Lleftarrow:"⇚",llhard:"⥫",lltri:"◺",Lmidot:"Ŀ",lmidot:"ŀ",lmoust:"⎰",lmoustache:"⎰",lnap:"⪉",lnapprox:"⪉",lnE:"≨",lne:"⪇",lneq:"⪇",lneqq:"≨",lnsim:"⋦",loang:"⟬",loarr:"⇽",lobrk:"⟦",LongLeftArrow:"⟵",Longleftarrow:"⟸",longleftarrow:"⟵",LongLeftRightArrow:"⟷",Longleftrightarrow:"⟺",longleftrightarrow:"⟷",longmapsto:"⟼",LongRightArrow:"⟶",Longrightarrow:"⟹",longrightarrow:"⟶",looparrowleft:"↫",looparrowright:"↬",lopar:"⦅",Lopf:"𝕃",lopf:"𝕝",loplus:"⨭",lotimes:"⨴",lowast:"∗",lowbar:"_",LowerLeftArrow:"↙",LowerRightArrow:"↘",loz:"◊",lozenge:"◊",lozf:"⧫",lpar:"(",lparlt:"⦓",lrarr:"⇆",lrcorner:"⌟",lrhar:"⇋",lrhard:"⥭",lrm:"‎",lrtri:"⊿",lsaquo:"‹",Lscr:"ℒ",lscr:"𝓁",Lsh:"↰",lsh:"↰",lsim:"≲",lsime:"⪍",lsimg:"⪏",lsqb:"[",lsquo:"‘",lsquor:"‚",Lstrok:"Ł",lstrok:"ł",LT:"<",Lt:"≪",lt:"<",ltcc:"⪦",ltcir:"⩹",ltdot:"⋖",lthree:"⋋",ltimes:"⋉",ltlarr:"⥶",ltquest:"⩻",ltri:"◃",ltrie:"⊴",ltrif:"◂",ltrPar:"⦖",lurdshar:"⥊",luruhar:"⥦",lvertneqq:"≨︀",lvnE:"≨︀",macr:"¯",male:"♂",malt:"✠",maltese:"✠",Map:"⤅",map:"↦",mapsto:"↦",mapstodown:"↧",mapstoleft:"↤",mapstoup:"↥",marker:"▮",mcomma:"⨩",Mcy:"М",mcy:"м",mdash:"—",mDDot:"∺",measuredangle:"∡",MediumSpace:" ",Mellintrf:"ℳ",Mfr:"𝔐",mfr:"𝔪",mho:"℧",micro:"µ",mid:"∣",midast:"*",midcir:"⫰",middot:"·",minus:"−",minusb:"⊟",minusd:"∸",minusdu:"⨪",MinusPlus:"∓",mlcp:"⫛",mldr:"…",mnplus:"∓",models:"⊧",Mopf:"𝕄",mopf:"𝕞",mp:"∓",Mscr:"ℳ",mscr:"𝓂",mstpos:"∾",Mu:"Μ",mu:"μ",multimap:"⊸",mumap:"⊸",nabla:"∇",Nacute:"Ń",nacute:"ń",nang:"∠⃒",nap:"≉",napE:"⩰̸",napid:"≋̸",napos:"ʼn",napprox:"≉",natur:"♮",natural:"♮",naturals:"ℕ",nbsp:" ",nbump:"≎̸",nbumpe:"≏̸",ncap:"⩃",Ncaron:"Ň",ncaron:"ň",Ncedil:"Ņ",ncedil:"ņ",ncong:"≇",ncongdot:"⩭̸",ncup:"⩂",Ncy:"Н",ncy:"н",ndash:"–",ne:"≠",nearhk:"⤤",neArr:"⇗",nearr:"↗",nearrow:"↗",nedot:"≐̸",NegativeMediumSpace:"​",NegativeThickSpace:"​",NegativeThinSpace:"​",NegativeVeryThinSpace:"​",nequiv:"≢",nesear:"⤨",nesim:"≂̸",NestedGreaterGreater:"≫",NestedLessLess:"≪",NewLine:"\n",nexist:"∄",nexists:"∄",Nfr:"𝔑",nfr:"𝔫",ngE:"≧̸",nge:"≱",ngeq:"≱",ngeqq:"≧̸",ngeqslant:"⩾̸",nges:"⩾̸",nGg:"⋙̸",ngsim:"≵",nGt:"≫⃒",ngt:"≯",ngtr:"≯",nGtv:"≫̸",nhArr:"⇎",nharr:"↮",nhpar:"⫲",ni:"∋",nis:"⋼",nisd:"⋺",niv:"∋",NJcy:"Њ",njcy:"њ",nlArr:"⇍",nlarr:"↚",nldr:"‥",nlE:"≦̸",nle:"≰",nLeftarrow:"⇍",nleftarrow:"↚",nLeftrightarrow:"⇎",nleftrightarrow:"↮",nleq:"≰",nleqq:"≦̸",nleqslant:"⩽̸",nles:"⩽̸",nless:"≮",nLl:"⋘̸",nlsim:"≴",nLt:"≪⃒",nlt:"≮",nltri:"⋪",nltrie:"⋬",nLtv:"≪̸",nmid:"∤",NoBreak:"⁠",NonBreakingSpace:" ",Nopf:"ℕ",nopf:"𝕟",Not:"⫬",not:"¬",NotCongruent:"≢",NotCupCap:"≭",NotDoubleVerticalBar:"∦",NotElement:"∉",NotEqual:"≠",NotEqualTilde:"≂̸",NotExists:"∄",NotGreater:"≯",NotGreaterEqual:"≱",NotGreaterFullEqual:"≧̸",NotGreaterGreater:"≫̸",NotGreaterLess:"≹",NotGreaterSlantEqual:"⩾̸",NotGreaterTilde:"≵",NotHumpDownHump:"≎̸",NotHumpEqual:"≏̸",notin:"∉",notindot:"⋵̸",notinE:"⋹̸",notinva:"∉",notinvb:"⋷",notinvc:"⋶",NotLeftTriangle:"⋪",NotLeftTriangleBar:"⧏̸",NotLeftTriangleEqual:"⋬",NotLess:"≮",NotLessEqual:"≰",NotLessGreater:"≸",NotLessLess:"≪̸",NotLessSlantEqual:"⩽̸",NotLessTilde:"≴",NotNestedGreaterGreater:"⪢̸",NotNestedLessLess:"⪡̸",notni:"∌",notniva:"∌",notnivb:"⋾",notnivc:"⋽",NotPrecedes:"⊀",NotPrecedesEqual:"⪯̸",NotPrecedesSlantEqual:"⋠",NotReverseElement:"∌",NotRightTriangle:"⋫",NotRightTriangleBar:"⧐̸",NotRightTriangleEqual:"⋭",NotSquareSubset:"⊏̸",NotSquareSubsetEqual:"⋢",NotSquareSuperset:"⊐̸",NotSquareSupersetEqual:"⋣",NotSubset:"⊂⃒",NotSubsetEqual:"⊈",NotSucceeds:"⊁",NotSucceedsEqual:"⪰̸",NotSucceedsSlantEqual:"⋡",NotSucceedsTilde:"≿̸",NotSuperset:"⊃⃒",NotSupersetEqual:"⊉",NotTilde:"≁",NotTildeEqual:"≄",NotTildeFullEqual:"≇",NotTildeTilde:"≉",NotVerticalBar:"∤",npar:"∦",nparallel:"∦",nparsl:"⫽⃥",npart:"∂̸",npolint:"⨔",npr:"⊀",nprcue:"⋠",npre:"⪯̸",nprec:"⊀",npreceq:"⪯̸",nrArr:"⇏",nrarr:"↛",nrarrc:"⤳̸",nrarrw:"↝̸",nRightarrow:"⇏",nrightarrow:"↛",nrtri:"⋫",nrtrie:"⋭",nsc:"⊁",nsccue:"⋡",nsce:"⪰̸",Nscr:"𝒩",nscr:"𝓃",nshortmid:"∤",nshortparallel:"∦",nsim:"≁",nsime:"≄",nsimeq:"≄",nsmid:"∤",nspar:"∦",nsqsube:"⋢",nsqsupe:"⋣",nsub:"⊄",nsubE:"⫅̸",nsube:"⊈",nsubset:"⊂⃒",nsubseteq:"⊈",nsubseteqq:"⫅̸",nsucc:"⊁",nsucceq:"⪰̸",nsup:"⊅",nsupE:"⫆̸",nsupe:"⊉",nsupset:"⊃⃒",nsupseteq:"⊉",nsupseteqq:"⫆̸",ntgl:"≹",Ntilde:"Ñ",ntilde:"ñ",ntlg:"≸",ntriangleleft:"⋪",ntrianglelefteq:"⋬",ntriangleright:"⋫",ntrianglerighteq:"⋭",Nu:"Ν",nu:"ν",num:"#",numero:"№",numsp:" ",nvap:"≍⃒",nVDash:"⊯",nVdash:"⊮",nvDash:"⊭",nvdash:"⊬",nvge:"≥⃒",nvgt:">⃒",nvHarr:"⤄",nvinfin:"⧞",nvlArr:"⤂",nvle:"≤⃒",nvlt:"<⃒",nvltrie:"⊴⃒",nvrArr:"⤃",nvrtrie:"⊵⃒",nvsim:"∼⃒",nwarhk:"⤣",nwArr:"⇖",nwarr:"↖",nwarrow:"↖",nwnear:"⤧",Oacute:"Ó",oacute:"ó",oast:"⊛",ocir:"⊚",Ocirc:"Ô",ocirc:"ô",Ocy:"О",ocy:"о",odash:"⊝",Odblac:"Ő",odblac:"ő",odiv:"⨸",odot:"⊙",odsold:"⦼",OElig:"Œ",oelig:"œ",ofcir:"⦿",Ofr:"𝔒",ofr:"𝔬",ogon:"˛",Ograve:"Ò",ograve:"ò",ogt:"⧁",ohbar:"⦵",ohm:"Ω",oint:"∮",olarr:"↺",olcir:"⦾",olcross:"⦻",oline:"‾",olt:"⧀",Omacr:"Ō",omacr:"ō",Omega:"Ω",omega:"ω",Omicron:"Ο",omicron:"ο",omid:"⦶",ominus:"⊖",Oopf:"𝕆",oopf:"𝕠",opar:"⦷",OpenCurlyDoubleQuote:"“",OpenCurlyQuote:"‘",operp:"⦹",oplus:"⊕",Or:"⩔",or:"∨",orarr:"↻",ord:"⩝",order:"ℴ",orderof:"ℴ",ordf:"ª",ordm:"º",origof:"⊶",oror:"⩖",orslope:"⩗",orv:"⩛",oS:"Ⓢ",Oscr:"𝒪",oscr:"ℴ",Oslash:"Ø",oslash:"ø",osol:"⊘",Otilde:"Õ",otilde:"õ",Otimes:"⨷",otimes:"⊗",otimesas:"⨶",Ouml:"Ö",ouml:"ö",ovbar:"⌽",OverBar:"‾",OverBrace:"⏞",OverBracket:"⎴",OverParenthesis:"⏜",par:"∥",para:"¶",parallel:"∥",parsim:"⫳",parsl:"⫽",part:"∂",PartialD:"∂",Pcy:"П",pcy:"п",percnt:"%",period:".",permil:"‰",perp:"⊥",pertenk:"‱",Pfr:"𝔓",pfr:"𝔭",Phi:"Φ",phi:"φ",phiv:"ϕ",phmmat:"ℳ",phone:"☎",Pi:"Π",pi:"π",pitchfork:"⋔",piv:"ϖ",planck:"ℏ",planckh:"ℎ",plankv:"ℏ",plus:"+",plusacir:"⨣",plusb:"⊞",pluscir:"⨢",plusdo:"∔",plusdu:"⨥",pluse:"⩲",PlusMinus:"±",plusmn:"±",plussim:"⨦",plustwo:"⨧",pm:"±",Poincareplane:"ℌ",pointint:"⨕",Popf:"ℙ",popf:"𝕡",pound:"£",Pr:"⪻",pr:"≺",prap:"⪷",prcue:"≼",prE:"⪳",pre:"⪯",prec:"≺",precapprox:"⪷",preccurlyeq:"≼",Precedes:"≺",PrecedesEqual:"⪯",PrecedesSlantEqual:"≼",PrecedesTilde:"≾",preceq:"⪯",precnapprox:"⪹",precneqq:"⪵",precnsim:"⋨",precsim:"≾",Prime:"″",prime:"′",primes:"ℙ",prnap:"⪹",prnE:"⪵",prnsim:"⋨",prod:"∏",Product:"∏",profalar:"⌮",profline:"⌒",profsurf:"⌓",prop:"∝",Proportion:"∷",Proportional:"∝",propto:"∝",prsim:"≾",prurel:"⊰",Pscr:"𝒫",pscr:"𝓅",Psi:"Ψ",psi:"ψ",puncsp:" ",Qfr:"𝔔",qfr:"𝔮",qint:"⨌",Qopf:"ℚ",qopf:"𝕢",qprime:"⁗",Qscr:"𝒬",qscr:"𝓆",quaternions:"ℍ",quatint:"⨖",quest:"?",questeq:"≟",QUOT:'"',quot:'"',rAarr:"⇛",race:"∽̱",Racute:"Ŕ",racute:"ŕ",radic:"√",raemptyv:"⦳",Rang:"⟫",rang:"⟩",rangd:"⦒",range:"⦥",rangle:"⟩",raquo:"»",Rarr:"↠",rArr:"⇒",rarr:"→",rarrap:"⥵",rarrb:"⇥",rarrbfs:"⤠",rarrc:"⤳",rarrfs:"⤞",rarrhk:"↪",rarrlp:"↬",rarrpl:"⥅",rarrsim:"⥴",Rarrtl:"⤖",rarrtl:"↣",rarrw:"↝",rAtail:"⤜",ratail:"⤚",ratio:"∶",rationals:"ℚ",RBarr:"⤐",rBarr:"⤏",rbarr:"⤍",rbbrk:"❳",rbrace:"}",rbrack:"]",rbrke:"⦌",rbrksld:"⦎",rbrkslu:"⦐",Rcaron:"Ř",rcaron:"ř",Rcedil:"Ŗ",rcedil:"ŗ",rceil:"⌉",rcub:"}",Rcy:"Р",rcy:"р",rdca:"⤷",rdldhar:"⥩",rdquo:"”",rdquor:"”",rdsh:"↳",Re:"ℜ",real:"ℜ",realine:"ℛ",realpart:"ℜ",reals:"ℝ",rect:"▭",REG:"®",reg:"®",ReverseElement:"∋",ReverseEquilibrium:"⇋",ReverseUpEquilibrium:"⥯",rfisht:"⥽",rfloor:"⌋",Rfr:"ℜ",rfr:"𝔯",rHar:"⥤",rhard:"⇁",rharu:"⇀",rharul:"⥬",Rho:"Ρ",rho:"ρ",rhov:"ϱ",RightAngleBracket:"⟩",RightArrow:"→",Rightarrow:"⇒",rightarrow:"→",RightArrowBar:"⇥",RightArrowLeftArrow:"⇄",rightarrowtail:"↣",RightCeiling:"⌉",RightDoubleBracket:"⟧",RightDownTeeVector:"⥝",RightDownVector:"⇂",RightDownVectorBar:"⥕",RightFloor:"⌋",rightharpoondown:"⇁",rightharpoonup:"⇀",rightleftarrows:"⇄",rightleftharpoons:"⇌",rightrightarrows:"⇉",rightsquigarrow:"↝",RightTee:"⊢",RightTeeArrow:"↦",RightTeeVector:"⥛",rightthreetimes:"⋌",RightTriangle:"⊳",RightTriangleBar:"⧐",RightTriangleEqual:"⊵",RightUpDownVector:"⥏",RightUpTeeVector:"⥜",RightUpVector:"↾",RightUpVectorBar:"⥔",RightVector:"⇀",RightVectorBar:"⥓",ring:"˚",risingdotseq:"≓",rlarr:"⇄",rlhar:"⇌",rlm:"‏",rmoust:"⎱",rmoustache:"⎱",rnmid:"⫮",roang:"⟭",roarr:"⇾",robrk:"⟧",ropar:"⦆",Ropf:"ℝ",ropf:"𝕣",roplus:"⨮",rotimes:"⨵",RoundImplies:"⥰",rpar:")",rpargt:"⦔",rppolint:"⨒",rrarr:"⇉",Rrightarrow:"⇛",rsaquo:"›",Rscr:"ℛ",rscr:"𝓇",Rsh:"↱",rsh:"↱",rsqb:"]",rsquo:"’",rsquor:"’",rthree:"⋌",rtimes:"⋊",rtri:"▹",rtrie:"⊵",rtrif:"▸",rtriltri:"⧎",RuleDelayed:"⧴",ruluhar:"⥨",rx:"℞",Sacute:"Ś",sacute:"ś",sbquo:"‚",Sc:"⪼",sc:"≻",scap:"⪸",Scaron:"Š",scaron:"š",sccue:"≽",scE:"⪴",sce:"⪰",Scedil:"Ş",scedil:"ş",Scirc:"Ŝ",scirc:"ŝ",scnap:"⪺",scnE:"⪶",scnsim:"⋩",scpolint:"⨓",scsim:"≿",Scy:"С",scy:"с",sdot:"⋅",sdotb:"⊡",sdote:"⩦",searhk:"⤥",seArr:"⇘",searr:"↘",searrow:"↘",sect:"§",semi:";",seswar:"⤩",setminus:"∖",setmn:"∖",sext:"✶",Sfr:"𝔖",sfr:"𝔰",sfrown:"⌢",sharp:"♯",SHCHcy:"Щ",shchcy:"щ",SHcy:"Ш",shcy:"ш",ShortDownArrow:"↓",ShortLeftArrow:"←",shortmid:"∣",shortparallel:"∥",ShortRightArrow:"→",ShortUpArrow:"↑",shy:"­",Sigma:"Σ",sigma:"σ",sigmaf:"ς",sigmav:"ς",sim:"∼",simdot:"⩪",sime:"≃",simeq:"≃",simg:"⪞",simgE:"⪠",siml:"⪝",simlE:"⪟",simne:"≆",simplus:"⨤",simrarr:"⥲",slarr:"←",SmallCircle:"∘",smallsetminus:"∖",smashp:"⨳",smeparsl:"⧤",smid:"∣",smile:"⌣",smt:"⪪",smte:"⪬",smtes:"⪬︀",SOFTcy:"Ь",softcy:"ь",sol:"/",solb:"⧄",solbar:"⌿",Sopf:"𝕊",sopf:"𝕤",spades:"♠",spadesuit:"♠",spar:"∥",sqcap:"⊓",sqcaps:"⊓︀",sqcup:"⊔",sqcups:"⊔︀",Sqrt:"√",sqsub:"⊏",sqsube:"⊑",sqsubset:"⊏",sqsubseteq:"⊑",sqsup:"⊐",sqsupe:"⊒",sqsupset:"⊐",sqsupseteq:"⊒",squ:"□",Square:"□",square:"□",SquareIntersection:"⊓",SquareSubset:"⊏",SquareSubsetEqual:"⊑",SquareSuperset:"⊐",SquareSupersetEqual:"⊒",SquareUnion:"⊔",squarf:"▪",squf:"▪",srarr:"→",Sscr:"𝒮",sscr:"𝓈",ssetmn:"∖",ssmile:"⌣",sstarf:"⋆",Star:"⋆",star:"☆",starf:"★",straightepsilon:"ϵ",straightphi:"ϕ",strns:"¯",Sub:"⋐",sub:"⊂",subdot:"⪽",subE:"⫅",sube:"⊆",subedot:"⫃",submult:"⫁",subnE:"⫋",subne:"⊊",subplus:"⪿",subrarr:"⥹",Subset:"⋐",subset:"⊂",subseteq:"⊆",subseteqq:"⫅",SubsetEqual:"⊆",subsetneq:"⊊",subsetneqq:"⫋",subsim:"⫇",subsub:"⫕",subsup:"⫓",succ:"≻",succapprox:"⪸",succcurlyeq:"≽",Succeeds:"≻",SucceedsEqual:"⪰",SucceedsSlantEqual:"≽",SucceedsTilde:"≿",succeq:"⪰",succnapprox:"⪺",succneqq:"⪶",succnsim:"⋩",succsim:"≿",SuchThat:"∋",Sum:"∑",sum:"∑",sung:"♪",Sup:"⋑",sup:"⊃",sup1:"¹",sup2:"²",sup3:"³",supdot:"⪾",supdsub:"⫘",supE:"⫆",supe:"⊇",supedot:"⫄",Superset:"⊃",SupersetEqual:"⊇",suphsol:"⟉",suphsub:"⫗",suplarr:"⥻",supmult:"⫂",supnE:"⫌",supne:"⊋",supplus:"⫀",Supset:"⋑",supset:"⊃",supseteq:"⊇",supseteqq:"⫆",supsetneq:"⊋",supsetneqq:"⫌",supsim:"⫈",supsub:"⫔",supsup:"⫖",swarhk:"⤦",swArr:"⇙",swarr:"↙",swarrow:"↙",swnwar:"⤪",szlig:"ß",Tab:"\t",target:"⌖",Tau:"Τ",tau:"τ",tbrk:"⎴",Tcaron:"Ť",tcaron:"ť",Tcedil:"Ţ",tcedil:"ţ",Tcy:"Т",tcy:"т",tdot:"⃛",telrec:"⌕",Tfr:"𝔗",tfr:"𝔱",there4:"∴",Therefore:"∴",therefore:"∴",Theta:"Θ",theta:"θ",thetasym:"ϑ",thetav:"ϑ",thickapprox:"≈",thicksim:"∼",ThickSpace:"  ",thinsp:" ",ThinSpace:" ",thkap:"≈",thksim:"∼",THORN:"Þ",thorn:"þ",Tilde:"∼",tilde:"˜",TildeEqual:"≃",TildeFullEqual:"≅",TildeTilde:"≈",times:"×",timesb:"⊠",timesbar:"⨱",timesd:"⨰",tint:"∭",toea:"⤨",top:"⊤",topbot:"⌶",topcir:"⫱",Topf:"𝕋",topf:"𝕥",topfork:"⫚",tosa:"⤩",tprime:"‴",TRADE:"™",trade:"™",triangle:"▵",triangledown:"▿",triangleleft:"◃",trianglelefteq:"⊴",triangleq:"≜",triangleright:"▹",trianglerighteq:"⊵",tridot:"◬",trie:"≜",triminus:"⨺",TripleDot:"⃛",triplus:"⨹",trisb:"⧍",tritime:"⨻",trpezium:"⏢",Tscr:"𝒯",tscr:"𝓉",TScy:"Ц",tscy:"ц",TSHcy:"Ћ",tshcy:"ћ",Tstrok:"Ŧ",tstrok:"ŧ",twixt:"≬",twoheadleftarrow:"↞",twoheadrightarrow:"↠",Uacute:"Ú",uacute:"ú",Uarr:"↟",uArr:"⇑",uarr:"↑",Uarrocir:"⥉",Ubrcy:"Ў",ubrcy:"ў",Ubreve:"Ŭ",ubreve:"ŭ",Ucirc:"Û",ucirc:"û",Ucy:"У",ucy:"у",udarr:"⇅",Udblac:"Ű",udblac:"ű",udhar:"⥮",ufisht:"⥾",Ufr:"𝔘",ufr:"𝔲",Ugrave:"Ù",ugrave:"ù",uHar:"⥣",uharl:"↿",uharr:"↾",uhblk:"▀",ulcorn:"⌜",ulcorner:"⌜",ulcrop:"⌏",ultri:"◸",Umacr:"Ū",umacr:"ū",uml:"¨",UnderBar:"_",UnderBrace:"⏟",UnderBracket:"⎵",UnderParenthesis:"⏝",Union:"⋃",UnionPlus:"⊎",Uogon:"Ų",uogon:"ų",Uopf:"𝕌",uopf:"𝕦",UpArrow:"↑",Uparrow:"⇑",uparrow:"↑",UpArrowBar:"⤒",UpArrowDownArrow:"⇅",UpDownArrow:"↕",Updownarrow:"⇕",updownarrow:"↕",UpEquilibrium:"⥮",upharpoonleft:"↿",upharpoonright:"↾",uplus:"⊎",UpperLeftArrow:"↖",UpperRightArrow:"↗",Upsi:"ϒ",upsi:"υ",upsih:"ϒ",Upsilon:"Υ",upsilon:"υ",UpTee:"⊥",UpTeeArrow:"↥",upuparrows:"⇈",urcorn:"⌝",urcorner:"⌝",urcrop:"⌎",Uring:"Ů",uring:"ů",urtri:"◹",Uscr:"𝒰",uscr:"𝓊",utdot:"⋰",Utilde:"Ũ",utilde:"ũ",utri:"▵",utrif:"▴",uuarr:"⇈",Uuml:"Ü",uuml:"ü",uwangle:"⦧",vangrt:"⦜",varepsilon:"ϵ",varkappa:"ϰ",varnothing:"∅",varphi:"ϕ",varpi:"ϖ",varpropto:"∝",vArr:"⇕",varr:"↕",varrho:"ϱ",varsigma:"ς",varsubsetneq:"⊊︀",varsubsetneqq:"⫋︀",varsupsetneq:"⊋︀",varsupsetneqq:"⫌︀",vartheta:"ϑ",vartriangleleft:"⊲",vartriangleright:"⊳",Vbar:"⫫",vBar:"⫨",vBarv:"⫩",Vcy:"В",vcy:"в",VDash:"⊫",Vdash:"⊩",vDash:"⊨",vdash:"⊢",Vdashl:"⫦",Vee:"⋁",vee:"∨",veebar:"⊻",veeeq:"≚",vellip:"⋮",Verbar:"‖",verbar:"|",Vert:"‖",vert:"|",VerticalBar:"∣",VerticalLine:"|",VerticalSeparator:"❘",VerticalTilde:"≀",VeryThinSpace:" ",Vfr:"𝔙",vfr:"𝔳",vltri:"⊲",vnsub:"⊂⃒",vnsup:"⊃⃒",Vopf:"𝕍",vopf:"𝕧",vprop:"∝",vrtri:"⊳",Vscr:"𝒱",vscr:"𝓋",vsubnE:"⫋︀",vsubne:"⊊︀",vsupnE:"⫌︀",vsupne:"⊋︀",Vvdash:"⊪",vzigzag:"⦚",Wcirc:"Ŵ",wcirc:"ŵ",wedbar:"⩟",Wedge:"⋀",wedge:"∧",wedgeq:"≙",weierp:"℘",Wfr:"𝔚",wfr:"𝔴",Wopf:"𝕎",wopf:"𝕨",wp:"℘",wr:"≀",wreath:"≀",Wscr:"𝒲",wscr:"𝓌",xcap:"⋂",xcirc:"◯",xcup:"⋃",xdtri:"▽",Xfr:"𝔛",xfr:"𝔵",xhArr:"⟺",xharr:"⟷",Xi:"Ξ",xi:"ξ",xlArr:"⟸",xlarr:"⟵",xmap:"⟼",xnis:"⋻",xodot:"⨀",Xopf:"𝕏",xopf:"𝕩",xoplus:"⨁",xotime:"⨂",xrArr:"⟹",xrarr:"⟶",Xscr:"𝒳",xscr:"𝓍",xsqcup:"⨆",xuplus:"⨄",xutri:"△",xvee:"⋁",xwedge:"⋀",Yacute:"Ý",yacute:"ý",YAcy:"Я",yacy:"я",Ycirc:"Ŷ",ycirc:"ŷ",Ycy:"Ы",ycy:"ы",yen:"¥",Yfr:"𝔜",yfr:"𝔶",YIcy:"Ї",yicy:"ї",Yopf:"𝕐",yopf:"𝕪",Yscr:"𝒴",yscr:"𝓎",YUcy:"Ю",yucy:"ю",Yuml:"Ÿ",yuml:"ÿ",Zacute:"Ź",zacute:"ź",Zcaron:"Ž",zcaron:"ž",Zcy:"З",zcy:"з",Zdot:"Ż",zdot:"ż",zeetrf:"ℨ",ZeroWidthSpace:"​",Zeta:"Ζ",zeta:"ζ",Zfr:"ℨ",zfr:"𝔷",ZHcy:"Ж",zhcy:"ж",zigrarr:"⇝",Zopf:"ℤ",zopf:"𝕫",Zscr:"𝒵",zscr:"𝓏",zwj:"‍",zwnj:"‌"},r=Object.prototype.hasOwnProperty;function n(e){return o=e,(n=t)&&r.call(n,o)?t[e]:e;var n,o}var o=Object.prototype.hasOwnProperty;function s(e,t){return!!e&&o.call(e,t)}function i(e){return[].slice.call(arguments,1).forEach(function(t){if(t){if("object"!=typeof t)throw new TypeError(t+"must be object");Object.keys(t).forEach(function(r){e[r]=t[r]})}}),e}var a=/\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g;function u(e){return e.indexOf("\\")<0?e:e.replace(a,"$1")}function l(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(65535!=(65535&e)&&65534!=(65535&e)&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function c(e){if(e>65535){var t=55296+((e-=65536)>>10),r=56320+(1023&e);return String.fromCharCode(t,r)}return String.fromCharCode(e)}var p=/&([a-z#][a-z0-9]{1,31});/gi,h=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i;function f(e,t){var r=0,o=n(t);return t!==o?o:35===t.charCodeAt(0)&&h.test(t)&&l(r="x"===t[1].toLowerCase()?parseInt(t.slice(2),16):parseInt(t.slice(1),10))?c(r):e}function g(e){return e.indexOf("&")<0?e:e.replace(p,f)}var d=/[&<>"]/,m=/[&<>"]/g,b={"&":"&","<":"<",">":">",'"':"""};function v(e){return b[e]}function k(e){return d.test(e)?e.replace(m,v):e}var A=Object.freeze({isString:function(e){return"[object String]"===function(e){return Object.prototype.toString.call(e)}(e)},has:s,assign:i,unescapeMd:u,isValidEntityCode:l,fromCodePoint:c,replaceEntities:g,escapeHtml:k}),y={};y.blockquote_open=function(){return"
    \n"},y.blockquote_close=function(e,t){return"
    "+x(e,t)},y.code=function(e,t){return e[t].block?"
    "+k(e[t].content)+"
    "+x(e,t):""+k(e[t].content)+""},y.fence=function(e,t,r,n,o){var i,a,l=e[t],c="",p=r.langPrefix;if(l.params){if(a=(i=l.params.split(/\s+/g)).join(" "),s(o.rules.fence_custom,i[0]))return o.rules.fence_custom[i[0]](e,t,r,n,o);c=' class="'+p+k(g(u(a)))+'"'}return"
    "+(r.highlight&&r.highlight.apply(r.highlight,[l.content].concat(i))||k(l.content))+"
    "+x(e,t)},y.fence_custom={},y.heading_open=function(e,t){return""},y.heading_close=function(e,t){return"\n"},y.hr=function(e,t,r){return(r.xhtmlOut?"
    ":"
    ")+x(e,t)},y.bullet_list_open=function(){return"
      \n"},y.bullet_list_close=function(e,t){return"
    "+x(e,t)},y.list_item_open=function(){return"
  • "},y.list_item_close=function(){return"
  • \n"},y.ordered_list_open=function(e,t){var r=e[t];return"1?' start="'+r.order+'"':"")+">\n"},y.ordered_list_close=function(e,t){return""+x(e,t)},y.paragraph_open=function(e,t){return e[t].tight?"":"

    "},y.paragraph_close=function(e,t){var r=!(e[t].tight&&t&&"inline"===e[t-1].type&&!e[t-1].content);return(e[t].tight?"":"

    ")+(r?x(e,t):"")},y.link_open=function(e,t,r){var n=e[t].title?' title="'+k(g(e[t].title))+'"':"",o=r.linkTarget?' target="'+r.linkTarget+'"':"";return'"},y.link_close=function(){return""},y.image=function(e,t,r){var n=' src="'+k(e[t].src)+'"',o=e[t].title?' title="'+k(g(e[t].title))+'"':"";return""},y.table_open=function(){return"\n"},y.table_close=function(){return"
    \n"},y.thead_open=function(){return"\n"},y.thead_close=function(){return"\n"},y.tbody_open=function(){return"\n"},y.tbody_close=function(){return"\n"},y.tr_open=function(){return""},y.tr_close=function(){return"\n"},y.th_open=function(e,t){var r=e[t];return""},y.th_close=function(){return""},y.td_open=function(e,t){var r=e[t];return""},y.td_close=function(){return""},y.strong_open=function(){return""},y.strong_close=function(){return""},y.em_open=function(){return""},y.em_close=function(){return""},y.del_open=function(){return""},y.del_close=function(){return""},y.ins_open=function(){return""},y.ins_close=function(){return""},y.mark_open=function(){return""},y.mark_close=function(){return""},y.sub=function(e,t){return""+k(e[t].content)+""},y.sup=function(e,t){return""+k(e[t].content)+""},y.hardbreak=function(e,t,r){return r.xhtmlOut?"
    \n":"
    \n"},y.softbreak=function(e,t,r){return r.breaks?r.xhtmlOut?"
    \n":"
    \n":"\n"},y.text=function(e,t){return k(e[t].content)},y.htmlblock=function(e,t){return e[t].content},y.htmltag=function(e,t){return e[t].content},y.abbr_open=function(e,t){return''},y.abbr_close=function(){return""},y.footnote_ref=function(e,t){var r=Number(e[t].id+1).toString(),n="fnref"+r;return e[t].subId>0&&(n+=":"+e[t].subId),'['+r+"]"},y.footnote_block_open=function(e,t,r){return(r.xhtmlOut?'
    \n':'
    \n')+'
    \n
      \n'},y.footnote_block_close=function(){return"
    \n
    \n"},y.footnote_open=function(e,t){return'
  • '},y.footnote_close=function(){return"
  • \n"},y.footnote_anchor=function(e,t){var r="fnref"+Number(e[t].id+1).toString();return e[t].subId>0&&(r+=":"+e[t].subId),' '},y.dl_open=function(){return"
    \n"},y.dt_open=function(){return"
    "},y.dd_open=function(){return"
    "},y.dl_close=function(){return"
    \n"},y.dt_close=function(){return"\n"},y.dd_close=function(){return"\n"};var x=y.getBreak=function(e,t){return(t=function e(t,r){return++r>=t.length-2?r:"paragraph_open"===t[r].type&&t[r].tight&&"inline"===t[r+1].type&&0===t[r+1].content.length&&"paragraph_close"===t[r+2].type&&t[r+2].tight?e(t,r+2):r}(e,t))1)break;if(41===r&&--n<0)break;t++}return s!==t&&(o=u(e.src.slice(s,t)),!!e.parser.validateLink(o)&&(e.linkContent=o,e.pos=t,!0))}function F(e,t){var r,n=t,o=e.posMax,s=e.src.charCodeAt(t);if(34!==s&&39!==s&&40!==s)return!1;for(t++,40===s&&(s=41);t=e.length)&&!I.test(e[t])}function H(e,t,r){return e.substr(0,t)+r+e.substr(t+1)}var V=[["block",function(e){e.inlineMode?e.tokens.push({type:"inline",content:e.src.replace(/\n/g," ").trim(),level:0,lines:[0,1],children:[]}):e.block.parse(e.src,e.options,e.env,e.tokens)}],["abbr",function(e){var t,r,n,o,s=e.tokens;if(!e.inlineMode)for(t=1,r=s.length-1;t0?i[t].count:1,n=0;n=0;t--)if("text"===(s=o[t]).type){for(u=0,i=s.content,c.lastIndex=0,l=s.level,a=[];p=c.exec(i);)c.lastIndex>u&&a.push({type:"text",content:i.slice(u,p.index+p[1].length),level:l}),a.push({type:"abbr_open",title:e.env.abbreviations[":"+p[2]],level:l++}),a.push({type:"text",content:p[2],level:l}),a.push({type:"abbr_close",level:--l}),u=c.lastIndex-p[3].length;a.length&&(u=0;s--)if("inline"===e.tokens[s].type)for(t=(o=e.tokens[s].children).length-1;t>=0;t--)"text"===(r=o[t]).type&&(n=r.content,n=(i=n).indexOf("(")<0?i:i.replace(N,function(e,t){return z[t.toLowerCase()]}),R.test(n)&&(n=n.replace(/\+-/g,"±").replace(/\.{2,}/g,"…").replace(/([?!])…/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---([^-]|$)/gm,"$1—$2").replace(/(^|\s)--(\s|$)/gm,"$1–$2").replace(/(^|[^-\s])--([^-\s]|$)/gm,"$1–$2")),r.content=n)}],["smartquotes",function(e){var t,r,n,o,s,i,a,u,l,c,p,h,f,g,d,m,b;if(e.options.typographer)for(b=[],d=e.tokens.length-1;d>=0;d--)if("inline"===e.tokens[d].type)for(m=e.tokens[d].children,b.length=0,t=0;t=0&&!(b[f].level<=a);f--);b.length=f+1,s=0,i=(n=r.content).length;e:for(;s=0&&(c=b[f],!(b[f].level=(o=e.eMarks[t])?-1:42!==(r=e.src.charCodeAt(n++))&&45!==r&&43!==r?-1:n=o)return-1;if((r=e.src.charCodeAt(n++))<48||r>57)return-1;for(;;){if(n>=o)return-1;if(!((r=e.src.charCodeAt(n++))>=48&&r<=57)){if(41===r||46===r)break;return-1}}return n=this.eMarks[e]},$.prototype.skipEmptyLines=function(e){for(var t=this.lineMax;er;)if(t!==this.src.charCodeAt(--e))return e+1;return e},$.prototype.getLines=function(e,t,r,n){var o,s,i,a,u,l=e;if(e>=t)return"";if(l+1===t)return s=this.bMarks[l]+Math.min(this.tShift[l],r),i=n?this.eMarks[l]+1:this.eMarks[l],this.src.slice(s,i);for(a=new Array(t-e),o=0;lr&&(u=r),u<0&&(u=0),s=this.bMarks[l]+u,i=l+1]/,K=/^<\/([a-zA-Z]{1,15})[\s>]/;function Q(e,t){var r=e.bMarks[t]+e.blkIndent,n=e.eMarks[t];return e.src.substr(r,n-r)}function X(e,t){var r,n,o=e.bMarks[t]+e.tShift[t],s=e.eMarks[t];return o>=s?-1:126!==(n=e.src.charCodeAt(o++))&&58!==n?-1:o===(r=e.skipSpaces(o))?-1:r>=s?-1:r}var ee=[["code",function(e,t,r){var n,o;if(e.tShift[t]-e.blkIndent<4)return!1;for(o=n=t+1;n=4))break;o=++n}return e.line=n,e.tokens.push({type:"code",content:e.getLines(t,o,4+e.blkIndent,!0),block:!0,lines:[t,e.line],level:e.level}),!0}],["fences",function(e,t,r,n){var o,s,i,a,u,l=!1,c=e.bMarks[t]+e.tShift[t],p=e.eMarks[t];if(c+3>p)return!1;if(126!==(o=e.src.charCodeAt(c))&&96!==o)return!1;if(u=c,(s=(c=e.skipChars(c,o))-u)<3)return!1;if((i=e.src.slice(c,p).trim()).indexOf("`")>=0)return!1;if(n)return!0;for(a=t;!(++a>=r||(c=u=e.bMarks[a]+e.tShift[a])<(p=e.eMarks[a])&&e.tShift[a]=4||(c=e.skipChars(c,o))-um)return!1;if(62!==e.src.charCodeAt(d++))return!1;if(e.level>=e.options.maxNesting)return!1;if(n)return!0;for(32===e.src.charCodeAt(d)&&d++,u=e.blkIndent,e.blkIndent=0,a=[e.bMarks[t]],e.bMarks[t]=d,s=(d=d=m,i=[e.tShift[t]],e.tShift[t]=d-e.bMarks[t],p=e.parser.ruler.getRules("blockquote"),o=t+1;o=(m=e.eMarks[o]));o++)if(62!==e.src.charCodeAt(d++)){if(s)break;for(g=!1,h=0,f=p.length;h=m,i.push(e.tShift[o]),e.tShift[o]=d-e.bMarks[o];for(l=e.parentType,e.parentType="blockquote",e.tokens.push({type:"blockquote_open",lines:c=[t,0],level:e.level++}),e.parser.tokenize(e,t,o),e.tokens.push({type:"blockquote_close",level:--e.level}),e.parentType=l,c[1]=e.line,h=0;hu)return!1;if(42!==(o=e.src.charCodeAt(a++))&&45!==o&&95!==o)return!1;for(s=1;a=0)d=!0;else{if(!((p=Z(e,t))>=0))return!1;d=!1}if(e.level>=e.options.maxNesting)return!1;if(g=e.src.charCodeAt(p-1),n)return!0;for(b=e.tokens.length,d?(c=e.bMarks[t]+e.tShift[t],f=Number(e.src.substr(c,p-c-1)),e.tokens.push({type:"ordered_list_open",order:f,lines:k=[t,0],level:e.level++})):e.tokens.push({type:"bullet_list_open",lines:k=[t,0],level:e.level++}),o=t,v=!1,y=e.parser.ruler.getRules("list");!(!(o=e.eMarks[o]?1:m-p)>4&&(h=1),h<1&&(h=1),s=p-e.bMarks[o]+h,e.tokens.push({type:"list_item_open",lines:A=[t,0],level:e.level++}),a=e.blkIndent,u=e.tight,i=e.tShift[t],l=e.parentType,e.tShift[t]=m-e.bMarks[t],e.blkIndent=s,e.tight=!0,e.parentType="list",e.parser.tokenize(e,t,r,!0),e.tight&&!v||(E=!1),v=e.line-t>1&&e.isEmpty(e.line-1),e.blkIndent=a,e.tShift[t]=i,e.tight=u,e.parentType=l,e.tokens.push({type:"list_item_close",level:--e.level}),o=t=e.line,A[1]=o,m=e.bMarks[t],o>=r)||e.isEmpty(o)||e.tShift[o]c)return!1;if(91!==e.src.charCodeAt(l))return!1;if(94!==e.src.charCodeAt(l+1))return!1;if(e.level>=e.options.maxNesting)return!1;for(a=l+2;a=c||58!==e.src.charCodeAt(++a)||!n&&(a++,e.env.footnotes||(e.env.footnotes={}),e.env.footnotes.refs||(e.env.footnotes.refs={}),u=e.src.slice(l+2,a-2),e.env.footnotes.refs[":"+u]=-1,e.tokens.push({type:"footnote_reference_open",label:u,level:e.level++}),o=e.bMarks[t],s=e.tShift[t],i=e.parentType,e.tShift[t]=e.skipSpaces(a)-a,e.bMarks[t]=a,e.blkIndent+=4,e.parentType="footnote",e.tShift[t]=u)return!1;if(35!==(o=e.src.charCodeAt(a))||a>=u)return!1;for(s=1,o=e.src.charCodeAt(++a);35===o&&a6||aa&&32===e.src.charCodeAt(i-1)&&(u=i),e.line=t+1,e.tokens.push({type:"heading_open",hLevel:s,lines:[t,e.line],level:e.level}),a=r||e.tShift[i]3||(o=e.bMarks[i]+e.tShift[i])>=(s=e.eMarks[i])||45!==(n=e.src.charCodeAt(o))&&61!==n||(o=e.skipChars(o,n),(o=e.skipSpaces(o))3||a+2>=u)return!1;if(60!==e.src.charCodeAt(a))return!1;if(33===(o=e.src.charCodeAt(a+1))||63===o){if(n)return!0}else{if(47!==o&&!function(e){var t=32|e;return t>=97&&t<=122}(o))return!1;if(47===o){if(!(s=e.src.slice(a,u).match(K)))return!1}else if(!(s=e.src.slice(a,u).match(Y)))return!1;if(!0!==J[s[1].toLowerCase()])return!1;if(n)return!0}for(i=t+1;ir)return!1;if(u=t+1,e.tShift[u]=e.eMarks[u])return!1;if(124!==(o=e.src.charCodeAt(i))&&45!==o&&58!==o)return!1;if(s=Q(e,t+1),!/^[-:| ]+$/.test(s))return!1;if((l=s.split("|"))<=2)return!1;for(p=[],a=0;a=0;if(c=t+1,e.isEmpty(c)&&++c>r)return!1;if(e.tShift[c]=e.options.maxNesting)return!1;l=e.tokens.length,e.tokens.push({type:"dl_open",lines:u=[t,0],level:e.level++}),i=t,s=c;e:for(;;){for(b=!0,m=!1,e.tokens.push({type:"dt_open",lines:[i,i],level:e.level++}),e.tokens.push({type:"inline",content:e.getLines(i,i+1,e.blkIndent,!1).trim(),level:e.level+1,lines:[i,i],children:[]}),e.tokens.push({type:"dt_close",level:--e.level});;){if(e.tokens.push({type:"dd_open",lines:a=[c,0],level:e.level++}),d=e.tight,h=e.ddIndent,p=e.blkIndent,g=e.tShift[s],f=e.parentType,e.blkIndent=e.ddIndent=e.tShift[s]+2,e.tShift[s]=o-e.bMarks[s],e.tight=!0,e.parentType="deflist",e.parser.tokenize(e,s,r,!0),e.tight&&!m||(b=!1),m=e.line-s>1&&e.isEmpty(e.line-1),e.tShift[s]=g,e.tight=d,e.parentType=f,e.blkIndent=p,e.ddIndent=h,e.tokens.push({type:"dd_close",level:--e.level}),a[1]=c=e.line,c>=r)break e;if(e.tShift[c]=r)break;if(i=c,e.isEmpty(i))break;if(e.tShift[i]=r)break;if(e.isEmpty(s)&&s++,s>=r)break;if(e.tShift[s]3)){for(o=!1,s=0,i=a.length;s=r))&&!(e.tShift[i]=0&&(e=e.replace(re,function(t,r){var n;return 10===e.charCodeAt(r)?(s=r+1,i=0,t):(n=" ".slice((r-s-i)%4),i=r-s+1,n)})),o=new $(e,this,t,r,n),this.tokenize(o,o.line,o.lineMax)};for(var ie=[],ae=0;ae<256;ae++)ie.push(0);function ue(e){return e>=48&&e<=57||e>=65&&e<=90||e>=97&&e<=122}function le(e,t){var r,n,o,s=t,i=!0,a=!0,u=e.posMax,l=e.src.charCodeAt(t);for(r=t>0?e.src.charCodeAt(t-1):-1;s=u&&(i=!1),(o=s-t)>=4?i=a=!1:(32!==(n=s?@[]^_`{|}~-".split("").forEach(function(e){ie[e.charCodeAt(0)]=1});var ce=/\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g;var pe=/\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g;var he=["coap","doi","javascript","aaa","aaas","about","acap","cap","cid","crid","data","dav","dict","dns","file","ftp","geo","go","gopher","h323","http","https","iax","icap","im","imap","info","ipp","iris","iris.beep","iris.xpc","iris.xpcs","iris.lwz","ldap","mailto","mid","msrp","msrps","mtqp","mupdate","news","nfs","ni","nih","nntp","opaquelocktoken","pop","pres","rtsp","service","session","shttp","sieve","sip","sips","sms","snmp","soap.beep","soap.beeps","tag","tel","telnet","tftp","thismessage","tn3270","tip","tv","urn","vemmi","ws","wss","xcon","xcon-userid","xmlrpc.beep","xmlrpc.beeps","xmpp","z39.50r","z39.50s","adiumxtra","afp","afs","aim","apt","attachment","aw","beshare","bitcoin","bolo","callto","chrome","chrome-extension","com-eventbrite-attendee","content","cvs","dlna-playsingle","dlna-playcontainer","dtn","dvb","ed2k","facetime","feed","finger","fish","gg","git","gizmoproject","gtalk","hcp","icon","ipn","irc","irc6","ircs","itms","jar","jms","keyparc","lastfm","ldaps","magnet","maps","market","message","mms","ms-help","msnim","mumble","mvn","notes","oid","palm","paparazzi","platform","proxy","psyc","query","res","resource","rmi","rsync","rtmp","secondlife","sftp","sgn","skype","smb","soldat","spotify","ssh","steam","svn","teamspeak","things","udp","unreal","ut2004","ventrilo","view-source","webcal","wtai","wyciwyg","xfire","xri","ymsgr"],fe=/^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/,ge=/^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/;function de(e,t){return e=e.source,t=t||"",function r(n,o){return n?(o=o.source||o,e=e.replace(n,o),r):new RegExp(e,t)}}var me=de(/(?:unquoted|single_quoted|double_quoted)/)("unquoted",/[^"'=<>`\x00-\x20]+/)("single_quoted",/'[^']*'/)("double_quoted",/"[^"]*"/)(),be=de(/(?:\s+attr_name(?:\s*=\s*attr_value)?)/)("attr_name",/[a-zA-Z_:][a-zA-Z0-9:._-]*/)("attr_value",me)(),ve=de(/<[A-Za-z][A-Za-z0-9]*attribute*\s*\/?>/)("attribute",be)(),ke=de(/^(?:open_tag|close_tag|comment|processing|declaration|cdata)/)("open_tag",ve)("close_tag",/<\/[A-Za-z][A-Za-z0-9]*\s*>/)("comment",/|/)("processing",/<[?].*?[?]>/)("declaration",/]*>/)("cdata",//)();var Ae=/^&#((?:x[a-f0-9]{1,8}|[0-9]{1,8}));/i,ye=/^&([a-z][a-z0-9]{1,31});/i;var xe=[["text",function(e,t){for(var r=e.pos;r=0&&32===e.pending.charCodeAt(r))if(r>=1&&32===e.pending.charCodeAt(r-1)){for(var s=r-2;s>=0;s--)if(32!==e.pending.charCodeAt(s)){e.pending=e.pending.substring(0,s+1);break}e.push({type:"hardbreak",level:e.level})}else e.pending=e.pending.slice(0,-1),e.push({type:"softbreak",level:e.level});else e.push({type:"softbreak",level:e.level});for(o++;o=a)return!1;if(126!==e.src.charCodeAt(u+1))return!1;if(e.level>=e.options.maxNesting)return!1;if(s=u>0?e.src.charCodeAt(u-1):-1,i=e.src.charCodeAt(u+2),126===s)return!1;if(126===i)return!1;if(32===i||10===i)return!1;for(n=u+2;nu+3)return e.pos+=n-u,t||(e.pending+=e.src.slice(u,n)),!0;for(e.pos=u+2,o=1;e.pos+1=a)return!1;if(43!==e.src.charCodeAt(u+1))return!1;if(e.level>=e.options.maxNesting)return!1;if(s=u>0?e.src.charCodeAt(u-1):-1,i=e.src.charCodeAt(u+2),43===s)return!1;if(43===i)return!1;if(32===i||10===i)return!1;for(n=u+2;n=a)return!1;if(61!==e.src.charCodeAt(u+1))return!1;if(e.level>=e.options.maxNesting)return!1;if(s=u>0?e.src.charCodeAt(u-1):-1,i=e.src.charCodeAt(u+2),61===s)return!1;if(61===i)return!1;if(32===i||10===i)return!1;for(n=u+2;n=e.options.maxNesting)return!1;for(e.pos=c+r,a=[r];e.pos=o)return!1;if(e.level>=e.options.maxNesting)return!1;for(e.pos=s+1;e.pos=o)return!1;if(e.level>=e.options.maxNesting)return!1;for(e.pos=s+1;e.pos=e.options.maxNesting)return!1;if(r=f+1,(n=D(e,f))<0)return!1;if((a=n+1)=h)return!1;for(f=a,q(e,a)?(s=e.linkContent,a=e.pos):s="",f=a;a=h||41!==e.src.charCodeAt(a))return e.pos=p,!1;a++}else{if(e.linkLevel>0)return!1;for(;a=0?o=e.src.slice(f,a++):a=f-1),o||(void 0===o&&(a=n+1),o=e.src.slice(r,n)),!(u=e.env.references[M(o)]))return e.pos=p,!1;s=u.href,i=u.title}return t||(e.pos=r,e.posMax=n,c?e.push({type:"image",src:s,title:i,alt:e.src.substr(r,n-r),level:e.level}):(e.push({type:"link_open",href:s,title:i,level:e.level++}),e.linkLevel++,e.parser.tokenize(e),e.linkLevel--,e.push({type:"link_close",level:--e.level}))),e.pos=a,e.posMax=h,!0}],["footnote_inline",function(e,t){var r,n,o,s,i=e.posMax,a=e.pos;return!(a+2>=i||94!==e.src.charCodeAt(a)||91!==e.src.charCodeAt(a+1)||e.level>=e.options.maxNesting||(r=a+2,(n=D(e,a+1))<0||(t||(e.env.footnotes||(e.env.footnotes={}),e.env.footnotes.list||(e.env.footnotes.list=[]),o=e.env.footnotes.list.length,e.pos=r,e.posMax=n,e.push({type:"footnote_ref",id:o,level:e.level}),e.linkLevel++,s=e.tokens.length,e.parser.tokenize(e),e.env.footnotes.list[o]={tokens:e.tokens.splice(s)},e.linkLevel--),e.pos=n+1,e.posMax=i,0)))}],["footnote_ref",function(e,t){var r,n,o,s,i=e.posMax,a=e.pos;if(a+3>i)return!1;if(!e.env.footnotes||!e.env.footnotes.refs)return!1;if(91!==e.src.charCodeAt(a))return!1;if(94!==e.src.charCodeAt(a+1))return!1;if(e.level>=e.options.maxNesting)return!1;for(n=a+2;n=i||(n++,r=e.src.slice(a+2,n-1),void 0===e.env.footnotes.refs[":"+r]||(t||(e.env.footnotes.list||(e.env.footnotes.list=[]),e.env.footnotes.refs[":"+r]<0?(o=e.env.footnotes.list.length,e.env.footnotes.list[o]={label:r,count:0},e.env.footnotes.refs[":"+r]=o):o=e.env.footnotes.refs[":"+r],s=e.env.footnotes.list[o].count,e.env.footnotes.list[o].count++,e.push({type:"footnote_ref",id:o,subId:s,level:e.level})),e.pos=n,e.posMax=i,0)))}],["autolink",function(e,t){var r,n,o,s,i,a=e.pos;return!(60!==e.src.charCodeAt(a)||(r=e.src.slice(a)).indexOf(">")<0||((n=r.match(ge))?he.indexOf(n[1].toLowerCase())<0||(i=B(s=n[0].slice(1,-1)),!e.parser.validateLink(s)||(t||(e.push({type:"link_open",href:i,level:e.level}),e.push({type:"text",content:s,level:e.level+1}),e.push({type:"link_close",level:e.level})),e.pos+=n[0].length,0)):!(o=r.match(fe))||(i=B("mailto:"+(s=o[0].slice(1,-1))),!e.parser.validateLink(i)||(t||(e.push({type:"link_open",href:i,level:e.level}),e.push({type:"text",content:s,level:e.level+1}),e.push({type:"link_close",level:e.level})),e.pos+=o[0].length,0))))}],["htmltag",function(e,t){var r,n,o,s=e.pos;return!(!e.options.html||(o=e.posMax,60!==e.src.charCodeAt(s)||s+2>=o||33!==(r=e.src.charCodeAt(s+1))&&63!==r&&47!==r&&!function(e){var t=32|e;return t>=97&&t<=122}(r)||!(n=e.src.slice(s).match(ke))||(t||e.push({type:"htmltag",content:e.src.slice(s,s+n[0].length),level:e.level}),e.pos+=n[0].length,0)))}],["entity",function(e,t){var r,o,s=e.pos,i=e.posMax;if(38!==e.src.charCodeAt(s))return!1;if(s+10)e.pos=r;else{for(t=0;t=s)break}else e.pending+=e.src[e.pos++]}e.pending&&e.pushPending()},we.prototype.parse=function(e,t,r,n){var o=new E(e,this,t,r,n);this.tokenize(o)};var Ee={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkTarget:"",typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{rules:["block","inline","references","replacements","smartquotes","references","abbr2","footnote_tail"]},block:{rules:["blockquote","code","fences","footnote","heading","hr","htmlblock","lheading","list","paragraph","table"]},inline:{rules:["autolink","backticks","del","emphasis","entity","escape","footnote_ref","htmltag","links","newline","text"]}}},full:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkTarget:"",typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{},block:{},inline:{}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkTarget:"",typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{rules:["block","inline","references","abbr2"]},block:{rules:["blockquote","code","fences","heading","hr","htmlblock","lheading","list","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","htmltag","links","newline","text"]}}}};function De(e,t,r){this.src=t,this.env=r,this.options=e.options,this.tokens=[],this.inlineMode=!1,this.inline=e.inline,this.block=e.block,this.renderer=e.renderer,this.typographer=e.typographer}function _e(e,t){"string"!=typeof e&&(t=e,e="default"),t&&null!=t.linkify&&console.warn("linkify option is removed. Use linkify plugin instead:\n\nimport Remarkable from 'remarkable';\nimport linkify from 'remarkable/linkify';\nnew Remarkable().use(linkify)\n"),this.inline=new we,this.block=new te,this.core=new G,this.renderer=new w,this.ruler=new C,this.options={},this.configure(Ee[e]),this.set(t||{})}function Be(e,t){if(Array.prototype.indexOf)return e.indexOf(t);for(var r=0,n=e.length;r=0;r--)!0===t(e[r])&&e.splice(r,1)}function Fe(e){throw new Error("Unhandled case for value: '"+e+"'")}_e.prototype.set=function(e){i(this.options,e)},_e.prototype.configure=function(e){var t=this;if(!e)throw new Error("Wrong `remarkable` preset, check name/content");e.options&&t.set(e.options),e.components&&Object.keys(e.components).forEach(function(r){e.components[r].rules&&t[r].ruler.enable(e.components[r].rules,!0)})},_e.prototype.use=function(e,t){return e(this,t),this},_e.prototype.parse=function(e,t){var r=new De(this,e,t);return this.core.process(r),r.tokens},_e.prototype.render=function(e,t){return t=t||{},this.renderer.render(this.parse(e,t),this.options,t)},_e.prototype.parseInline=function(e,t){var r=new De(this,e,t);return r.inlineMode=!0,this.core.process(r),r.tokens},_e.prototype.renderInline=function(e,t){return t=t||{},this.renderer.render(this.parseInline(e,t),this.options,t)};var Me=function(){function e(e){void 0===e&&(e={}),this.tagName="",this.attrs={},this.innerHTML="",this.whitespaceRegex=/\s+/,this.tagName=e.tagName||"",this.attrs=e.attrs||{},this.innerHTML=e.innerHtml||e.innerHTML||""}return e.prototype.setTagName=function(e){return this.tagName=e,this},e.prototype.getTagName=function(){return this.tagName||""},e.prototype.setAttr=function(e,t){return this.getAttrs()[e]=t,this},e.prototype.getAttr=function(e){return this.getAttrs()[e]},e.prototype.setAttrs=function(e){return Object.assign(this.getAttrs(),e),this},e.prototype.getAttrs=function(){return this.attrs||(this.attrs={})},e.prototype.setClass=function(e){return this.setAttr("class",e)},e.prototype.addClass=function(e){for(var t,r=this.getClass(),n=this.whitespaceRegex,o=r?r.split(n):[],s=e.split(n);t=s.shift();)-1===Be(o,t)&&o.push(t);return this.getAttrs().class=o.join(" "),this},e.prototype.removeClass=function(e){for(var t,r=this.getClass(),n=this.whitespaceRegex,o=r?r.split(n):[],s=e.split(n);o.length&&(t=s.shift());){var i=Be(o,t);-1!==i&&o.splice(i,1)}return this.getAttrs().class=o.join(" "),this},e.prototype.getClass=function(){return this.getAttrs().class||""},e.prototype.hasClass=function(e){return-1!==(" "+this.getClass()+" ").indexOf(" "+e+" ")},e.prototype.setInnerHTML=function(e){return this.innerHTML=e,this},e.prototype.setInnerHtml=function(e){return this.setInnerHTML(e)},e.prototype.getInnerHTML=function(){return this.innerHTML||""},e.prototype.getInnerHtml=function(){return this.getInnerHTML()},e.prototype.toAnchorString=function(){var e=this.getTagName(),t=this.buildAttrsStr();return["<",e,t=t?" "+t:"",">",this.getInnerHtml(),""].join("")},e.prototype.buildAttrsStr=function(){if(!this.attrs)return"";var e=this.getAttrs(),t=[];for(var r in e)e.hasOwnProperty(r)&&t.push(r+'="'+e[r]+'"');return t.join(" ")},e}();var Se=function(){function e(e){void 0===e&&(e={}),this.newWindow=!1,this.truncate={},this.className="",this.newWindow=e.newWindow||!1,this.truncate=e.truncate||{},this.className=e.className||""}return e.prototype.build=function(e){return new Me({tagName:"a",attrs:this.createAttrs(e),innerHtml:this.processAnchorText(e.getAnchorText())})},e.prototype.createAttrs=function(e){var t={href:e.getAnchorHref()},r=this.createCssClass(e);return r&&(t.class=r),this.newWindow&&(t.target="_blank",t.rel="noopener noreferrer"),this.truncate&&this.truncate.length&&this.truncate.length=a)return u.host.length==t?(u.host.substr(0,t-o)+r).substr(0,a+n):i(c,a).substr(0,a+n);var p="";if(u.path&&(p+="/"+u.path),u.query&&(p+="?"+u.query),p){if((c+p).length>=a)return(c+p).length==t?(c+p).substr(0,t):(c+i(p,a-c.length)).substr(0,a+n);c+=p}if(u.fragment){var h="#"+u.fragment;if((c+h).length>=a)return(c+h).length==t?(c+h).substr(0,t):(c+i(h,a-c.length)).substr(0,a+n);c+=h}if(u.scheme&&u.host){var f=u.scheme+"://";if((c+f).length0&&(g=c.substr(-1*Math.floor(a/2))),(c.substr(0,Math.ceil(a/2))+r+g).substr(0,a+n)}(e,r):"middle"===n?function(e,t,r){if(e.length<=t)return e;var n,o;null==r?(r="…",n=8,o=3):(n=r.length,o=r.length);var s=t-o,i="";return s>0&&(i=e.substr(-1*Math.floor(s/2))),(e.substr(0,Math.ceil(s/2))+r+i).substr(0,s+n)}(e,r):function(e,t,r){return function(e,t,r){var n;return e.length>t&&(null==r?(r="…",n=3):n=r.length,e=e.substring(0,t-n)+r),e}(e,t,r)}(e,r)},e}(),Te=function(){function e(e){this.__jsduckDummyDocProp=null,this.matchedText="",this.offset=0,this.tagBuilder=e.tagBuilder,this.matchedText=e.matchedText,this.offset=e.offset}return e.prototype.getMatchedText=function(){return this.matchedText},e.prototype.setOffset=function(e){this.offset=e},e.prototype.getOffset=function(){return this.offset},e.prototype.getCssClassSuffixes=function(){return[this.getType()]},e.prototype.buildTag=function(){return this.tagBuilder.build(this)},e}(),Le=function(e,t){return(Le=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)};function Re(e,t){function r(){this.constructor=e}Le(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}var Ne=function(){return(Ne=Object.assign||function(e){for(var t,r=1,n=arguments.length;r-1},e.isValidUriScheme=function(e){var t=e.match(this.uriSchemeRegex),r=t&&t[0].toLowerCase();return"javascript:"!==r&&"vbscript:"!==r},e.urlMatchDoesNotHaveProtocolOrDot=function(e,t){return!(!e||t&&this.hasFullProtocolRegex.test(t)||-1!==e.indexOf("."))},e.urlMatchDoesNotHaveAtLeastOneWordChar=function(e,t){return!(!e||!t)&&!this.hasWordCharAfterProtocolRegex.test(e)},e.hasFullProtocolRegex=/^[A-Za-z][-.+A-Za-z0-9]*:\/\//,e.uriSchemeRegex=/^[A-Za-z][-.+A-Za-z0-9]*:/,e.hasWordCharAfterProtocolRegex=new RegExp(":[^\\s]*?["+We+"]"),e.ipRegex=/[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?(:[0-9]*)?\/?$/,e}(),ut=function(e){function t(t){var r,n=e.call(this,t)||this;return n.stripPrefix={scheme:!0,www:!0},n.stripTrailingSlash=!0,n.decodePercentEncoding=!0,n.matcherRegex=(r=new RegExp("[/?#](?:["+Qe+"\\-+&@#/%=~_()|'$*\\[\\]?!:,.;✓]*["+Qe+"\\-+&@#/%=~_()|'$*\\[\\]✓])?"),new RegExp(["(?:","(",/(?:[A-Za-z][-.+A-Za-z0-9]{0,63}:(?![A-Za-z][-.+A-Za-z0-9]{0,63}:\/\/)(?!\d+\/?)(?:\/\/)?)/.source,rt(2),")","|","(","(//)?",/(?:www\.)/.source,rt(6),")","|","(","(//)?",rt(10)+"\\.",ot.source,"(?![-"+Ke+"])",")",")","(?::[0-9]+)?","(?:"+r.source+")?"].join(""),"gi")),n.wordCharRegExp=new RegExp("["+Qe+"]"),n.stripPrefix=t.stripPrefix,n.stripTrailingSlash=t.stripTrailingSlash,n.decodePercentEncoding=t.decodePercentEncoding,n}return Re(t,e),t.prototype.parseMatches=function(e){for(var t,r=this.matcherRegex,n=this.stripPrefix,o=this.stripTrailingSlash,s=this.decodePercentEncoding,i=this.tagBuilder,a=[],u=function(){var r=t[0],u=t[1],c=t[4],p=t[5],h=t[9],f=t.index,g=p||h,d=e.charAt(f-1);if(!at.isValid(r,u))return"continue";if(f>0&&"@"===d)return"continue";if(f>0&&g&&l.wordCharRegExp.test(d))return"continue";if(/\?$/.test(r)&&(r=r.substr(0,r.length-1)),l.matchHasUnbalancedClosingParen(r))r=r.substr(0,r.length-1);else{var m=l.matchHasInvalidCharAfterTld(r,u);m>-1&&(r=r.substr(0,m))}var b=["http://","https://"].find(function(e){return!!u&&-1!==u.indexOf(e)});if(b){var v=r.indexOf(b);r=r.substr(v),u=u.substr(v),f+=v}var k=u?"scheme":c?"www":"tld",A=!!u;a.push(new Oe({tagBuilder:i,matchedText:r,offset:f,urlMatchType:k,url:r,protocolUrlMatch:A,protocolRelativeMatch:!!g,stripPrefix:n,stripTrailingSlash:o,decodePercentEncoding:s}))},l=this;null!==(t=r.exec(e));)u();return a},t.prototype.matchHasUnbalancedClosingParen=function(e){var t,r=e.charAt(e.length-1);if(")"===r)t="(";else{if("]"!==r)return!1;t="["}for(var n=0,o=0,s=e.length-1;o"===e?(h=new ft(Ne({},h,{name:P()})),z()):He.test(e)||Ve.test(e)||":"===e||R()}function b(e){">"===e?R():He.test(e)?c=3:R()}function v(e){Ge.test(e)||("/"===e?c=12:">"===e?z():"<"===e?N():"="===e||$e.test(e)||Ze.test(e)?R():c=5)}function k(e){Ge.test(e)?c=6:"/"===e?c=12:"="===e?c=7:">"===e?z():"<"===e?N():$e.test(e)&&R()}function A(e){Ge.test(e)||("/"===e?c=12:"="===e?c=7:">"===e?z():"<"===e?N():$e.test(e)?R():c=5)}function y(e){Ge.test(e)||('"'===e?c=8:"'"===e?c=9:/[>=`]/.test(e)?R():"<"===e?N():c=10)}function x(e){'"'===e&&(c=11)}function w(e){"'"===e&&(c=11)}function C(e){Ge.test(e)?c=4:">"===e?z():"<"===e&&N()}function E(e){Ge.test(e)?c=4:"/"===e?c=12:">"===e?z():"<"===e?N():(c=4,u--)}function D(e){">"===e?(h=new ft(Ne({},h,{isClosing:!0})),z()):c=4}function _(t){"--"===e.substr(u,2)?(u+=2,h=new ft(Ne({},h,{type:"comment"})),c=14):"DOCTYPE"===e.substr(u,7).toUpperCase()?(u+=7,h=new ft(Ne({},h,{type:"doctype"})),c=20):R()}function B(e){"-"===e?c=15:">"===e?R():c=16}function q(e){"-"===e?c=18:">"===e?R():c=16}function F(e){"-"===e&&(c=17)}function M(e){c="-"===e?18:16}function S(e){">"===e?z():"!"===e?c=19:"-"===e||(c=16)}function T(e){"-"===e?c=17:">"===e?z():c=16}function L(e){">"===e?z():"<"===e&&N()}function R(){c=0,h=a}function N(){c=1,h=new ft({idx:u})}function z(){var t=e.slice(p,h.idx);t&&o(t,p),"comment"===h.type?s(h.idx):"doctype"===h.type?i(h.idx):(h.isOpening&&r(h.name,h.idx),h.isClosing&&n(h.name,h.idx)),R(),p=u+1}function P(){var t=h.idx+(h.isClosing?2:1);return e.slice(t,u).toLowerCase()}p=0&&n++},onText:function(e,r){if(0===n){var s=function(e,t){if(!t.global)throw new Error("`splitRegex` must have the 'g' flag set");for(var r,n=[],o=0;r=t.exec(e);)n.push(e.substring(o,r.index)),n.push(r[0]),o=r.index+r[0].length;return n.push(e.substring(o)),n}(e,/( | |<|<|>|>|"|"|')/gi),i=r;s.forEach(function(e,r){if(r%2==0){var n=t.parseText(e,i);o.push.apply(o,n)}i+=e.length})}},onCloseTag:function(e){r.indexOf(e)>=0&&(n=Math.max(n-1,0))},onComment:function(e){},onDoctype:function(e){}}),o=this.compactMatches(o),o=this.removeUnwantedMatches(o)},e.prototype.compactMatches=function(e){e.sort(function(e,t){return e.getOffset()-t.getOffset()});for(var t=0;to?t:t+1;e.splice(i,1);continue}e[t+1].getOffset()/i.test(e)}function bt(){var e=[],t=new gt({stripPrefix:!1,url:!0,email:!0,replaceFn:function(t){switch(t.getType()){case"url":e.push({text:t.matchedText,url:t.getUrl()});break;case"email":e.push({text:t.matchedText,url:"mailto:"+t.getEmail().replace(/^mailto:/i,"")})}return!1}});return{links:e,autolinker:t}}function vt(e){var t,r,n,o,s,i,a,u,l,c,p,h,f,g,d=e.tokens,m=null;for(r=0,n=d.length;r=0;t--)if("link_close"!==(s=o[t]).type){if("htmltag"===s.type&&(g=s.content,/^\s]/i.test(g)&&p>0&&p--,mt(s.content)&&p++),!(p>0)&&"text"===s.type&&dt.test(s.content)){if(m||(h=(m=bt()).links,f=m.autolinker),i=s.content,h.length=0,f.link(i),!h.length)continue;for(a=[],c=s.level,u=0;u":">","'":"'",'"':"""},re=/(?:\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c\udffb|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c\udffb|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c\udffb|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb\udffc]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udffd]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d])|(?:\ud83d[\udc68\udc69])(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a-\udc6d\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5\udeeb\udeec\udef4-\udefa\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd71\udd73-\udd76\udd7a-\udda2\udda5-\uddaa\uddae-\uddb4\uddb7\uddba\uddbc-\uddca\uddd0\uddde-\uddff\ude70-\ude73\ude78-\ude7a\ude80-\ude82\ude90-\ude95]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g,UFE0Fg=/\uFE0F/g,U200D=String.fromCharCode(8205),rescaper=/[&<>'"]/g,shouldntBeParsed=/^(?:iframe|noframes|noscript|script|select|style|textarea)$/,fromCharCode=String.fromCharCode;return twemoji;function createText(text,clean){return document.createTextNode(clean?text.replace(UFE0Fg,""):text)}function escapeHTML(s){return s.replace(rescaper,replacer)}function defaultImageSrcGenerator(icon,options){return"".concat(options.base,options.size,"/",icon,options.ext)}function grabAllTextNodes(node,allText){var childNodes=node.childNodes,length=childNodes.length,subnode,nodeType;while(length--){subnode=childNodes[length];nodeType=subnode.nodeType;if(nodeType===3){allText.push(subnode)}else if(nodeType===1&&!("ownerSVGElement"in subnode)&&!shouldntBeParsed.test(subnode.nodeName.toLowerCase())){grabAllTextNodes(subnode,allText)}}return allText}function grabTheRightIcon(rawText){return toCodePoint(rawText.indexOf(U200D)<0?rawText.replace(UFE0Fg,""):rawText)}function parseNode(node,options){var allText=grabAllTextNodes(node,[]),length=allText.length,attrib,attrname,modified,fragment,subnode,text,match,i,index,img,rawText,iconId,src;while(length--){modified=false;fragment=document.createDocumentFragment();subnode=allText[length];text=subnode.nodeValue;i=0;while(match=re.exec(text)){index=match.index;if(index!==i){fragment.appendChild(createText(text.slice(i,index),true))}rawText=match[0];iconId=grabTheRightIcon(rawText);i=index+rawText.length;src=options.callback(iconId,options);if(iconId&&src){img=new Image;img.onerror=options.onerror;img.setAttribute("draggable","false");attrib=options.attributes(rawText,iconId);for(attrname in attrib){if(attrib.hasOwnProperty(attrname)&&attrname.indexOf("on")!==0&&!img.hasAttribute(attrname)){img.setAttribute(attrname,attrib[attrname])}}img.className=options.className;img.alt=rawText;img.src=src;modified=true;fragment.appendChild(img)}if(!img)fragment.appendChild(createText(rawText,false));img=null}if(modified){if(i")}return ret})}function replacer(m){return escaper[m]}function returnNull(){return null}function toSizeSquaredAsset(value){return typeof value==="number"?value+"x"+value:value}function fromCodePoint(codepoint){var code=typeof codepoint==="string"?parseInt(codepoint,16):codepoint;if(code<65536){return fromCharCode(code)}code-=65536;return fromCharCode(55296+(code>>10),56320+(code&1023))}function parse(what,how){if(!how||typeof how==="function"){how={callback:how}}return(typeof what==="string"?parseString:parseNode)(what,{callback:how.callback||defaultImageSrcGenerator,attributes:typeof how.attributes==="function"?how.attributes:returnNull,base:typeof how.base==="string"?how.base:twemoji.base,ext:how.ext||twemoji.ext,size:how.folder||toSizeSquaredAsset(how.size||twemoji.size),className:how.className||twemoji.className,onerror:how.onerror||twemoji.onerror})}function replace(text,callback){return String(text).replace(re,callback)}function test(text){re.lastIndex=0;var result=re.test(text);re.lastIndex=0;return result}function toCodePoint(unicodeSurrogates,sep){var r=[],c=0,p=0,i=0;while(i; let _initialized_listener: (() => any)[] = []; let _master_volume: number = 1; + let _no_device = false; export function initialize() : boolean { context(); @@ -35,7 +36,7 @@ namespace audio.player { _initialized_listener.unshift(() => { _global_destination = _globalContext.createGain(); - _global_destination.gain.value = _master_volume; + _global_destination.gain.value = _no_device ? 0 : _master_volume; _global_destination.connect(_globalContext.destination); }); if(_globalContext.state == "suspended") { @@ -47,14 +48,14 @@ namespace audio.player { }); } _globalContext.resume(); //We already have our listener - return undefined; + return _globalContext; } if(_globalContext.state == "running") { fire_initialized(); return _globalContext; } - return undefined; + return _globalContext; } export function get_master_volume() : number { @@ -62,6 +63,8 @@ namespace audio.player { } export function set_master_volume(volume: number) { _master_volume = volume; + if(_global_destination) + _global_destination.gain.value = _no_device ? 0 : _master_volume; } export function destination() : AudioNode { @@ -78,13 +81,16 @@ namespace audio.player { _initialized_listener.push(cb); } - export const WEB_DEVICE: Device = {device_id: "", name: "default playback"}; + export const WEB_DEVICE: Device = {device_id: "default", name: "default playback"}; export function available_devices() : Promise { return Promise.resolve([WEB_DEVICE]) } export function set_device(device_id: string) : Promise { + _no_device = !device_id; + _global_destination.gain.value = _no_device ? 0 : _master_volume; + return Promise.resolve(); } diff --git a/web/js/connection/ServerConnection.ts b/web/js/connection/ServerConnection.ts index adf97541..e1e72e41 100644 --- a/web/js/connection/ServerConnection.ts +++ b/web/js/connection/ServerConnection.ts @@ -16,7 +16,6 @@ namespace connection { private _command_boss: ServerConnectionCommandBoss; private _command_handler_default: ConnectionCommandHandler; - private _command_handler_handshake: AbstractCommandHandler; //The new handshake handler :) private _connect_timeout_timer: NodeJS.Timer = undefined; private _connected: boolean = false; @@ -26,6 +25,20 @@ namespace connection { private _connection_state_listener: connection.ConnectionStateListener; private _voice_connection: audio.js.VoiceConnection; + private _ping = { + thread_id: 0, + + last_request: 0, + last_response: 0, + + request_id: 0, + interval: 5000, + timeout: 7500, + + value: 0, + value_native: 0 /* ping value for native (WS) */ + }; + constructor(client : ConnectionHandler) { super(client); @@ -43,6 +56,35 @@ namespace connection { this._voice_connection = new audio.js.VoiceConnection(this); } + destroy() { + this.disconnect("handle destroyed").catch(error => { + console.warn(tr("Failed to disconnect on server connection destroy: %o"), error); + }).then(() => { + clearInterval(this._ping.thread_id); + clearTimeout(this._connect_timeout_timer); + + for(const listener of this._retListener) { + try { + listener.reject("handler destroyed"); + } catch(error) { + console.warn(tr("Failed to reject command promise: %o"), error); + } + } + this._retListener = undefined; + + this.command_helper.destroy(); + + this._command_handler_default && this._command_boss.unregister_handler(this._command_handler_default); + this._command_handler_default = undefined; + + this._voice_connection && this._voice_connection.destroy(); + this._voice_connection = undefined; + + this._command_boss && this._command_boss.destroy(); + this._command_boss = undefined; + }); + } + on_connect: () => void = () => { console.log(tr("Socket connected")); this.client.log.log(log.server.Type.CONNECTION_LOGIN, {}); @@ -55,7 +97,7 @@ namespace connection { } async connect(address : ServerAddress, handshake: HandshakeHandler, timeout?: number) : Promise { - timeout = typeof(timeout) === "number" ? timeout : 0; + timeout = typeof(timeout) === "number" ? timeout : 5000; if(this._connect_timeout_timer) { clearTimeout(this._connect_timeout_timer); @@ -79,7 +121,7 @@ namespace connection { try { local_timeout_timer = setTimeout(async () => { - console.log(tr("Connect timeout triggered!")); + log.error(LogCategory.NETWORKING, tr("Connect timeout triggered!")); try { await this.disconnect(); } catch(error) { @@ -90,17 +132,37 @@ namespace connection { this._connect_timeout_timer = local_timeout_timer; this._socket = (local_socket = new WebSocket('wss://' + address.host + ":" + address.port)); /* this may hangs */ + + if(this._socket != local_socket) + return; /* something had changed and we dont use this connection anymore! */ + + try { + await new Promise((resolve, reject) => { + local_socket.onopen = resolve; + local_socket.onerror = event => reject(event); + local_socket.onclose = event => reject(event); + if(local_socket.readyState == WebSocket.OPEN) + resolve(); + }); + } catch(error) { + log.error(LogCategory.NETWORKING, tr("Failed to wait for connected (%o)"), error); + try { + await this.disconnect(); + } catch(error) { + log.warn(LogCategory.NETWORKING, tr("Failed to close connection after timeout had been triggered! (%o)"), error); + } + this.client.handleDisconnect(DisconnectReason.CONNECT_FAILURE); + return; + } clearTimeout(local_timeout_timer); if(this._connect_timeout_timer == local_timeout_timer) this._connect_timeout_timer = undefined; - if(this._socket != local_socket) return; /* something had changed and we dont use this connection anymore! */ - local_socket.onopen = () => { - if(this._socket != local_socket) return; /* this socket isn't from interest anymore */ + if(this._socket != local_socket) + return; /* this socket isn't from interest anymore */ - this._connected = true; - this.on_connect(); - }; + this._connected = true; + this.on_connect(); local_socket.onclose = event => { if(this._socket != local_socket) return; /* this socket isn't from interest anymore */ @@ -123,6 +185,7 @@ namespace connection { self.handle_socket_message(msg.data); }; + this.updateConnectionState(ConnectionState.INITIALISING); } catch (e) { clearTimeout(local_timeout_timer); @@ -143,10 +206,17 @@ namespace connection { } async disconnect(reason?: string) : Promise { + clearTimeout(this._connect_timeout_timer); + this._connect_timeout_timer = undefined; + + clearTimeout(this._ping.thread_id); + this._ping.thread_id = undefined; + if(typeof(reason) === "string") { //TODO send disconnect reason } + if(this._connectionState == ConnectionState.UNCONNECTED) return; @@ -189,16 +259,35 @@ namespace connection { arguments: json["data"] }); - if(json["command"] === "initserver" && this._voice_connection) - this._voice_connection.createSession(); /* FIXME: Move it to a handler boss and not here! */ + if(json["command"] === "initserver") { + this._ping.thread_id = setInterval(() => this.do_ping(), this._ping.interval); + this.do_ping(); + this.updateConnectionState(ConnectionState.CONNECTED); + if(this._voice_connection) + this._voice_connection.createSession(); /* FIXME: Move it to a handler boss and not here! */ + } group.end(); } else if(json["type"] === "WebRTC") { if(this._voice_connection) this._voice_connection.handleControlPacket(json); else console.log(tr("Dropping WebRTC command packet, because we haven't a bridge.")) - } - else { + } else if(json["type"] === "ping") { + this.sendData(JSON.stringify({ + type: 'pong', + payload: json["payload"] + })); + } else if(json["type"] === "pong") { + const id = parseInt(json["payload"]); + if(id != this._ping.request_id) { + log.warn(LogCategory.NETWORKING, tr("Received pong which is older than the last request. Delay may over %oms? (Index: %o, Current index: %o)"), this._ping.timeout, id, this._ping.request_id); + } else { + this._ping.last_response = 'now' in performance ? performance.now() : Date.now(); + this._ping.value = this._ping.last_response - this._ping.last_request; + this._ping.value_native = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */ + log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3)); + } + } else { console.log(tr("Unknown command type %o"), json["type"]); } } else { @@ -223,6 +312,7 @@ namespace connection { return value; } }); + } send_command(command: string, data?: any | any[], _options?: CommandOptions) : Promise { @@ -267,7 +357,7 @@ namespace connection { } connected() : boolean { - return this._socket && this._socket.readyState == WebSocket.OPEN; + return !!this._socket && this._socket.readyState == WebSocket.OPEN; } support_voice(): boolean { @@ -297,9 +387,36 @@ namespace connection { remote_address(): ServerAddress { return this._remote_address; } + + private do_ping() { + if(this._ping.last_request + this._ping.timeout < Date.now()) { + this._ping.value = this._ping.timeout; + this._ping.last_response = this._ping.last_request + 1; + } + if(this._ping.last_response > this._ping.last_request) { + this._ping.last_request = 'now' in performance ? performance.now() : Date.now(); + this.sendData(JSON.stringify({ + type: 'ping', + payload: (++this._ping.request_id).toString() + })); + } + } + + ping(): { native: number; javascript?: number } { + return { + javascript: this._ping.value, + native: this._ping.value_native + }; + } } export function spawn_server_connection(handle: ConnectionHandler) : AbstractServerConnection { return new ServerConnection(handle); /* will be overridden by the client */ } + + export function destroy_server_connection(handle: AbstractServerConnection) { + if(!(handle instanceof ServerConnection)) + throw "invalid handle"; + handle.destroy(); + } } \ No newline at end of file diff --git a/web/js/voice/JavascriptRecorder.ts b/web/js/voice/JavascriptRecorder.ts index 6fb20058..fb68d0ea 100644 --- a/web/js/voice/JavascriptRecorder.ts +++ b/web/js/voice/JavascriptRecorder.ts @@ -2,8 +2,8 @@ namespace audio { export namespace recorder { - /* TODO: Recognise if we got device permission and update list */ let _queried_devices: JavascriptInputDevice[]; + let _queried_permissioned: boolean = false; export interface JavascriptInputDevice extends InputDevice { device_id: string; @@ -11,18 +11,24 @@ namespace audio { } async function query_devices() { - const general_supported = !!getUserMediaFunction(); + const general_supported = !!getUserMediaFunctionPromise(); try { const context = player.context(); const devices = await navigator.mediaDevices.enumerateDevices(); + _queried_permissioned = false; + if(devices.filter(e => !!e.label).length > 0) + _queried_permissioned = true; + _queried_devices = devices.filter(e => e.kind === "audioinput").map((e: MediaDeviceInfo): JavascriptInputDevice => { return { channels: context ? context.destination.channelCount : 2, sample_rate: context ? context.sampleRate : 44100, default_input: e.deviceId == "default", + + driver: "WebAudio", name: e.label || "device-id{" + e.deviceId+ "}", supported: general_supported, @@ -30,9 +36,11 @@ namespace audio { device_id: e.deviceId, group_id: e.groupId, - unique_id: e.groupId + "-" + e.deviceId + unique_id: e.deviceId } }); + if(_queried_devices.length > 0 && _queried_devices.filter(e => e.default_input).length == 0) + _queried_devices[0].default_input = true; } catch(error) { console.warn(tr("Failed to query microphone devices (%o)"), error); _queried_devices = []; @@ -52,7 +60,17 @@ namespace audio { export function create_input() : AbstractInput { return new JavascriptInput(); } - query_devices(); /* general query */ + export async function create_levelmeter(device: InputDevice) : Promise { + const meter = new JavascriptLevelmeter(device as any); + await meter.initialize(); + return meter; + } + + loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { + function: async () => { query_devices(); }, /* May wait for it? */ + priority: 10, + name: "query media devices" + }); export namespace filter { export abstract class JAbstractFilter implements Filter { @@ -76,7 +94,7 @@ namespace audio { } export class JThresholdFilter extends JAbstractFilter implements ThresholdFilter { - private static update_task_interval = 20; /* 20ms */ + public static update_task_interval = 20; /* 20ms */ type = Type.THRESHOLD; callback_level?: (value: number) => any; @@ -163,17 +181,16 @@ namespace audio { return Promise.resolve(); } - private _analyse() { + public static process(buffer: Uint8Array, ftt_size: number, previous: number, smooth: number) { let level; { let total = 0, float, rms; - this._analyser.getByteTimeDomainData(this._analyse_buffer); - for(let index = 0; index < this._analyser.fftSize; index++) { - float = ( this._analyse_buffer[index++] / 0x7f ) - 1; + for(let index = 0; index < ftt_size; index++) { + float = ( buffer[index++] / 0x7f ) - 1; total += (float * float); } - rms = Math.sqrt(total / this._analyser.fftSize); + rms = Math.sqrt(total / ftt_size); let db = 20 * ( Math.log(rms) / Math.log(10) ); // sanity check @@ -181,16 +198,19 @@ namespace audio { level = 100 + ( db * 1.92 ); } - { - const last_level = this._current_level; - let smooth; - if(this._silence_count == 0) - smooth = this._smooth_release; - else - smooth = this._smooth_attack; - this._current_level = last_level * smooth + level * (1 - smooth); - } + return previous * smooth + level * (1 - smooth); + } + private _analyse() { + this._analyser.getByteTimeDomainData(this._analyse_buffer); + + let smooth; + if(this._silence_count == 0) + smooth = this._smooth_release; + else + smooth = this._smooth_attack; + + this._current_level = JThresholdFilter.process(this._analyse_buffer, this._analyser.fftSize, this._current_level, smooth); this._update_gain_node(); if(this.callback_level) @@ -272,11 +292,14 @@ namespace audio { private _audio_context: AudioContext; private _source_node: AudioNode; /* last node which could be connected to the target; target might be the _consumer_node */ private _consumer_callback_node: ScriptProcessorNode; + private _volume_node: GainNode; private _mute_node: GainNode; private _filters: filter.Filter[] = []; private _filter_active: boolean = false; + private _volume: number = 1; + callback_begin: () => any = undefined; callback_end: () => any = undefined; @@ -297,6 +320,9 @@ namespace audio { this._consumer_callback_node.addEventListener('audioprocess', event => this._audio_callback(event)); this._consumer_callback_node.connect(this._mute_node); + this._volume_node = this._audio_context.createGain(); + this._volume_node.gain.value = this._volume; + if(this._state === InputState.INITIALIZING) this.start(); } @@ -308,9 +334,9 @@ namespace audio { filter.finalize(); } - if(this._audio_context && this._current_audio_stream) { + if(this._audio_context && this._volume_node) { const active_filter = filters.filter(e => e.is_enabled()); - let stream: AudioNode = this._current_audio_stream; + let stream: AudioNode = this._volume_node; for(const f of active_filter) { f.initialize(this._audio_context, stream); stream = f.audio_node; @@ -333,55 +359,115 @@ namespace audio { current_state() : InputState { return this._state; }; - async start() { - this._state = InputState.INITIALIZING; - if(!this._current_device) - return; + private _start_promise: Promise; + async start() : Promise { + if(this._start_promise) { + try { + await this._start_promise; + if(this._state != InputState.PAUSED) + return; + } catch(error) { + console.debug(tr("JavascriptInput:start() Start promise await resulted in an error: %o"), error); + } + } - if(!this._audio_context) - return; + return await (this._start_promise = this._start()); + } + + /* request permission for devices only one per time! */ + private static _running_request: Promise; + static async request_media_stream(device_id: string, group_id: string) : Promise { + while(this._running_request) { + try { + await this._running_request; + } catch(error) { } + } + const promise = (this._running_request = this.request_media_stream0(device_id, group_id)); + try { + return await this._running_request; + } finally { + if(this._running_request === promise) + this._running_request = undefined; + } + } + + static async request_media_stream0(device_id: string, group_id: string) : Promise { + const media_function = getUserMediaFunctionPromise(); + if(!media_function) return InputStartResult.ENOTSUPPORTED; try { - const media_function = getUserMediaFunction(); - if(!media_function) - throw tr("recording isn't supported"); + console.info(tr("Requesting a microphone stream for device %s in group %s"), device_id, group_id); - try { - this._current_stream = await new Promise((resolve, reject) => { - media_function({ - audio: { - deviceId: this._current_device.device_id, - groupId: this._current_device.group_id, + const audio_constrains: MediaTrackConstraints = {}; + audio_constrains.deviceId = device_id; + audio_constrains.groupId = group_id; - echoCancellation: true /* enable by default */ - }, - video: false - }, stream => resolve(stream), error => reject(error)); - }); - } catch(error) { - if(error instanceof DOMException) { - if(error.code == 0 || error.name == "NotAllowedError") { - console.warn(tr("Browser does not allow microphone access")); - this._state = InputState.PAUSED; - createErrorModal(tr("Failed to create microphone"), tr("Microphone recording failed. Please allow TeaWeb access to your microphone")).open(); - return; - } + audio_constrains.echoCancellation = true; + /* may supported */ (audio_constrains as any).autoGainControl = true; + /* may supported */ (audio_constrains as any).noiseSuppression = true; + audio_constrains.sampleSize = {min: 420, max: 960 * 10, ideal: 960}; + + const stream = await media_function({audio: audio_constrains, video: undefined}); + if(!_queried_permissioned) query_devices(); /* we now got permissions, requery devices */ + return stream; + } catch(error) { + if('name' in error) { + if(error.name === "NotAllowedError") { + //createErrorModal(tr("Failed to create microphone"), tr("Microphone recording failed. Please allow TeaWeb access to your microphone")).open(); + //FIXME: Move this to somewhere else! + + console.warn(tr("Microphone request failed (No permissions). Browser message: %o"), error.message); + return InputStartResult.ENOTALLOWED; + } else { + console.warn(tr("Microphone request failed. Request resulted in error: %o: %o"), error.name, error); } + } else { console.warn(tr("Failed to initialize recording stream (%o)"), error); - throw tr("record stream initialisation failed"); } + return InputStartResult.EUNKNOWN; + } + } + + private async _start() : Promise { + try { + if(this._state != InputState.PAUSED) + throw tr("recorder already started"); + + this._state = InputState.INITIALIZING; + if(!this._current_device) + throw tr("invalid device"); + + if(!this._audio_context) + throw tr("missing audio context"); + + const _result = await JavascriptInput.request_media_stream(this._current_device.device_id, this._current_device.group_id); + if(!(_result instanceof MediaStream)) { + this._state = InputState.PAUSED; + return _result; + } + this._current_stream = _result; this._current_audio_stream = this._audio_context.createMediaStreamSource(this._current_stream); - this._initialize_filters(); + this._current_audio_stream.connect(this._volume_node); this._state = InputState.RECORDING; + return InputStartResult.EOK; } catch(error) { - this._state = InputState.PAUSED; + if(this._state == InputState.INITIALIZING) { + this._state = InputState.PAUSED; + } throw error; + } finally { + this._start_promise = undefined; } - return undefined; } async stop() { + /* await all starts */ + try { + if(this._start_promise) + await this._start_promise; + } catch(error) {} + this._state = InputState.PAUSED; if(this._current_audio_stream) this._current_audio_stream.disconnect(); @@ -397,7 +483,6 @@ namespace audio { this._current_stream = undefined; this._current_audio_stream = undefined; - this._initialize_filters(); return undefined; } @@ -420,11 +505,11 @@ namespace audio { this._current_device = device as any; /* TODO: Test for device_id and device_group */ if(!device) { - this._state = InputState.PAUSED; + this._state = saved_state === InputState.PAUSED ? InputState.PAUSED : InputState.DRY; return; } - if(saved_state == InputState.DRY || saved_state == InputState.INITIALIZING || saved_state == InputState.RECORDING) { + if(saved_state !== InputState.PAUSED) { try { await this.start() } catch(error) { @@ -478,20 +563,6 @@ namespace audio { return undefined; } - private previous_filter(type: filter.Type) : filter.JAbstractFilter | undefined { - for(let index = 1; index < this._filters.length; index++) - if(this._filters[index].type === type) - return this._filters.slice(0, index).reverse().find(e => e.is_enabled()) as any; - return undefined; - } - - private next_filter(type: filter.Type) : filter.JAbstractFilter | undefined { - for(let index = 0; index < this._filters.length - 1; index++) - if(this._filters[index].type === type) - return this._filters.slice(index + 1).find(e => e.is_enabled()) as any; - return undefined; - } - clear_filter() { for(const _filter of this._filters) { if(!_filter.is_enabled()) @@ -589,6 +660,146 @@ namespace audio { } this._source_node = new_node; } + + get_volume(): number { + return this._volume; + } + + set_volume(volume: number) { + if(volume === this._volume) + return; + this._volume = volume; + this._volume_node.gain.value = volume; + } + } + + class JavascriptLevelmeter implements LevelMeter { + private static _instances: JavascriptLevelmeter[] = []; + private static _update_task: number; + + readonly _device: JavascriptInputDevice; + + private _callback: (num: number) => any; + + private _context: AudioContext; + private _gain_node: GainNode; + private _source_node: MediaStreamAudioSourceNode; + private _analyser_node: AnalyserNode; + + private _media_stream: MediaStream; + + private _analyse_buffer: Uint8Array; + + private _current_level = 0; + + constructor(device: JavascriptInputDevice) { + this._device = device; + } + + async initialize() { + try { + await new Promise((resolve, reject) => { + const timeout = setTimeout(reject, 5000); + player.on_ready(() => { + clearTimeout(timeout); + resolve(); + }); + }); + } catch(error) { + throw tr("audio context timeout"); + } + this._context = player.context(); + if(!this._context) throw tr("invalid context"); + + this._gain_node = this._context.createGain(); + this._gain_node.gain.setValueAtTime(0, 0); + + /* analyser node */ + this._analyser_node = this._context.createAnalyser(); + + const optimal_ftt_size = Math.ceil(this._context.sampleRate * (filter.JThresholdFilter.update_task_interval / 1000)); + this._analyser_node.fftSize = Math.pow(2, Math.ceil(Math.log2(optimal_ftt_size))); + + if(!this._analyse_buffer || this._analyse_buffer.length < this._analyser_node.fftSize) + this._analyse_buffer = new Uint8Array(this._analyser_node.fftSize); + + /* starting stream */ + const _result = await JavascriptInput.request_media_stream(this._device.device_id, this._device.group_id); + if(!(_result instanceof MediaStream)){ + if(_result === InputStartResult.ENOTALLOWED) + throw tr("No permissions"); + if(_result === InputStartResult.ENOTSUPPORTED) + throw tr("Not supported"); + if(_result === InputStartResult.EBUSY) + throw tr("Device busy"); + if(_result === InputStartResult.EUNKNOWN) + throw tr("an error occurred"); + throw _result; + } + this._media_stream = _result; + + this._source_node = this._context.createMediaStreamSource(this._media_stream); + this._source_node.connect(this._analyser_node); + this._analyser_node.connect(this._gain_node); + this._gain_node.connect(this._context.destination); + + JavascriptLevelmeter._instances.push(this); + if(JavascriptLevelmeter._instances.length == 1) { + clearInterval(JavascriptLevelmeter._update_task); + JavascriptLevelmeter._update_task = setInterval(() => JavascriptLevelmeter._analyse_all(), filter.JThresholdFilter.update_task_interval) as any; + } + } + + destory() { + JavascriptLevelmeter._instances.remove(this); + if(JavascriptLevelmeter._instances.length == 0) { + clearInterval(JavascriptLevelmeter._update_task); + JavascriptLevelmeter._update_task = 0; + } + + if(this._source_node) { + this._source_node.disconnect(); + this._source_node = undefined; + } + if(this._media_stream) { + if(this._media_stream.stop) + this._media_stream.stop(); + else + this._media_stream.getTracks().forEach(value => { + value.stop(); + }); + this._media_stream = undefined; + } + if(this._gain_node) { + this._gain_node.disconnect(); + this._gain_node = undefined; + } + if(this._analyser_node) { + this._analyser_node.disconnect(); + this._analyser_node = undefined; + } + } + + device(): audio.recorder.InputDevice { + return this._device; + } + + set_observer(callback: (value: number) => any) { + this._callback = callback; + } + + private static _analyse_all() { + for(const instance of [...this._instances]) + instance._analyse(); + } + + private _analyse() { + this._analyser_node.getByteTimeDomainData(this._analyse_buffer); + + this._current_level = filter.JThresholdFilter.process(this._analyse_buffer, this._analyser_node.fftSize, this._current_level, .75); + if(this._callback) + this._callback(this._current_level); + } } } } \ No newline at end of file diff --git a/web/js/voice/VoiceHandler.ts b/web/js/voice/VoiceHandler.ts index 1af1de0f..22c28073 100644 --- a/web/js/voice/VoiceHandler.ts +++ b/web/js/voice/VoiceHandler.ts @@ -145,6 +145,23 @@ namespace audio { this.send_task = setInterval(this.send_next_voice_packet.bind(this), 20); /* send all 20ms out voice packets */ } + destroy() { + clearInterval(this.send_task); + this.dropSession(); + this.acquire_voice_recorder(undefined, true).catch(error => { + console.warn(tr("Failed to release voice recorder: %o"), error); + }).then(() => { + for(const client of this._audio_clients) { + client.abort_replay(); + client.callback_playback = undefined; + client.callback_state_changed = undefined; + client.callback_stopped = undefined; + } + this._audio_clients = undefined; + this._audio_source = undefined; + }); + } + native_encoding_supported() : boolean { const context = window.webkitAudioContext || window.AudioContext; if(!context) @@ -515,7 +532,8 @@ namespace audio { private handleVoiceEnded() { const chandler = this.connection.client; - chandler.getClient().speaking = false; + const ch = chandler.getClient(); + if(ch) ch.speaking = false; if(!chandler.connected) return false; @@ -530,7 +548,9 @@ namespace audio { private handleVoiceStarted() { const chandler = this.connection.client; console.log(tr("Local voice started")); - chandler.getClient().speaking = true; + + const ch = chandler.getClient(); + if(ch) ch.speaking = true; } private on_recoder_yield() {