serverless-ircd 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +6 -1
- package/.gitmodules +3 -0
- package/CHANGELOG.md +214 -0
- package/README.md +110 -23
- package/apps/aws-stack/package.json +1 -1
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/origin-allowlist.ts +99 -0
- package/apps/cf-worker/src/worker.ts +89 -9
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
- package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
- package/apps/cf-worker/tests/smoke.test.ts +28 -2
- package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +54 -0
- package/apps/local-cli/package.json +1 -1
- package/apps/web/landing/index.html +217 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +125 -0
- package/apps/web/src/build-env.ts +43 -0
- package/apps/web/src/config-schema.ts +138 -0
- package/apps/web/static/config.json +28 -0
- package/apps/web/static/config.prod.json +28 -0
- package/apps/web/static/config.staging.json +28 -0
- package/apps/web/tests/build-env.test.ts +63 -0
- package/apps/web/tests/build-smoke.test.ts +103 -0
- package/apps/web/tests/config-schema.test.ts +432 -0
- package/apps/web/tests/workspace.test.ts +12 -0
- package/apps/web/tsconfig.json +10 -0
- package/apps/web/upstream/.browserslistrc +3 -0
- package/apps/web/upstream/.editorconfig +9 -0
- package/apps/web/upstream/.eslintignore +3 -0
- package/apps/web/upstream/.eslintrc.js +90 -0
- package/apps/web/upstream/.github/FUNDING.yml +12 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/apps/web/upstream/.github/workflows/push.yml +26 -0
- package/apps/web/upstream/.github/workflows/release.yml +33 -0
- package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
- package/apps/web/upstream/.prettierrc.js +9 -0
- package/apps/web/upstream/.stylelintignore +5 -0
- package/apps/web/upstream/.stylelintrc.js +21 -0
- package/apps/web/upstream/LICENSE +201 -0
- package/apps/web/upstream/README.md +76 -0
- package/apps/web/upstream/babel.config.js +25 -0
- package/apps/web/upstream/crowdin.yml +3 -0
- package/apps/web/upstream/index.html +35 -0
- package/apps/web/upstream/jest.config.js +7 -0
- package/apps/web/upstream/jsdoc.json +22 -0
- package/apps/web/upstream/package.json +80 -0
- package/apps/web/upstream/scripts/pre-commit +43 -0
- package/apps/web/upstream/src/components/App.vue +516 -0
- package/apps/web/upstream/src/components/AppSettings.vue +587 -0
- package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
- package/apps/web/upstream/src/components/Avatar.vue +105 -0
- package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
- package/apps/web/upstream/src/components/BufferKey.vue +121 -0
- package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
- package/apps/web/upstream/src/components/Captcha.vue +85 -0
- package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
- package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
- package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
- package/apps/web/upstream/src/components/ChannelList.vue +345 -0
- package/apps/web/upstream/src/components/Container.vue +401 -0
- package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
- package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
- package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
- package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
- package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
- package/apps/web/upstream/src/components/MessageList.vue +1028 -0
- package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
- package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
- package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
- package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
- package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
- package/apps/web/upstream/src/components/Nicklist.vue +309 -0
- package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
- package/apps/web/upstream/src/components/NotConnected.vue +271 -0
- package/apps/web/upstream/src/components/SelfUser.vue +283 -0
- package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
- package/apps/web/upstream/src/components/ServerView.vue +126 -0
- package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
- package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
- package/apps/web/upstream/src/components/Sidebar.vue +429 -0
- package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
- package/apps/web/upstream/src/components/SidebarState.vue +145 -0
- package/apps/web/upstream/src/components/StartupError.vue +35 -0
- package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
- package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
- package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
- package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
- package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
- package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
- package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
- package/apps/web/upstream/src/components/UserBox.vue +728 -0
- package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
- package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
- package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
- package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
- package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
- package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
- package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
- package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
- package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
- package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
- package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
- package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
- package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
- package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
- package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
- package/apps/web/upstream/src/helpers/Colours.js +128 -0
- package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
- package/apps/web/upstream/src/helpers/Md5.js +193 -0
- package/apps/web/upstream/src/helpers/Misc.js +401 -0
- package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
- package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
- package/apps/web/upstream/src/libs/AudioManager.js +60 -0
- package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
- package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
- package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
- package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
- package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
- package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
- package/apps/web/upstream/src/libs/IPC.js +51 -0
- package/apps/web/upstream/src/libs/InputHandler.js +921 -0
- package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
- package/apps/web/upstream/src/libs/Logger.js +71 -0
- package/apps/web/upstream/src/libs/Message.js +169 -0
- package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
- package/apps/web/upstream/src/libs/MessageParser.js +249 -0
- package/apps/web/upstream/src/libs/Notifications.js +78 -0
- package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
- package/apps/web/upstream/src/libs/ServerSession.js +102 -0
- package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
- package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
- package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
- package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
- package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
- package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
- package/apps/web/upstream/src/libs/bufferTools.js +177 -0
- package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
- package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
- package/apps/web/upstream/src/libs/settingTools.js +31 -0
- package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
- package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
- package/apps/web/upstream/src/libs/state/UserState.js +89 -0
- package/apps/web/upstream/src/libs/state/common.js +26 -0
- package/apps/web/upstream/src/libs/state.js +961 -0
- package/apps/web/upstream/src/libs/storage/Local.js +51 -0
- package/apps/web/upstream/src/main.js +566 -0
- package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
- package/apps/web/upstream/src/res/configTemplates.js +373 -0
- package/apps/web/upstream/src/res/globalStyle.css +277 -0
- package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
- package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
- package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
- package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
- package/apps/web/upstream/src/res/localesList.json +27 -0
- package/apps/web/upstream/src/res/logo.png +0 -0
- package/apps/web/upstream/src/thirdparty/about.html +43 -0
- package/apps/web/upstream/src/thirdparty/index.js +5 -0
- package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
- package/apps/web/upstream/static/config.json +28 -0
- package/apps/web/upstream/static/emoticons/smile.png +0 -0
- package/apps/web/upstream/static/favicon.png +0 -0
- package/apps/web/upstream/static/highlight.mp3 +0 -0
- package/apps/web/upstream/static/highlight.ogg +0 -0
- package/apps/web/upstream/static/locales/.gitignore +2 -0
- package/apps/web/upstream/static/plugins/customise.html.example +10 -0
- package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
- package/apps/web/upstream/static/themes/common/base.css +1064 -0
- package/apps/web/upstream/static/themes/dark/theme.css +196 -0
- package/apps/web/upstream/static/themes/default/theme.css +92 -0
- package/apps/web/upstream/static/themes/elite/theme.css +248 -0
- package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
- package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
- package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
- package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
- package/apps/web/upstream/static/themes/sky/theme.css +107 -0
- package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
- package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
- package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
- package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
- package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
- package/apps/web/upstream/vue.config.js +150 -0
- package/apps/web/upstream/yarn.lock +9767 -0
- package/apps/web/vitest.config.ts +13 -0
- package/biome.json +1 -0
- package/docs/Home.md +1 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/reports/mutation/mutation.html +342 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class SoundBleep {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.audio = document.createElement('audio');
|
|
4
|
+
let source = document.createElement('source');
|
|
5
|
+
|
|
6
|
+
if (this.audio.canPlayType('audio/mpeg;')) {
|
|
7
|
+
source.type = 'audio/mpeg';
|
|
8
|
+
source.src = 'static/highlight.mp3';
|
|
9
|
+
} else {
|
|
10
|
+
source.type = 'audio/ogg';
|
|
11
|
+
source.src = 'static/highlight.ogg';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
this.audio.appendChild(source);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
play() {
|
|
18
|
+
this.audio.play();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'kiwi public';
|
|
2
|
+
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
|
|
5
|
+
export default class StatePersistence {
|
|
6
|
+
constructor(storageKey, state, storage, logger) {
|
|
7
|
+
this.storageKey = storageKey;
|
|
8
|
+
this.state = state;
|
|
9
|
+
this.storage = storage;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
this.isPersisting = false;
|
|
12
|
+
this.includeBuffers = true;
|
|
13
|
+
this.watcher = null;
|
|
14
|
+
|
|
15
|
+
this.state.persistence = this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async loadStateIfExists() {
|
|
19
|
+
if (!this.storageKey) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If we have networks from a previous state, launch directly into it
|
|
24
|
+
let storedState = await this.storage.get(this.storageKey);
|
|
25
|
+
if (storedState) {
|
|
26
|
+
if (this.logger) {
|
|
27
|
+
this.logger('Importing state', storedState);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.state.importState(storedState);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
stopWatchingState() {
|
|
35
|
+
if (this.watcher) {
|
|
36
|
+
this.watcher();
|
|
37
|
+
this.watcher = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
watchStateForChanges() {
|
|
42
|
+
if (!this.storageKey) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Throttle saving the state into storage so we don't thrash the disk
|
|
47
|
+
let debouncedSaveState = _.debounce(() => {
|
|
48
|
+
if (this.logger) {
|
|
49
|
+
this.logger('State updated, setting localStorage');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.storage.set(this.storageKey, this.state.exportState(this.includeBuffers));
|
|
53
|
+
}, 1000);
|
|
54
|
+
|
|
55
|
+
this.state.$watch('networks', debouncedSaveState, { deep: true });
|
|
56
|
+
this.state.$watch('user_settings', debouncedSaveState, { deep: true });
|
|
57
|
+
|
|
58
|
+
// We need to touch each property of each buffer for that we want to save our state on.
|
|
59
|
+
// If any of the properties change then the $watch()er will call debouncedSaveState()
|
|
60
|
+
this.watcher = this.state.$watch(() => {
|
|
61
|
+
let watchVals = [];
|
|
62
|
+
|
|
63
|
+
this.state.networks.forEach((network) => {
|
|
64
|
+
let buffersVals = [];
|
|
65
|
+
buffersVals.push(network.buffers.map((b) => b.name).join(','));
|
|
66
|
+
|
|
67
|
+
network.buffers.forEach((buffer) => {
|
|
68
|
+
buffersVals.push([
|
|
69
|
+
buffer.name,
|
|
70
|
+
buffer.settings,
|
|
71
|
+
buffer.joined,
|
|
72
|
+
buffer.enabled,
|
|
73
|
+
buffer.last_read,
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
watchVals.push(buffersVals);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return JSON.stringify(watchVals);
|
|
81
|
+
}, debouncedSaveState);
|
|
82
|
+
|
|
83
|
+
this.isPersisting = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async forgetState() {
|
|
87
|
+
this.state.resetState();
|
|
88
|
+
await this.storage.set(this.storageKey, null);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'kiwi public';
|
|
2
|
+
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
|
|
5
|
+
let createdInstance = null;
|
|
6
|
+
|
|
7
|
+
export default class ThemeManager {
|
|
8
|
+
constructor(state) {
|
|
9
|
+
this.state = state;
|
|
10
|
+
this.listenForIrcEvents();
|
|
11
|
+
this.varsEl = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
themeVar(varName) {
|
|
15
|
+
if (!this.varsEl) {
|
|
16
|
+
this.varsEl = document.querySelector('.kiwi-wrap');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let styles = window.getComputedStyle(this.varsEl);
|
|
20
|
+
let v = styles.getPropertyValue('--kiwi-' + varName);
|
|
21
|
+
return (v || '').trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
availableThemes() {
|
|
25
|
+
return this.state.settings.themes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
currentTheme() {
|
|
29
|
+
let state = this.state;
|
|
30
|
+
let currentThemeName = state.setting('theme');
|
|
31
|
+
currentThemeName = currentThemeName.toLowerCase();
|
|
32
|
+
|
|
33
|
+
let theme = _.find(state.settings.themes, (t) => {
|
|
34
|
+
let isMatch = t.name.toLowerCase() === currentThemeName;
|
|
35
|
+
return isMatch;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// If no theme was set, use the first one in our theme list
|
|
39
|
+
if (!theme) {
|
|
40
|
+
theme = state.settings.themes[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return theme;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setTheme(theme) {
|
|
47
|
+
let theTheme = null;
|
|
48
|
+
|
|
49
|
+
if (typeof theme === 'string') {
|
|
50
|
+
// Make sure this theme exists
|
|
51
|
+
theTheme = _.find(this.availableThemes(), (t) => {
|
|
52
|
+
let isMatch = t.name.toLowerCase() === theme.toLowerCase();
|
|
53
|
+
return isMatch;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!theTheme) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
theTheme = theme;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.state.setting('theme', theTheme.name);
|
|
64
|
+
this.state.$emit('theme.change');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
reload() {
|
|
68
|
+
let theme = this.currentTheme();
|
|
69
|
+
if (!theme) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let url = theme.url;
|
|
74
|
+
if (url.indexOf('cb=') > -1) {
|
|
75
|
+
url = url.replace(/cb=[0-9]+/, () => 'cb=' + Date.now());
|
|
76
|
+
} else if (url.indexOf('?') > -1) {
|
|
77
|
+
url += '&cb=' + Date.now();
|
|
78
|
+
} else {
|
|
79
|
+
url += '?cb=' + Date.now();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
theme.url = url;
|
|
83
|
+
this.state.$emit('theme.change');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static themeUrl(theme) {
|
|
87
|
+
let parts = theme.url.split('?');
|
|
88
|
+
let url = parts[0];
|
|
89
|
+
let qs = parts[1] || '';
|
|
90
|
+
|
|
91
|
+
if (url[url.length - 1] !== '/') {
|
|
92
|
+
url += '/';
|
|
93
|
+
}
|
|
94
|
+
return url + 'theme.css' + (qs ? '?' + qs : '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setCustomThemeUrl(url) {
|
|
98
|
+
let theme = _.find(ThemeManager.instance().availableThemes(), {
|
|
99
|
+
name: 'custom',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (theme) {
|
|
103
|
+
theme.url = url;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (theme.name === 'custom') {
|
|
107
|
+
this.state.$emit('theme.change');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// When we get a CTCP 'kiwi theme reload' then reload our theme. Handy for theme devs
|
|
112
|
+
listenForIrcEvents() {
|
|
113
|
+
this.state.$on('irc.ctcp request', (event, network) => {
|
|
114
|
+
let ctcpType = (event.type || '').toLowerCase();
|
|
115
|
+
if (ctcpType === 'kiwi' && event.message.indexOf('theme reload') > -1) {
|
|
116
|
+
this.reload();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ThemeManager.instance = function instance(state) {
|
|
123
|
+
if (!createdInstance) {
|
|
124
|
+
createdInstance = new ThemeManager(state);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return createdInstance;
|
|
128
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'kiwi public';
|
|
2
|
+
|
|
3
|
+
/** @module */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds the +typing IRCv3 spec to irc-framework
|
|
7
|
+
*/
|
|
8
|
+
export default function typingMiddleware() {
|
|
9
|
+
return function middleware(client, rawEvents, parsedEvents) {
|
|
10
|
+
addFunctionsToClient(client);
|
|
11
|
+
rawEvents.use(theMiddleware);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function theMiddleware(command, message, rawLine, client, next) {
|
|
15
|
+
if (
|
|
16
|
+
!(command === 'TAGMSG' && message.tags['+typing']) &&
|
|
17
|
+
!(command === 'PRIVMSG' && message.nick)
|
|
18
|
+
) {
|
|
19
|
+
next();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// if we are params[0] then the target is the sender (direct message)
|
|
24
|
+
let target = (message.params[0].toLowerCase() === client.user.nick.toLowerCase()) ?
|
|
25
|
+
message.nick :
|
|
26
|
+
message.params[0];
|
|
27
|
+
|
|
28
|
+
// if its a privmsg without typing tag emit done
|
|
29
|
+
let status = message.tags['+typing'] || 'done';
|
|
30
|
+
|
|
31
|
+
client.emit('typing', {
|
|
32
|
+
target: target,
|
|
33
|
+
nick: message.nick,
|
|
34
|
+
ident: message.ident,
|
|
35
|
+
hostname: message.hostname,
|
|
36
|
+
status: status,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
next();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function addFunctionsToClient(client) {
|
|
44
|
+
let typing = client.typing = {};
|
|
45
|
+
let activeTyping = Object.create(null);
|
|
46
|
+
|
|
47
|
+
function isEnabled() {
|
|
48
|
+
return client.network.cap.isEnabled('message-tags');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
typing.start = function start(target) {
|
|
52
|
+
if (!isEnabled()) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let lastSentStatus = activeTyping[target.toLowerCase()];
|
|
57
|
+
if (lastSentStatus && lastSentStatus > Date.now() - 3000) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
activeTyping[target.toLowerCase()] = Date.now();
|
|
62
|
+
|
|
63
|
+
let message = new client.Message('TAGMSG', target);
|
|
64
|
+
message.tags['+typing'] = 'active';
|
|
65
|
+
client.raw(message);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
typing.pause = function pause(target) {
|
|
69
|
+
if (!isEnabled()) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!activeTyping[target.toLowerCase()]) {
|
|
74
|
+
// Did not start typing so cannot pause
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let message = new client.Message('TAGMSG', target);
|
|
79
|
+
message.tags['+typing'] = 'paused';
|
|
80
|
+
client.raw(message);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
typing.stop = function stop(target, sendStop) {
|
|
84
|
+
if (!isEnabled()) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!activeTyping[target.toLowerCase()]) {
|
|
89
|
+
// Did not start typing so cannot stop
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
delete activeTyping[target.toLowerCase()];
|
|
94
|
+
|
|
95
|
+
if (!sendStop) {
|
|
96
|
+
// Stop was called after a message was sent,
|
|
97
|
+
// only a cleanup of activeTyping is needed
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let message = new client.Message('TAGMSG', target);
|
|
102
|
+
message.tags['+typing'] = 'done';
|
|
103
|
+
client.raw(message);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export default class WindowTitle {
|
|
2
|
+
constructor(state) {
|
|
3
|
+
this.state = state;
|
|
4
|
+
this.title = state.settings.windowTitle || '';
|
|
5
|
+
this.alert = false;
|
|
6
|
+
|
|
7
|
+
// Update the window title if we have one
|
|
8
|
+
if (state.settings.windowTitle) {
|
|
9
|
+
this.updateTitle();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
state.$watch('settings.windowTitle', (newVal) => {
|
|
13
|
+
this.updateTitle(newVal);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
state.$watch('ui.app_has_focus', (newVal) => {
|
|
17
|
+
if (newVal && this.alertTmr) {
|
|
18
|
+
this.stopAlert();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
state.$on('notification.title', (enable) => {
|
|
23
|
+
if (enable) {
|
|
24
|
+
this.startAlert();
|
|
25
|
+
} else {
|
|
26
|
+
this.stopAlert();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
startAlert() {
|
|
32
|
+
if (this.alertTmr) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.alertTmr = setInterval(() => {
|
|
37
|
+
this.updateTitle();
|
|
38
|
+
}, 1000);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
stopAlert() {
|
|
42
|
+
if (this.alertTmr) {
|
|
43
|
+
clearInterval(this.alertTmr);
|
|
44
|
+
this.alertTmr = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.updateTitle();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updateTitle(newTitle) {
|
|
51
|
+
if (typeof newTitle === 'string') {
|
|
52
|
+
this.title = newTitle;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (this.alertTmr && !this.alert) {
|
|
56
|
+
window.document.title = '* ' + this.title;
|
|
57
|
+
this.alert = true;
|
|
58
|
+
} else {
|
|
59
|
+
window.document.title = this.title;
|
|
60
|
+
this.alert = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'kiwi public';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* batchedAdd prevents a flood of new inserts into state. Based on JS ticks, inserts /sec are
|
|
5
|
+
* counted by each JS tick that inserts an item. Eg. 10 inserts in 1 tick = 1 count. 10 inserts
|
|
6
|
+
* on different JS ticks = 10 counts.
|
|
7
|
+
* After X counts/sec, batch each second worth of new items at the same time.
|
|
8
|
+
*/
|
|
9
|
+
export default function batchedAdd(singleFn, batchedFn, numInsertsSec = 3) {
|
|
10
|
+
let inTick = false;
|
|
11
|
+
let queue = [];
|
|
12
|
+
let numInLastSec = 0;
|
|
13
|
+
let queueLoopTmr = null;
|
|
14
|
+
let loopInterval = 1000;
|
|
15
|
+
let checkSecRateTmr = null;
|
|
16
|
+
|
|
17
|
+
function queueLoop() {
|
|
18
|
+
numInLastSec = 0;
|
|
19
|
+
if (queue.length) {
|
|
20
|
+
// emptying queue before calling batchedFn in case that function triggers
|
|
21
|
+
// code that needs to see that the queue has been processed.
|
|
22
|
+
let q = queue;
|
|
23
|
+
queue = [];
|
|
24
|
+
batchedFn(q);
|
|
25
|
+
queueLoopTmr = setTimeout(queueLoop, loopInterval);
|
|
26
|
+
} else {
|
|
27
|
+
queueLoopTmr = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function maybeStartLoop() {
|
|
32
|
+
if (!queueLoopTmr) {
|
|
33
|
+
queueLoopTmr = setTimeout(queueLoop, loopInterval);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Reset numInLastSec after loopInterval. This allows enough time for the counter to
|
|
38
|
+
// increase to detect batching. Only needs to run if we are not currently batching and
|
|
39
|
+
// only needs to run once at a time.
|
|
40
|
+
function resetAddCounter() {
|
|
41
|
+
if (!queueLoopTmr && !checkSecRateTmr) {
|
|
42
|
+
checkSecRateTmr = setTimeout(() => {
|
|
43
|
+
checkSecRateTmr = null;
|
|
44
|
+
if (!queueLoopTmr) {
|
|
45
|
+
numInLastSec = 0;
|
|
46
|
+
}
|
|
47
|
+
}, loopInterval);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function batchFn(item) {
|
|
52
|
+
if (!inTick) {
|
|
53
|
+
numInLastSec++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// If already queuing or we reached our limit on items/sec, queue the item
|
|
57
|
+
if (queue.length || numInLastSec > numInsertsSec) {
|
|
58
|
+
queue.push(item);
|
|
59
|
+
maybeStartLoop();
|
|
60
|
+
} else {
|
|
61
|
+
inTick = true;
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
inTick = false;
|
|
64
|
+
});
|
|
65
|
+
singleFn(item);
|
|
66
|
+
resetAddCounter();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
batchFn.queue = function getQueue() {
|
|
70
|
+
return queue;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return batchFn;
|
|
74
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'kiwi public';
|
|
2
|
+
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import getState from '@/libs/state';
|
|
5
|
+
|
|
6
|
+
export function orderBuffers(buffers) {
|
|
7
|
+
// Since vuejs will sort in-place and update views when .sort is called
|
|
8
|
+
// on an array, clone it first so that we have a plain array to sort
|
|
9
|
+
let list = buffers.map((b) => b);
|
|
10
|
+
|
|
11
|
+
list = _.filter(list, (buffer) => !buffer.isServer());
|
|
12
|
+
list = list.sort((a, b) => {
|
|
13
|
+
let order = 0;
|
|
14
|
+
if (a.isChannel() && b.isQuery()) {
|
|
15
|
+
order = -1;
|
|
16
|
+
} else if (a.isQuery() && b.isChannel()) {
|
|
17
|
+
order = 1;
|
|
18
|
+
} else {
|
|
19
|
+
order = a.name.localeCompare(b.name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return order;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return list;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function orderedMessages(buffer, opts = {}) {
|
|
29
|
+
let state = getState();
|
|
30
|
+
let network = buffer.getNetwork();
|
|
31
|
+
let currentNick = network.nick;
|
|
32
|
+
let bufferMessages = buffer.getMessages();
|
|
33
|
+
|
|
34
|
+
// Hack; We need to make vue aware that we depend on buffer.message_count in order to
|
|
35
|
+
// get the messagelist to update its DOM, as the change of message_count alerts
|
|
36
|
+
// us that the messages have changed. This is done so that vue does not have to make
|
|
37
|
+
// every emssage reactive which gets very expensive.
|
|
38
|
+
/* eslint-disable no-unused-vars */
|
|
39
|
+
let ignoredVar = buffer.message_count;
|
|
40
|
+
|
|
41
|
+
let messages = [];
|
|
42
|
+
if (opts.inPlace) {
|
|
43
|
+
messages = bufferMessages;
|
|
44
|
+
} else {
|
|
45
|
+
messages = bufferMessages.slice(0, bufferMessages.length);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
messages.sort((a, b) => {
|
|
49
|
+
if (a.time > b.time) {
|
|
50
|
+
return 1;
|
|
51
|
+
} else if (b.time > a.time) {
|
|
52
|
+
return -1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return a.instance_num > b.instance_num ?
|
|
56
|
+
1 :
|
|
57
|
+
-1;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (opts.noFilter) {
|
|
61
|
+
return messages;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let messageTypesToShowSetting = {
|
|
65
|
+
// message.type: 'settings_name'
|
|
66
|
+
traffic: 'show_joinparts',
|
|
67
|
+
topic: 'show_topics',
|
|
68
|
+
nick: 'show_nick_changes',
|
|
69
|
+
mode: 'show_mode_changes',
|
|
70
|
+
presence: 'show_presence_changes',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let hiddenMessageTypes = Object.keys(messageTypesToShowSetting)
|
|
74
|
+
.filter((type) => !buffer.setting(messageTypesToShowSetting[type]));
|
|
75
|
+
|
|
76
|
+
let typeExtraIncludesSelf = (message) => message.type_extra &&
|
|
77
|
+
message.type_extra.endsWith('_self');
|
|
78
|
+
|
|
79
|
+
let settingShowTopicInHeader = buffer.setting('show_topic_in_header');
|
|
80
|
+
|
|
81
|
+
let list = [];
|
|
82
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
83
|
+
// don't include hidden message types
|
|
84
|
+
if (hiddenMessageTypes.includes(messages[i].type) && !typeExtraIncludesSelf(messages[i])) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Ignored users have the ignore flag set
|
|
89
|
+
if (messages[i].ignore) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Don't show the first connection message. Channels are only interested in
|
|
94
|
+
// the joining message at first. Dis/connection messages are only relevant here
|
|
95
|
+
// if the dis/connection happens between messages (during a conversation)
|
|
96
|
+
if (messages[i].type === 'connection' && i === 0) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
messages[i].type === 'topic' &&
|
|
102
|
+
messages[i].type_extra === 'topic_join' &&
|
|
103
|
+
!state.ui.is_narrow &&
|
|
104
|
+
settingShowTopicInHeader
|
|
105
|
+
) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// When we join a channel the topic is usually sent next. But this looks
|
|
110
|
+
// ugly when rendered. So we switch the topic + join messages around so
|
|
111
|
+
// that the topic is first in the message list.
|
|
112
|
+
if (
|
|
113
|
+
messages[i].type === 'topic' &&
|
|
114
|
+
messages[i - 1] &&
|
|
115
|
+
messages[i - 1].type === 'traffic' &&
|
|
116
|
+
messages[i - 1].nick === currentNick
|
|
117
|
+
) {
|
|
118
|
+
list.push(messages[i - 1]);
|
|
119
|
+
list.push(messages[i]);
|
|
120
|
+
i--;
|
|
121
|
+
} else {
|
|
122
|
+
list.push(messages[i]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return list.reverse();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getNextBuffer() {
|
|
130
|
+
return getBufferFromDirection(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getPreviousBuffer() {
|
|
134
|
+
return getBufferFromDirection(-1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getBufferFromDirection(direction) {
|
|
138
|
+
let state = getState();
|
|
139
|
+
let network = state.getActiveNetwork();
|
|
140
|
+
let buffer = state.getActiveBuffer();
|
|
141
|
+
|
|
142
|
+
if (!network || !buffer) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let ordered = orderBuffers(network.buffers);
|
|
147
|
+
let index = _.findIndex(ordered, ['name', buffer.name]) + direction;
|
|
148
|
+
|
|
149
|
+
if (index >= ordered.length || index < 0) {
|
|
150
|
+
network = getNetworkFromDirection(direction);
|
|
151
|
+
ordered = orderBuffers(network.buffers);
|
|
152
|
+
buffer = (direction === 1) ? ordered[0] : ordered[ordered.length - 1];
|
|
153
|
+
} else {
|
|
154
|
+
buffer = ordered[index];
|
|
155
|
+
}
|
|
156
|
+
return buffer;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getNetworkFromDirection(direction) {
|
|
160
|
+
let state = getState();
|
|
161
|
+
let network = state.getActiveNetwork();
|
|
162
|
+
for (let i = 0; i < state.networks.length; i++) {
|
|
163
|
+
let index = _.findIndex(state.networks, ['id', network.id]) + direction;
|
|
164
|
+
|
|
165
|
+
if (index >= state.networks.length && state.networks.length >= 0) {
|
|
166
|
+
network = state.networks[0];
|
|
167
|
+
} else if (index < 0) {
|
|
168
|
+
network = state.networks[state.networks.length - 1];
|
|
169
|
+
} else {
|
|
170
|
+
network = state.networks[index];
|
|
171
|
+
}
|
|
172
|
+
if (network.buffers.length > 1) {
|
|
173
|
+
return network;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return network;
|
|
177
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// From the Element.closest mdn page.
|
|
2
|
+
if (!Element.prototype.matches) {
|
|
3
|
+
Element.prototype.matches = Element.prototype.msMatchesSelector ||
|
|
4
|
+
Element.prototype.webkitMatchesSelector;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (!Element.prototype.closest) {
|
|
8
|
+
Element.prototype.closest = function closest(s) {
|
|
9
|
+
let el = this;
|
|
10
|
+
do {
|
|
11
|
+
if (el.matches(s)) {
|
|
12
|
+
return el;
|
|
13
|
+
}
|
|
14
|
+
el = el.parentElement || el.parentNode;
|
|
15
|
+
} while (el !== null && el.nodeType === 1);
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
}
|