From 454318403fa5109558a8f4472e91a33a75e21e22 Mon Sep 17 00:00:00 2001 From: dreammu <84692291+dreammu@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:31:10 +0800 Subject: [PATCH 01/30] Update url params clean: `is_room_feed` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 似乎是从用户空间进入直播间会带的参数 --- registry/lib/components/utils/url-params-clean/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/registry/lib/components/utils/url-params-clean/index.ts b/registry/lib/components/utils/url-params-clean/index.ts index 50d6d6586..144064f64 100644 --- a/registry/lib/components/utils/url-params-clean/index.ts +++ b/registry/lib/components/utils/url-params-clean/index.ts @@ -74,6 +74,10 @@ const entry = async () => { match: /\/\/live\.bilibili\.com\//, param: 'session_id', }, + { + match: /\/\/live\.bilibili\.com\//, + param: 'is_room_feed', + }, { match: /\/\/www\.bilibili\.com\/bangumi\//, param: 'theme', From 4430a80e81bee48b61ad0170ea85c221bede3596 Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 1 Sep 2024 16:04:35 +0800 Subject: [PATCH 02/30] Refactor Shadow DOM APIs --- .../style/simplify/comments/index.ts | 4 +- src/core/core-apis.ts | 6 +- src/core/shadow-dom.ts | 192 ------------------ src/core/shadow-root/dom-entry.ts | 77 +++++++ src/core/shadow-root/dom-observer.ts | 121 +++++++++++ src/core/shadow-root/index.ts | 3 + src/core/shadow-root/root-observer.ts | 36 ++++ src/core/shadow-root/styles.ts | 52 +++++ src/core/shadow-root/types.ts | 5 + 9 files changed, 299 insertions(+), 197 deletions(-) delete mode 100644 src/core/shadow-dom.ts create mode 100644 src/core/shadow-root/dom-entry.ts create mode 100644 src/core/shadow-root/dom-observer.ts create mode 100644 src/core/shadow-root/index.ts create mode 100644 src/core/shadow-root/root-observer.ts create mode 100644 src/core/shadow-root/styles.ts create mode 100644 src/core/shadow-root/types.ts diff --git a/registry/lib/components/style/simplify/comments/index.ts b/registry/lib/components/style/simplify/comments/index.ts index 8efcf7e19..002971a25 100644 --- a/registry/lib/components/style/simplify/comments/index.ts +++ b/registry/lib/components/style/simplify/comments/index.ts @@ -51,9 +51,9 @@ export const component = wrapSwitchOptions({ true, ) - const { ShadowDomStyles } = await import('@/core/shadow-dom') + const { ShadowRootStyles } = await import('@/core/shadow-root') const v3Styles = await import('./comments-v3.scss').then(m => m.default) - const shadowDom = new ShadowDomStyles() + const shadowDom = new ShadowRootStyles() shadowDom.addStyle(v3Styles) }, instantStyles: [ diff --git a/src/core/core-apis.ts b/src/core/core-apis.ts index aa400dfd1..2e1fea54d 100644 --- a/src/core/core-apis.ts +++ b/src/core/core-apis.ts @@ -17,7 +17,7 @@ import * as spinQuery from '@/core/spin-query' import * as style from '@/core/style' import * as textColor from '@/core/text-color' import * as settings from '@/core/settings' -import * as shadowDom from '@/core/shadow-dom' +import * as shadowRoot from '@/core/shadow-root' import * as userInfo from '@/core/user-info' import * as version from '@/core/version' import * as commonUtils from '@/core/utils' @@ -60,7 +60,7 @@ export const coreApis = { userInfo, version, settings, - shadowDom, + shadowRoot, toast, themeColor, utils: { @@ -101,7 +101,7 @@ export const externalApis = { ...textColor, ...userInfo, ...version, - ...shadowDom, + ...shadowRoot, settingsApis: settings, get settings() { return settings.settings diff --git a/src/core/shadow-dom.ts b/src/core/shadow-dom.ts deleted file mode 100644 index 98c788d0f..000000000 --- a/src/core/shadow-dom.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { allMutations, childListSubtree } from './observer' -import { addStyle } from './style' -import { deleteValue, getRandomId } from './utils' - -enum ShadowRootEvents { - Added = 'shadowRootAdded', - Removed = 'shadowRootRemoved', - Updated = 'shadowRootUpdated', -} -interface ShadowRootEntry { - shadowRoot: ShadowRoot - observer: MutationObserver -} -interface ShadowRootStyleEntry { - shadowRoot: ShadowRoot -} -export class ShadowDomObserver extends EventTarget { - static enforceOpenRoot() { - const originalAttachShadow = Element.prototype.attachShadow - Element.prototype.attachShadow = function attachShadow(options: ShadowRootInit) { - return originalAttachShadow.call(this, { - ...options, - mode: 'open', - }) - } - } - - private observing = false - - entries: ShadowRootEntry[] = [] - - constructor() { - super() - } - - protected queryAllShadowRoots( - root: DocumentFragment | Element = document.body, - deep = false, - ): ShadowRoot[] { - return [root, ...root.querySelectorAll('*')] - .filter((e): e is Element => e instanceof Element && e.shadowRoot !== null) - .flatMap(e => { - if (deep) { - return [e.shadowRoot, ...this.queryAllShadowRoots(e.shadowRoot)] - } - return [e.shadowRoot] - }) - } - - protected mutationHandler(records: MutationRecord[]) { - records.forEach(record => { - record.removedNodes.forEach(node => { - if (node instanceof Element && node.shadowRoot !== null) { - this.removeEntry(node.shadowRoot) - } - }) - record.addedNodes.forEach(node => { - if (node instanceof Element) { - this.queryAllShadowRoots(node).forEach(shadowRoot => { - this.addEntry(shadowRoot) - }) - } - }) - }) - } - - protected addEntry(shadowRoot: ShadowRoot) { - if (this.shadowRoots.includes(shadowRoot)) { - return - } - const shadowRootChildren = this.queryAllShadowRoots(shadowRoot) - shadowRootChildren.forEach(child => this.addEntry(child)) - - const [observer] = childListSubtree(shadowRoot, records => this.mutationHandler(records)) - this.entries.push({ - shadowRoot, - observer, - }) - this.dispatchEvent(new CustomEvent('shadowRootAdded', { detail: shadowRoot })) - } - - protected removeEntry(shadowRoot: ShadowRoot) { - const children = this.shadowRoots.filter(it => shadowRoot.contains(it.host)) - children.forEach(child => this.removeEntry(child)) - - const entry = this.entries.find(it => it.shadowRoot === shadowRoot) - if (entry !== undefined) { - entry.observer.disconnect() - deleteValue(this.entries, it => it === entry) - } - } - - get shadowRoots() { - return this.entries.map(entry => entry.shadowRoot) - } - - addEventListener( - type: `${ShadowRootEvents}`, - callback: EventListenerOrEventListenerObject | null, - options?: AddEventListenerOptions | boolean, - ): void { - super.addEventListener(type, callback, options) - } - - removeEventListener( - type: `${ShadowRootEvents}`, - callback: EventListenerOrEventListenerObject | null, - options?: EventListenerOptions | boolean, - ): void { - super.removeEventListener(type, callback, options) - } - - forEachShadowRoot(callbacks: { - added?: (shadowRoot: ShadowRoot) => void - removed?: (shadowRoot: ShadowRoot) => void - }) { - this.shadowRoots.forEach(it => callbacks.added?.(it)) - const addedListener = (e: CustomEvent) => callbacks?.added(e.detail) - const removedListener = (e: CustomEvent) => callbacks?.removed(e.detail) - this.addEventListener(ShadowRootEvents.Added, addedListener) - this.addEventListener(ShadowRootEvents.Removed, removedListener) - return () => { - this.removeEventListener(ShadowRootEvents.Added, addedListener) - this.removeEventListener(ShadowRootEvents.Removed, removedListener) - } - } - - observe() { - if (this.observing) { - return - } - const existingRoots = this.queryAllShadowRoots() - existingRoots.forEach(root => this.addEntry(root)) - allMutations(records => this.mutationHandler(records)) - this.observing = true - } - - disconnect() { - this.entries.forEach(entry => entry.observer.disconnect()) - this.entries = [] - this.observing = false - } -} - -const shadowDomObserver = new ShadowDomObserver() - -export class ShadowDomStyles { - observer: ShadowDomObserver = shadowDomObserver - entries: ShadowRootStyleEntry[] = [] - - get shadowRoots() { - return this.entries.map(entry => entry.shadowRoot) - } - - protected addEntry(shadowRoot: ShadowRoot) { - this.entries.push({ - shadowRoot, - }) - } - - protected removeEntry(shadowRoot: ShadowRoot) { - const entry = this.entries.find(it => it.shadowRoot === shadowRoot) - if (entry !== undefined) { - deleteValue(this.entries, it => it === entry) - } - } - - addStyle(text: string) { - this.observer.observe() - const id = `shadow-dom-style-${getRandomId()}` - const element = addStyle(text, id) - const destroy = this.observer.forEachShadowRoot({ - added: async shadowRoot => { - if (this.shadowRoots.includes(shadowRoot)) { - return - } - this.addEntry(shadowRoot) - const sheet = new CSSStyleSheet() - await sheet.replace(element.innerHTML) - shadowRoot.adoptedStyleSheets.push(sheet) - }, - removed: shadowRoot => { - shadowRoot.getElementById(id)?.remove() - this.removeEntry(shadowRoot) - }, - }) - return () => { - destroy() - element.remove() - } - } -} diff --git a/src/core/shadow-root/dom-entry.ts b/src/core/shadow-root/dom-entry.ts new file mode 100644 index 000000000..ceb39a657 --- /dev/null +++ b/src/core/shadow-root/dom-entry.ts @@ -0,0 +1,77 @@ +import { childListSubtree } from '../observer' +import { deleteValue } from '../utils' +import type { ShadowDomObserver } from './dom-observer' +import { ShadowRootObserver } from './root-observer' +import { ShadowRootEvents } from './types' + +export type ShadowDomParent = ShadowDomEntry | ShadowDomObserver +export type ShadowDomCallback = (shadowDom: ShadowDomEntry) => void +export class ShadowDomEntry extends ShadowRootObserver { + readonly shadowRoot: ShadowRoot + readonly elementName: string + readonly parent: ShadowDomParent | null = null + readonly children: ShadowDomEntry[] = [] + protected observer: MutationObserver + protected callbacksMap = new Map() + + constructor(shadowRoot: ShadowRoot, parent: ShadowDomParent) { + super() + this.shadowRoot = shadowRoot + this.parent = parent + this.elementName = shadowRoot.host.tagName.toLowerCase() + ShadowRootObserver.queryAllShadowRoots(shadowRoot).map(it => this.addChild(it)) + this.observe() + } + + override dispatchEvent(event: Event): boolean { + return super.dispatchEvent(event) && this.parent.dispatchEvent(event) + } + + protected mutationHandler(records: MutationRecord[]) { + records.forEach(record => { + record.removedNodes.forEach(node => { + if (node instanceof Element) { + ShadowRootObserver.queryAllShadowRoots(node).forEach(shadowRoot => { + const child = this.children.find(it => it.shadowRoot === shadowRoot) + if (child === undefined) { + return + } + this.removeChild(child) + }) + } + }) + record.addedNodes.forEach(node => { + if (node instanceof Element) { + ShadowRootObserver.queryAllShadowRoots(node).forEach(shadowRoot => { + this.addChild(shadowRoot) + }) + } + }) + }) + } + + addChild(childShadowRoot: ShadowRoot) { + const child = new ShadowDomEntry(childShadowRoot, this) + this.children.push(child) + this.dispatchEvent(new CustomEvent(ShadowRootEvents.Added, { detail: child })) + return child + } + + removeChild(child: ShadowDomEntry) { + child.disconnect() + deleteValue(this.children, it => it === child) + this.dispatchEvent(new CustomEvent(ShadowRootEvents.Removed, { detail: child })) + } + + observe() { + const [observer] = childListSubtree(this.shadowRoot, records => { + this.mutationHandler(records) + }) + this.observer = observer + } + + disconnect() { + this.children.forEach(child => this.removeChild(child)) + this.observer?.disconnect() + } +} diff --git a/src/core/shadow-root/dom-observer.ts b/src/core/shadow-root/dom-observer.ts new file mode 100644 index 000000000..f149e6736 --- /dev/null +++ b/src/core/shadow-root/dom-observer.ts @@ -0,0 +1,121 @@ +import { childListSubtree } from '../observer' +import { deleteValue } from '../utils' +import { ShadowDomCallback, ShadowDomEntry } from './dom-entry' +import { ShadowRootObserver } from './root-observer' +import { ShadowRootEvents } from './types' + +export class ShadowDomObserver extends ShadowRootObserver { + static enforceOpenRoot() { + const originalAttachShadow = Element.prototype.attachShadow + Element.prototype.attachShadow = function attachShadow(options: ShadowRootInit) { + return originalAttachShadow.call(this, { + ...options, + mode: 'open', + }) + } + } + + private observing = false + + entries: ShadowDomEntry[] = [] + + constructor() { + super() + } + + protected mutationHandler(records: MutationRecord[]) { + records.forEach(record => { + record.removedNodes.forEach(node => { + if (node instanceof Element) { + ShadowRootObserver.queryAllShadowRoots(node).forEach(shadowRoot => { + this.removeEntryByShadowRoot(shadowRoot) + }) + } + }) + record.addedNodes.forEach(node => { + if (node instanceof Element) { + ShadowRootObserver.queryAllShadowRoots(node).forEach(shadowRoot => { + this.addEntry(shadowRoot) + }) + } + }) + }) + } + + protected addEntry(shadowRoot: ShadowRoot) { + const match = this.entries.find(e => e.shadowRoot === shadowRoot) + if (match !== undefined) { + return match + } + + const shadowDomEntry = new ShadowDomEntry(shadowRoot, this) + shadowDomEntry.observe() + this.entries.push(shadowDomEntry) + this.dispatchEvent(new CustomEvent(ShadowRootEvents.Added, { detail: shadowDomEntry })) + + return shadowDomEntry + } + + protected removeEntry(entry: ShadowDomEntry) { + const match = this.entries.find(it => it === entry) + if (match === undefined) { + return + } + + match.children.forEach(child => this.removeEntry(child)) + match.disconnect() + deleteValue(this.entries, it => it === match) + this.dispatchEvent(new CustomEvent(ShadowRootEvents.Removed, { detail: match })) + } + + protected removeEntryByShadowRoot(shadowRoot: ShadowRoot) { + const match = this.entries.find(it => it.shadowRoot === shadowRoot) + if (match === undefined) { + return + } + this.removeEntry(match) + } + + forEachShadowDom(callback: ShadowDomCallback) { + const callCurrentAndNextLevel = (currentEntry: ShadowDomEntry) => { + callback(currentEntry) + currentEntry.children.forEach(child => { + callCurrentAndNextLevel(child) + }) + } + + this.entries.forEach(entry => { + callCurrentAndNextLevel(entry) + }) + } + + watchShadowDom(callbacks: { added?: ShadowDomCallback; removed?: ShadowDomCallback }) { + this.forEachShadowDom(it => callbacks.added?.(it)) + const addedListener = (e: CustomEvent) => callbacks?.added(e.detail) + const removedListener = (e: CustomEvent) => callbacks?.removed(e.detail) + this.addEventListener(ShadowRootEvents.Added, addedListener) + this.addEventListener(ShadowRootEvents.Removed, removedListener) + return () => { + this.removeEventListener(ShadowRootEvents.Added, addedListener) + this.removeEventListener(ShadowRootEvents.Removed, removedListener) + } + } + + observe() { + if (this.observing) { + return + } + const existingRoots = ShadowRootObserver.queryAllShadowRoots() + existingRoots.forEach(root => this.addEntry(root)) + childListSubtree(document.body, records => this.mutationHandler(records)) + this.observing = true + } + + disconnect() { + this.entries.forEach(entry => entry.disconnect()) + this.entries = [] + this.observing = false + } +} + +export const shadowDomObserver = new ShadowDomObserver() diff --git a/src/core/shadow-root/index.ts b/src/core/shadow-root/index.ts new file mode 100644 index 000000000..6e25d7470 --- /dev/null +++ b/src/core/shadow-root/index.ts @@ -0,0 +1,3 @@ +export * from './dom-observer' +export * from './styles' +export * from './types' diff --git a/src/core/shadow-root/root-observer.ts b/src/core/shadow-root/root-observer.ts new file mode 100644 index 000000000..e5173b8f4 --- /dev/null +++ b/src/core/shadow-root/root-observer.ts @@ -0,0 +1,36 @@ +import { ShadowRootEvents } from './types' + +export abstract class ShadowRootObserver extends EventTarget { + static queryAllShadowRoots( + root: DocumentFragment | Element = document.body, + deep = false, + ): ShadowRoot[] { + return [root, ...root.querySelectorAll('*')] + .filter((e): e is Element => e instanceof Element && e.shadowRoot !== null) + .flatMap(e => { + if (deep) { + return [e.shadowRoot, ...ShadowRootObserver.queryAllShadowRoots(e.shadowRoot)] + } + return [e.shadowRoot] + }) + } + + addEventListener( + type: `${ShadowRootEvents}`, + callback: EventListenerOrEventListenerObject | null, + options?: AddEventListenerOptions | boolean, + ): void { + super.addEventListener(type, callback, options) + } + + removeEventListener( + type: `${ShadowRootEvents}`, + callback: EventListenerOrEventListenerObject | null, + options?: EventListenerOptions | boolean, + ): void { + super.removeEventListener(type, callback, options) + } + + abstract observe(): void + abstract disconnect(): void +} diff --git a/src/core/shadow-root/styles.ts b/src/core/shadow-root/styles.ts new file mode 100644 index 000000000..5feebcec8 --- /dev/null +++ b/src/core/shadow-root/styles.ts @@ -0,0 +1,52 @@ +import { addStyle } from '../style' +import { deleteValue, getRandomId } from '../utils' +import { ShadowDomObserver, shadowDomObserver } from './dom-observer' + +interface ShadowRootStyleEntry { + shadowRoot: ShadowRoot +} +export class ShadowRootStyles { + observer: ShadowDomObserver = shadowDomObserver + entries: ShadowRootStyleEntry[] = [] + + get shadowRoots() { + return this.entries.map(entry => entry.shadowRoot) + } + + protected addEntry(shadowRoot: ShadowRoot) { + this.entries.push({ + shadowRoot, + }) + } + + protected removeEntry(shadowRoot: ShadowRoot) { + const entry = this.entries.find(it => it.shadowRoot === shadowRoot) + if (entry !== undefined) { + deleteValue(this.entries, it => it === entry) + } + } + + addStyle(text: string) { + this.observer.observe() + const id = `shadow-dom-style-${getRandomId()}` + const element = addStyle(text, id) + const destroy = this.observer.watchShadowDom({ + added: async shadowDom => { + if (this.shadowRoots.includes(shadowDom.shadowRoot)) { + return + } + this.addEntry(shadowDom.shadowRoot) + const sheet = new CSSStyleSheet() + await sheet.replace(element.innerHTML) + shadowDom.shadowRoot.adoptedStyleSheets.push(sheet) + }, + removed: shadowDom => { + this.removeEntry(shadowDom.shadowRoot) + }, + }) + return () => { + destroy() + element.remove() + } + } +} diff --git a/src/core/shadow-root/types.ts b/src/core/shadow-root/types.ts new file mode 100644 index 000000000..93d22c238 --- /dev/null +++ b/src/core/shadow-root/types.ts @@ -0,0 +1,5 @@ +export enum ShadowRootEvents { + Added = 'shadowRootAdded', + Removed = 'shadowRootRemoved', + Updated = 'shadowRootUpdated', +} From 964b68ce98a3938672968b2ef4864356ffb1f710 Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 1 Sep 2024 20:58:07 +0800 Subject: [PATCH 03/30] Fix undefined reference --- src/core/shadow-root/dom-observer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/shadow-root/dom-observer.ts b/src/core/shadow-root/dom-observer.ts index f149e6736..e34728b2e 100644 --- a/src/core/shadow-root/dom-observer.ts +++ b/src/core/shadow-root/dom-observer.ts @@ -91,8 +91,8 @@ export class ShadowDomObserver extends ShadowRootObserver { watchShadowDom(callbacks: { added?: ShadowDomCallback; removed?: ShadowDomCallback }) { this.forEachShadowDom(it => callbacks.added?.(it)) - const addedListener = (e: CustomEvent) => callbacks?.added(e.detail) - const removedListener = (e: CustomEvent) => callbacks?.removed(e.detail) + const addedListener = (e: CustomEvent) => callbacks?.added?.(e.detail) + const removedListener = (e: CustomEvent) => callbacks?.removed?.(e.detail) this.addEventListener(ShadowRootEvents.Added, addedListener) this.addEventListener(ShadowRootEvents.Removed, removedListener) return () => { From d19ce3d7423e7b6c779b289aaed59398896730ff Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 1 Sep 2024 20:59:04 +0800 Subject: [PATCH 04/30] Use adoptedStyleSheets for document --- src/core/shadow-root/styles.ts | 43 +++++++++++----------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/core/shadow-root/styles.ts b/src/core/shadow-root/styles.ts index 5feebcec8..2ded6336c 100644 --- a/src/core/shadow-root/styles.ts +++ b/src/core/shadow-root/styles.ts @@ -1,52 +1,37 @@ -import { addStyle } from '../style' import { deleteValue, getRandomId } from '../utils' import { ShadowDomObserver, shadowDomObserver } from './dom-observer' -interface ShadowRootStyleEntry { - shadowRoot: ShadowRoot -} export class ShadowRootStyles { observer: ShadowDomObserver = shadowDomObserver - entries: ShadowRootStyleEntry[] = [] - - get shadowRoots() { - return this.entries.map(entry => entry.shadowRoot) - } + protected stylesMap = new Map() - protected addEntry(shadowRoot: ShadowRoot) { - this.entries.push({ - shadowRoot, - }) + protected addStyleRecord(id: string, style: CSSStyleSheet) { + this.stylesMap.set(id, style) } - protected removeEntry(shadowRoot: ShadowRoot) { - const entry = this.entries.find(it => it.shadowRoot === shadowRoot) - if (entry !== undefined) { - deleteValue(this.entries, it => it === entry) + protected removeStyleRecord(id: string) { + if (this.stylesMap.has(id)) { + this.stylesMap.delete(id) } } - addStyle(text: string) { + async addStyle(text: string) { this.observer.observe() const id = `shadow-dom-style-${getRandomId()}` - const element = addStyle(text, id) + const sheet = new CSSStyleSheet() + await sheet.replace(text) + document.adoptedStyleSheets.push(sheet) + this.addStyleRecord(id, sheet) + const destroy = this.observer.watchShadowDom({ added: async shadowDom => { - if (this.shadowRoots.includes(shadowDom.shadowRoot)) { - return - } - this.addEntry(shadowDom.shadowRoot) - const sheet = new CSSStyleSheet() - await sheet.replace(element.innerHTML) shadowDom.shadowRoot.adoptedStyleSheets.push(sheet) }, - removed: shadowDom => { - this.removeEntry(shadowDom.shadowRoot) - }, }) return () => { destroy() - element.remove() + deleteValue(document.adoptedStyleSheets, it => it === sheet) + this.removeStyleRecord(id) } } } From 5daf767966bbab8e86fcbafd5604a19638573d3e Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 1 Sep 2024 21:05:09 +0800 Subject: [PATCH 05/30] Add ShadowRootStyleEntry --- src/core/shadow-root/styles.ts | 37 ++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/core/shadow-root/styles.ts b/src/core/shadow-root/styles.ts index 2ded6336c..31ce8f752 100644 --- a/src/core/shadow-root/styles.ts +++ b/src/core/shadow-root/styles.ts @@ -1,15 +1,20 @@ import { deleteValue, getRandomId } from '../utils' import { ShadowDomObserver, shadowDomObserver } from './dom-observer' +interface ShadowRootStyleEntry { + id: string + styleSheet: CSSStyleSheet + remove: () => void +} export class ShadowRootStyles { observer: ShadowDomObserver = shadowDomObserver - protected stylesMap = new Map() + protected stylesMap = new Map() - protected addStyleRecord(id: string, style: CSSStyleSheet) { - this.stylesMap.set(id, style) + protected addEntry(id: string, entry: ShadowRootStyleEntry) { + this.stylesMap.set(id, entry) } - protected removeStyleRecord(id: string) { + protected removeEntry(id: string) { if (this.stylesMap.has(id)) { this.stylesMap.delete(id) } @@ -18,20 +23,26 @@ export class ShadowRootStyles { async addStyle(text: string) { this.observer.observe() const id = `shadow-dom-style-${getRandomId()}` - const sheet = new CSSStyleSheet() - await sheet.replace(text) - document.adoptedStyleSheets.push(sheet) - this.addStyleRecord(id, sheet) + const styleSheet = new CSSStyleSheet() + await styleSheet.replace(text) + document.adoptedStyleSheets.push(styleSheet) const destroy = this.observer.watchShadowDom({ added: async shadowDom => { - shadowDom.shadowRoot.adoptedStyleSheets.push(sheet) + shadowDom.shadowRoot.adoptedStyleSheets.push(styleSheet) }, }) - return () => { - destroy() - deleteValue(document.adoptedStyleSheets, it => it === sheet) - this.removeStyleRecord(id) + + const entry: ShadowRootStyleEntry = { + id, + styleSheet, + remove: () => { + destroy() + deleteValue(document.adoptedStyleSheets, it => it === styleSheet) + this.removeEntry(id) + }, } + this.addEntry(id, entry) + return entry } } From 2f4e5d963dcc06cb596dfadb3875a6eb716e17d1 Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 1 Sep 2024 21:18:09 +0800 Subject: [PATCH 06/30] Add DomCommentArea --- src/components/utils/comment/areas/base.ts | 88 +------------------ src/components/utils/comment/areas/dom.ts | 87 ++++++++++++++++++ src/components/utils/comment/areas/v1.ts | 4 +- src/components/utils/comment/areas/v2.ts | 4 +- src/components/utils/comment/areas/v3.ts | 0 .../utils/comment/comment-area-manager.ts | 4 +- 6 files changed, 96 insertions(+), 91 deletions(-) create mode 100644 src/components/utils/comment/areas/dom.ts create mode 100644 src/components/utils/comment/areas/v3.ts diff --git a/src/components/utils/comment/areas/base.ts b/src/components/utils/comment/areas/base.ts index 1535690d1..d1203f728 100644 --- a/src/components/utils/comment/areas/base.ts +++ b/src/components/utils/comment/areas/base.ts @@ -1,27 +1,18 @@ -import { childListSubtree } from '@/core/observer' import type { CommentItem } from '../comment-item' -import type { CommentCallbackInput, CommentCallbackPair, CommentItemCallback } from '../types' import type { CommentReplyItem } from '../reply-item' -import { getRandomId } from '@/core/utils' +import type { CommentCallbackInput, CommentCallbackPair, CommentItemCallback } from '../types' -/** 表示一个评论区 */ export abstract class CommentArea { - /** 对应元素 */ element: HTMLElement - /** 评论列表 */ items: CommentItem[] = [] - /** 与之关联的 MutationObserver */ - protected observer?: MutationObserver protected itemCallbacks: CommentCallbackPair[] = [] - protected static replyItemClasses = ['list-item.reply-wrap', 'reply-item'] - protected static replyItemSelector = CommentArea.replyItemClasses.map(c => `.${c}`).join(',') constructor(element: HTMLElement) { this.element = element } - abstract parseCommentItem(element: HTMLElement): CommentItem - abstract getCommentId(element: HTMLElement): string + abstract observe(): void + abstract disconnect(): void abstract addMenuItem( item: CommentReplyItem, config: { @@ -31,79 +22,6 @@ export abstract class CommentArea { }, ): void - protected isCommentItem(n: Node): n is HTMLElement { - return n instanceof HTMLElement && n.matches(CommentArea.replyItemSelector) - } - - /** 在每一轮 CommentItem 解析前调用 */ - protected beforeParse(elements: HTMLElement[]) { - return lodash.noop(elements) - } - - observeItems() { - if (this.observer) { - return - } - performance.mark('observeItems start') - const elements = dqa(this.element, CommentArea.replyItemSelector) as HTMLElement[] - if (elements.length > 0) { - this.beforeParse(elements) - this.items = elements.map(it => this.parseCommentItem(it as HTMLElement)) - } - this.items.forEach(item => { - this.itemCallbacks.forEach(c => c.added?.(item)) - }) - ;[this.observer] = childListSubtree(this.element, records => { - const observerCallId = getRandomId() - performance.mark(`observeItems subtree start ${observerCallId}`) - const addedCommentElements: HTMLElement[] = [] - const removedCommentElements: HTMLElement[] = [] - records.forEach(r => { - r.addedNodes.forEach(n => { - if (this.isCommentItem(n)) { - addedCommentElements.push(n) - } - }) - r.removedNodes.forEach(n => { - if (this.isCommentItem(n)) { - removedCommentElements.push(n) - } - }) - }) - if (addedCommentElements.length > 0) { - this.beforeParse(addedCommentElements) - } - addedCommentElements.forEach(n => { - const commentItem = this.parseCommentItem(n) - this.items.push(commentItem) - this.itemCallbacks.forEach(c => c.added?.(commentItem)) - }) - removedCommentElements.forEach(n => { - const id = this.getCommentId(n) - const index = this.items.findIndex(item => item.id === id) - if (index !== -1) { - const [commentItem] = this.items.splice(index, 1) - this.itemCallbacks.forEach(c => c.removed?.(commentItem)) - } - }) - performance.mark(`observeItems subtree end ${observerCallId}`) - performance.measure( - `observeItems subtree ${observerCallId}`, - `observeItems subtree start ${observerCallId}`, - `observeItems subtree end ${observerCallId}`, - ) - }) - performance.mark('observeItems end') - performance.measure('observeItems', 'observeItems start', 'observeItems end') - } - - destroy() { - this.observer?.disconnect() - this.items.forEach(item => { - this.itemCallbacks.forEach(pair => pair.removed?.(item)) - }) - } - static resolveCallbackPair void>( input: CommentCallbackInput, ): CommentCallbackPair { diff --git a/src/components/utils/comment/areas/dom.ts b/src/components/utils/comment/areas/dom.ts new file mode 100644 index 000000000..53bef42f3 --- /dev/null +++ b/src/components/utils/comment/areas/dom.ts @@ -0,0 +1,87 @@ +import { CommentArea } from './base' +import { childListSubtree } from '@/core/observer' +import type { CommentItem } from '../comment-item' +import { getRandomId } from '@/core/utils' + +/** 表示一个基于常规 DOM 结构的评论区 */ +export abstract class DomCommentArea extends CommentArea { + protected observer?: MutationObserver + protected static replyItemClasses = ['list-item.reply-wrap', 'reply-item'] + protected static replyItemSelector = DomCommentArea.replyItemClasses.map(c => `.${c}`).join(',') + + abstract parseCommentItem(element: HTMLElement): CommentItem + abstract getCommentId(element: HTMLElement): string + + protected isCommentItem(n: Node): n is HTMLElement { + return n instanceof HTMLElement && n.matches(DomCommentArea.replyItemSelector) + } + + /** 在每一轮 CommentItem 解析前调用 */ + protected beforeParse(elements: HTMLElement[]) { + return lodash.noop(elements) + } + + observe() { + if (this.observer) { + return + } + performance.mark('observeItems start') + const elements = dqa(this.element, DomCommentArea.replyItemSelector) as HTMLElement[] + if (elements.length > 0) { + this.beforeParse(elements) + this.items = elements.map(it => this.parseCommentItem(it as HTMLElement)) + } + this.items.forEach(item => { + this.itemCallbacks.forEach(c => c.added?.(item)) + }) + ;[this.observer] = childListSubtree(this.element, records => { + const observerCallId = getRandomId() + performance.mark(`observeItems subtree start ${observerCallId}`) + const addedCommentElements: HTMLElement[] = [] + const removedCommentElements: HTMLElement[] = [] + records.forEach(r => { + r.addedNodes.forEach(n => { + if (this.isCommentItem(n)) { + addedCommentElements.push(n) + } + }) + r.removedNodes.forEach(n => { + if (this.isCommentItem(n)) { + removedCommentElements.push(n) + } + }) + }) + if (addedCommentElements.length > 0) { + this.beforeParse(addedCommentElements) + } + addedCommentElements.forEach(n => { + const commentItem = this.parseCommentItem(n) + this.items.push(commentItem) + this.itemCallbacks.forEach(c => c.added?.(commentItem)) + }) + removedCommentElements.forEach(n => { + const id = this.getCommentId(n) + const index = this.items.findIndex(item => item.id === id) + if (index !== -1) { + const [commentItem] = this.items.splice(index, 1) + this.itemCallbacks.forEach(c => c.removed?.(commentItem)) + } + }) + performance.mark(`observeItems subtree end ${observerCallId}`) + performance.measure( + `observeItems subtree ${observerCallId}`, + `observeItems subtree start ${observerCallId}`, + `observeItems subtree end ${observerCallId}`, + ) + }) + performance.mark('observeItems end') + performance.measure('observeItems', 'observeItems start', 'observeItems end') + } + + disconnect() { + this.observer?.disconnect() + this.items.forEach(item => { + this.itemCallbacks.forEach(pair => pair.removed?.(item)) + }) + } +} diff --git a/src/components/utils/comment/areas/v1.ts b/src/components/utils/comment/areas/v1.ts index 60090ece7..da4894812 100644 --- a/src/components/utils/comment/areas/v1.ts +++ b/src/components/utils/comment/areas/v1.ts @@ -1,9 +1,9 @@ import { childList } from '@/core/observer' import { CommentItem } from '../comment-item' import { CommentReplyItem } from '../reply-item' -import { CommentArea } from './base' +import { DomCommentArea } from './dom' -export class CommentAreaV1 extends CommentArea { +export class CommentAreaV1 extends DomCommentArea { addMenuItem( item: CommentReplyItem, config: { className: string; text: string; action: (e: MouseEvent) => void }, diff --git a/src/components/utils/comment/areas/v2.ts b/src/components/utils/comment/areas/v2.ts index 7f1bc2f39..9c393b30a 100644 --- a/src/components/utils/comment/areas/v2.ts +++ b/src/components/utils/comment/areas/v2.ts @@ -1,11 +1,11 @@ /* eslint-disable no-underscore-dangle */ import { childList } from '@/core/observer' import { CommentItem } from '../comment-item' -import { CommentArea } from './base' +import { DomCommentArea } from './dom' import { CommentReplyItem } from '../reply-item' import { HTMLElementWithVue, VNodeManager } from '../vnode-manager' -export class CommentAreaV2 extends CommentArea { +export class CommentAreaV2 extends DomCommentArea { private vnodeManager: VNodeManager constructor(element: HTMLElement) { diff --git a/src/components/utils/comment/areas/v3.ts b/src/components/utils/comment/areas/v3.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/utils/comment/comment-area-manager.ts b/src/components/utils/comment/comment-area-manager.ts index 0873659fc..831b2c4ed 100644 --- a/src/components/utils/comment/comment-area-manager.ts +++ b/src/components/utils/comment/comment-area-manager.ts @@ -34,7 +34,7 @@ export class CommentAreaManager { ) { const area = getCommentArea(node) this.commentAreas.push(area) - area.observeItems() + area.observe() this.commentAreaCallbacks.forEach(c => c.added?.(area)) const [observer] = childList(area.element.parentElement, records => { records.forEach(r => { @@ -42,7 +42,7 @@ export class CommentAreaManager { if (removedNode === area.element) { deleteValue(this.commentAreas, a => a === area) this.commentAreaCallbacks.forEach(c => c.removed?.(area)) - area.destroy() + area.disconnect() observer.disconnect() } }) From 6f04f26d3390e2511c1fee658b7275bcd35c4d31 Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 1 Sep 2024 23:51:56 +0800 Subject: [PATCH 07/30] Add CommentAreaV3 --- .../lib/components/utils/ip-show/index.ts | 2 +- src/components/utils/comment/areas/v1.ts | 4 +- src/components/utils/comment/areas/v2.ts | 4 +- src/components/utils/comment/areas/v3.ts | 166 ++++++++++++++++++ .../utils/comment/comment-area-manager.ts | 11 +- src/components/utils/comment/comment-area.ts | 4 + src/components/utils/comment/reply-item.ts | 6 +- src/core/shadow-root/dom-entry.ts | 73 +++++++- 8 files changed, 251 insertions(+), 19 deletions(-) diff --git a/registry/lib/components/utils/ip-show/index.ts b/registry/lib/components/utils/ip-show/index.ts index d08e9f33e..d00ccb1aa 100644 --- a/registry/lib/components/utils/ip-show/index.ts +++ b/registry/lib/components/utils/ip-show/index.ts @@ -5,7 +5,7 @@ import { CommentItem, CommentReplyItem } from '@/components/utils/comment-apis' // 新版评论区IP属地获取 const getIpLocation = (item: CommentReplyItem) => { - const reply = item.vueProps + const reply = item.frameworkSpecificProps return reply?.reply_control?.location ?? undefined } diff --git a/src/components/utils/comment/areas/v1.ts b/src/components/utils/comment/areas/v1.ts index da4894812..bd1b395c4 100644 --- a/src/components/utils/comment/areas/v1.ts +++ b/src/components/utils/comment/areas/v1.ts @@ -44,7 +44,7 @@ export class CommentAreaV1 extends DomCommentArea { content: replyElement.querySelector('.text-con').textContent, timeText: replyElement.querySelector('.info .time, .info .time-location').textContent, likes: parseInt(replyElement.querySelector('.info .like span').textContent), - vueProps: undefined, + frameworkSpecificProps: undefined, }) } const item = new CommentItem({ @@ -56,7 +56,7 @@ export class CommentAreaV1 extends DomCommentArea { timeText: element.querySelector('.con .info .time, .info .time-location').textContent, likes: parseInt(element.querySelector('.con .like span').textContent), replies: [], - vueProps: undefined, + frameworkSpecificProps: undefined, }) if (dq(element, '.reply-box .view-more')) { const replyBox = dq(element, '.reply-box') as HTMLElement diff --git a/src/components/utils/comment/areas/v2.ts b/src/components/utils/comment/areas/v2.ts index 9c393b30a..ef3b7990a 100644 --- a/src/components/utils/comment/areas/v2.ts +++ b/src/components/utils/comment/areas/v2.ts @@ -94,7 +94,7 @@ export class CommentAreaV2 extends DomCommentArea { content: r.content.message, time: r.ctime * 1000, likes: r.like, - vueProps: r, + frameworkSpecificProps: r, }) }) } @@ -110,7 +110,7 @@ export class CommentAreaV2 extends DomCommentArea { return img.img_src }), replies: parseReplies(), - vueProps, + frameworkSpecificProps: vueProps, }) if (item.replies.length < vueProps.rcount) { const replyBox = dq(element, '.sub-reply-list') diff --git a/src/components/utils/comment/areas/v3.ts b/src/components/utils/comment/areas/v3.ts index e69de29bb..5777b3bc2 100644 --- a/src/components/utils/comment/areas/v3.ts +++ b/src/components/utils/comment/areas/v3.ts @@ -0,0 +1,166 @@ +import { ShadowDomObserver, shadowDomObserver, ShadowRootEvents } from '@/core/shadow-root' +import { CommentReplyItem } from '../reply-item' +import { CommentArea } from './base' +import { ShadowDomEntry } from '@/core/shadow-root/dom-entry' +import { CommentItem } from '../comment-item' +import { deleteValue } from '@/core/utils' + +export class CommentAreaV3 extends CommentArea { + protected static commentItemSelectors = 'bili-comment-thread-renderer' + protected static commentReplyItemSelectors = 'bili-comment-reply-renderer' + protected static getLitData(entry: ShadowDomEntry) { + const host = entry.element as HTMLElement & { __data: any } + // eslint-disable-next-line no-underscore-dangle + return host.__data + } + + static isV3Area(element: HTMLElement) { + return element.tagName.toLowerCase() === 'bili-comments' + } + + protected shadowDomObserver: ShadowDomObserver + protected commentAreaEntry: ShadowDomEntry + protected commentItemEntries: ShadowDomEntry[] = [] + + private handleEntryAdded: (e: CustomEvent) => void + private handleEntryRemoved: (e: CustomEvent) => void + private areaObserverDisposer: () => void + + constructor(element: HTMLElement) { + super(element) + shadowDomObserver.observe() + this.shadowDomObserver = shadowDomObserver + } + + protected getReplyItemElement(parent: ShadowDomEntry, replyId: string): HTMLElement { + const replies = parent.querySelectorAllAsEntry(CommentAreaV3.commentReplyItemSelectors) + return replies.find(r => CommentAreaV3.getLitData(r).rpid_str === replyId) + ?.element as HTMLElement + } + + protected parseCommentItem(entry: ShadowDomEntry): CommentItem { + const litData = CommentAreaV3.getLitData(entry) + const parseReplies = () => { + if (!litData.replies) { + return [] + } + return (litData.replies as any[]) + .map((r): CommentReplyItem | null => { + const element = this.getReplyItemElement(entry, r.rpid_str) + if (!element) { + return null + } + return new CommentReplyItem({ + id: r.rpid_str, + element, + userId: r.member.mid, + userName: r.member.uname, + content: r.content.message, + time: r.ctime * 1000, + likes: r.like, + frameworkSpecificProps: r, + }) + }) + .filter((r): r is CommentReplyItem => r !== null) + } + const item = new CommentItem({ + id: litData.rpid_str, + element: entry.element as HTMLElement, + userId: litData.member.mid, + userName: litData.member.uname, + content: litData.content.message, + time: litData.ctime * 1000, + likes: litData.like, + pictures: litData.content?.pictures?.map((img: { img_src: string }) => { + return img.img_src + }), + replies: parseReplies(), + frameworkSpecificProps: litData, + }) + if (item.replies.length < litData.rcount) { + const handler = (e: CustomEvent) => { + const replyEntry = e.detail + if (!replyEntry.element.matches(CommentAreaV3.commentReplyItemSelectors)) { + return + } + item.replies = parseReplies() + item.dispatchRepliesUpdate(item.replies) + if (item.replies.length >= litData.rcount) { + entry.removeEventListener(ShadowRootEvents.Added, handler) + } + } + entry.addEventListener(ShadowRootEvents.Added, handler) + } + return item + } + + protected addCommentItem(entry: ShadowDomEntry) { + if (this.commentItemEntries.includes(entry)) { + return + } + this.commentItemEntries.push(entry) + const commentItem = this.parseCommentItem(entry) + this.items.push(commentItem) + this.itemCallbacks.forEach(c => c.added?.(commentItem)) + } + + protected removeCommentItem(entry: ShadowDomEntry) { + const itemToRemove = this.items.find(it => it.element === entry.element) + deleteValue(this.commentItemEntries, it => it === entry) + deleteValue(this.items, it => it === itemToRemove) + this.itemCallbacks.forEach(c => c.removed?.(itemToRemove)) + } + + protected observeCommentItems() { + const entries = this.commentAreaEntry.querySelectorAllAsEntry( + CommentAreaV3.commentItemSelectors, + ) + this.commentItemEntries = entries + this.items = entries.map(it => this.parseCommentItem(it)) + this.items.forEach(item => { + this.itemCallbacks.forEach(c => c.added?.(item)) + }) + + this.handleEntryAdded = (e: CustomEvent) => { + const entry = e.detail + if (entry.element.matches(CommentAreaV3.commentItemSelectors)) { + this.addCommentItem(entry) + } + } + this.commentAreaEntry.addEventListener(ShadowRootEvents.Added, this.handleEntryAdded) + this.handleEntryRemoved = (e: CustomEvent) => { + const entry = e.detail + const match = this.commentItemEntries.find(it => it === entry) + if (match === undefined) { + return + } + this.removeCommentItem(match) + } + this.commentAreaEntry.addEventListener(ShadowRootEvents.Removed, this.handleEntryRemoved) + } + + observe() { + this.areaObserverDisposer = this.shadowDomObserver.watchShadowDom({ + added: shadowDom => { + if (shadowDom.element !== this.element) { + return + } + this.commentAreaEntry = shadowDom + this.observeCommentItems() + }, + }) + } + + disconnect() { + this.commentAreaEntry.removeEventListener(ShadowRootEvents.Added, this.handleEntryAdded) + this.commentAreaEntry.removeEventListener(ShadowRootEvents.Removed, this.handleEntryRemoved) + this.areaObserverDisposer?.() + } + + addMenuItem( + item: CommentReplyItem, + config: { className: string; text: string; action: (e: MouseEvent) => void }, + ) { + console.warn('Method not implemented.') + } +} diff --git a/src/components/utils/comment/comment-area-manager.ts b/src/components/utils/comment/comment-area-manager.ts index 831b2c4ed..6a8b86175 100644 --- a/src/components/utils/comment/comment-area-manager.ts +++ b/src/components/utils/comment/comment-area-manager.ts @@ -14,7 +14,7 @@ export class CommentAreaManager { commentAreas: CommentArea[] = [] commentAreaCallbacks: CommentCallbackPair[] = [] - protected static commentAreaClasses = ['bili-comment', 'bb-comment'] + protected static commentAreaSelectors = '.bili-comment, .bb-comment, bili-comments' init() { allMutations(records => { @@ -22,16 +22,11 @@ export class CommentAreaManager { r.addedNodes.forEach(n => this.observeAreas(n)) }) }) - dqa(CommentAreaManager.commentAreaClasses.map(c => `.${c}`).join(',')).forEach(it => - this.observeAreas(it), - ) + dqa(CommentAreaManager.commentAreaSelectors).forEach(it => this.observeAreas(it)) } observeAreas(node: Node) { - if ( - node instanceof HTMLElement && - CommentAreaManager.commentAreaClasses.some(c => node.classList.contains(c)) - ) { + if (node instanceof HTMLElement && node.matches(CommentAreaManager.commentAreaSelectors)) { const area = getCommentArea(node) this.commentAreas.push(area) area.observe() diff --git a/src/components/utils/comment/comment-area.ts b/src/components/utils/comment/comment-area.ts index 9bcf1deee..26be5071f 100644 --- a/src/components/utils/comment/comment-area.ts +++ b/src/components/utils/comment/comment-area.ts @@ -1,8 +1,12 @@ import type { CommentArea } from './areas/base' import { CommentAreaV1 } from './areas/v1' import { CommentAreaV2 } from './areas/v2' +import { CommentAreaV3 } from './areas/v3' export const getCommentArea = (element: HTMLElement): CommentArea => { + if (CommentAreaV3.isV3Area(element)) { + return new CommentAreaV3(element) + } if (CommentAreaV2.isV2Area(element)) { return new CommentAreaV2(element) } diff --git a/src/components/utils/comment/reply-item.ts b/src/components/utils/comment/reply-item.ts index c15bbc241..df2a80784 100644 --- a/src/components/utils/comment/reply-item.ts +++ b/src/components/utils/comment/reply-item.ts @@ -16,8 +16,8 @@ export class CommentReplyItem extends EventTarget { time?: number /** 点赞数 */ likes: number - /** 对应的 Vue Props */ - vueProps: any + /** 对应的框架特定 Props (可能是 Vue 或者 Lit) */ + frameworkSpecificProps: any constructor(initParams: Omit) { super() @@ -29,6 +29,6 @@ export class CommentReplyItem extends EventTarget { this.timeText = initParams.timeText this.time = initParams.time this.likes = initParams.likes - this.vueProps = initParams.vueProps + this.frameworkSpecificProps = initParams.frameworkSpecificProps } } diff --git a/src/core/shadow-root/dom-entry.ts b/src/core/shadow-root/dom-entry.ts index ceb39a657..eec2e734b 100644 --- a/src/core/shadow-root/dom-entry.ts +++ b/src/core/shadow-root/dom-entry.ts @@ -4,11 +4,11 @@ import type { ShadowDomObserver } from './dom-observer' import { ShadowRootObserver } from './root-observer' import { ShadowRootEvents } from './types' +export const ShadowDomEntrySymbol = Symbol.for('ShadowDomEntry') export type ShadowDomParent = ShadowDomEntry | ShadowDomObserver export type ShadowDomCallback = (shadowDom: ShadowDomEntry) => void export class ShadowDomEntry extends ShadowRootObserver { readonly shadowRoot: ShadowRoot - readonly elementName: string readonly parent: ShadowDomParent | null = null readonly children: ShadowDomEntry[] = [] protected observer: MutationObserver @@ -18,11 +18,21 @@ export class ShadowDomEntry extends ShadowRootObserver { super() this.shadowRoot = shadowRoot this.parent = parent - this.elementName = shadowRoot.host.tagName.toLowerCase() - ShadowRootObserver.queryAllShadowRoots(shadowRoot).map(it => this.addChild(it)) + this.element[ShadowDomEntrySymbol] = this + ShadowRootObserver.queryAllShadowRoots(shadowRoot) + .filter(it => it !== shadowRoot) + .map(it => this.addChild(it)) this.observe() } + get element() { + return this.shadowRoot.host + } + + get elementName() { + return this.element.tagName.toLowerCase() + } + override dispatchEvent(event: Event): boolean { return super.dispatchEvent(event) && this.parent.dispatchEvent(event) } @@ -51,6 +61,10 @@ export class ShadowDomEntry extends ShadowRootObserver { } addChild(childShadowRoot: ShadowRoot) { + const match = this.children.find(child => child.shadowRoot === childShadowRoot) + if (match) { + return match + } const child = new ShadowDomEntry(childShadowRoot, this) this.children.push(child) this.dispatchEvent(new CustomEvent(ShadowRootEvents.Added, { detail: child })) @@ -63,6 +77,59 @@ export class ShadowDomEntry extends ShadowRootObserver { this.dispatchEvent(new CustomEvent(ShadowRootEvents.Removed, { detail: child })) } + protected queryThroughChildren(predicate: (current: ShadowDomEntry) => T | null) { + const currentLevelResult = predicate(this) + if (currentLevelResult) { + return { + entry: this, + result: currentLevelResult, + } + } + for (const child of this.children) { + const childResult = predicate(child) + if (childResult !== null) { + return { + entry: child, + result: childResult, + } + } + } + return { + entry: null, + result: null, + } + } + + querySelectorAsEntry(selectors: string): ShadowDomEntry | null { + const { entry } = this.queryThroughChildren(current => { + if (current === this) { + return null + } + return current.element.matches(selectors) + }) + return entry + } + + querySelectorAllAsEntry(selectors: string): ShadowDomEntry[] { + const currentMatch = this.children.filter(child => child.element.matches(selectors)) + const childrenMatch = this.children.flatMap(child => child.querySelectorAllAsEntry(selectors)) + return [...currentMatch, ...childrenMatch] + } + + querySelector(selectors: string): Element | null { + const { result } = this.queryThroughChildren(current => + current.shadowRoot.querySelector(selectors), + ) + return result + } + + querySelectorAll(selectors: string): Element[] { + return [ + ...this.shadowRoot.querySelectorAll(selectors), + ...this.children.flatMap(child => child.querySelectorAll(selectors)), + ] + } + observe() { const [observer] = childListSubtree(this.shadowRoot, records => { this.mutationHandler(records) From fdf475f1edeaefd8f20a79bce4d043d67114043a Mon Sep 17 00:00:00 2001 From: the1812 Date: Mon, 2 Sep 2024 23:36:22 +0800 Subject: [PATCH 08/30] Add addMenuItem support --- src/components/utils/comment/areas/v3.ts | 61 +++++++++++++++++------- src/core/shadow-root/dom-entry.ts | 25 +++++----- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/components/utils/comment/areas/v3.ts b/src/components/utils/comment/areas/v3.ts index 5777b3bc2..71889c7cc 100644 --- a/src/components/utils/comment/areas/v3.ts +++ b/src/components/utils/comment/areas/v3.ts @@ -4,10 +4,13 @@ import { CommentArea } from './base' import { ShadowDomEntry } from '@/core/shadow-root/dom-entry' import { CommentItem } from '../comment-item' import { deleteValue } from '@/core/utils' +import { select } from '@/core/spin-query' export class CommentAreaV3 extends CommentArea { protected static commentItemSelectors = 'bili-comment-thread-renderer' protected static commentReplyItemSelectors = 'bili-comment-reply-renderer' + protected static commentActionsSelectors = 'bili-comment-action-buttons-renderer' + protected static commentMenuSelectors = 'bili-comment-menu' protected static getLitData(entry: ShadowDomEntry) { const host = entry.element as HTMLElement & { __data: any } // eslint-disable-next-line no-underscore-dangle @@ -20,7 +23,7 @@ export class CommentAreaV3 extends CommentArea { protected shadowDomObserver: ShadowDomObserver protected commentAreaEntry: ShadowDomEntry - protected commentItemEntries: ShadowDomEntry[] = [] + protected itemEntryMap = new Map() private handleEntryAdded: (e: CustomEvent) => void private handleEntryRemoved: (e: CustomEvent) => void @@ -32,9 +35,17 @@ export class CommentAreaV3 extends CommentArea { this.shadowDomObserver = shadowDomObserver } + protected matchChildEntryByReplyId( + parent: ShadowDomEntry, + childSelectors: string, + replyId: string, + ) { + const children = parent.querySelectorAllAsEntry(childSelectors) + return children.find(r => CommentAreaV3.getLitData(r).rpid_str === replyId) + } + protected getReplyItemElement(parent: ShadowDomEntry, replyId: string): HTMLElement { - const replies = parent.querySelectorAllAsEntry(CommentAreaV3.commentReplyItemSelectors) - return replies.find(r => CommentAreaV3.getLitData(r).rpid_str === replyId) + return this.matchChildEntryByReplyId(parent, CommentAreaV3.commentReplyItemSelectors, replyId) ?.element as HTMLElement } @@ -95,18 +106,18 @@ export class CommentAreaV3 extends CommentArea { } protected addCommentItem(entry: ShadowDomEntry) { - if (this.commentItemEntries.includes(entry)) { + if (this.itemEntryMap.has(entry)) { return } - this.commentItemEntries.push(entry) const commentItem = this.parseCommentItem(entry) + this.itemEntryMap.set(entry, commentItem) this.items.push(commentItem) this.itemCallbacks.forEach(c => c.added?.(commentItem)) } protected removeCommentItem(entry: ShadowDomEntry) { const itemToRemove = this.items.find(it => it.element === entry.element) - deleteValue(this.commentItemEntries, it => it === entry) + this.itemEntryMap.delete(entry) deleteValue(this.items, it => it === itemToRemove) this.itemCallbacks.forEach(c => c.removed?.(itemToRemove)) } @@ -115,11 +126,7 @@ export class CommentAreaV3 extends CommentArea { const entries = this.commentAreaEntry.querySelectorAllAsEntry( CommentAreaV3.commentItemSelectors, ) - this.commentItemEntries = entries - this.items = entries.map(it => this.parseCommentItem(it)) - this.items.forEach(item => { - this.itemCallbacks.forEach(c => c.added?.(item)) - }) + entries.forEach(entry => this.addCommentItem(entry)) this.handleEntryAdded = (e: CustomEvent) => { const entry = e.detail @@ -130,11 +137,10 @@ export class CommentAreaV3 extends CommentArea { this.commentAreaEntry.addEventListener(ShadowRootEvents.Added, this.handleEntryAdded) this.handleEntryRemoved = (e: CustomEvent) => { const entry = e.detail - const match = this.commentItemEntries.find(it => it === entry) - if (match === undefined) { + if (!this.itemEntryMap.has(entry)) { return } - this.removeCommentItem(match) + this.removeCommentItem(entry) } this.commentAreaEntry.addEventListener(ShadowRootEvents.Removed, this.handleEntryRemoved) } @@ -157,10 +163,33 @@ export class CommentAreaV3 extends CommentArea { this.areaObserverDisposer?.() } - addMenuItem( + async addMenuItem( item: CommentReplyItem, config: { className: string; text: string; action: (e: MouseEvent) => void }, ) { - console.warn('Method not implemented.') + const itemEntry = [...this.itemEntryMap.entries()].find( + ([, savedItem]) => savedItem === item, + )?.[0] + if (!itemEntry) { + return + } + + const actions = await select(() => + this.matchChildEntryByReplyId(itemEntry, CommentAreaV3.commentActionsSelectors, item.id), + ) + if (!actions) { + return + } + + const menu = actions.querySelectorAsEntry(CommentAreaV3.commentMenuSelectors) + const list = menu?.querySelector('#options') + const listItem = document.createElement('li') + listItem.innerHTML = config.text + listItem.className = config.className + listItem.addEventListener('click', e => { + config.action(e) + ;(menu.element as HTMLElement).style.setProperty('--bili-comment-menu-display', null) + }) + list?.appendChild(listItem) } } diff --git a/src/core/shadow-root/dom-entry.ts b/src/core/shadow-root/dom-entry.ts index eec2e734b..de5fd9622 100644 --- a/src/core/shadow-root/dom-entry.ts +++ b/src/core/shadow-root/dom-entry.ts @@ -77,21 +77,21 @@ export class ShadowDomEntry extends ShadowRootObserver { this.dispatchEvent(new CustomEvent(ShadowRootEvents.Removed, { detail: child })) } - protected queryThroughChildren(predicate: (current: ShadowDomEntry) => T | null) { - const currentLevelResult = predicate(this) - if (currentLevelResult) { + protected queryThroughChildren(predicate: (current: ShadowDomEntry) => T | null): { + entry: ShadowDomEntry | null + result: T | null + } { + const selfResult = predicate(this) + if (selfResult !== null) { return { entry: this, - result: currentLevelResult, + result: selfResult, } } for (const child of this.children) { - const childResult = predicate(child) - if (childResult !== null) { - return { - entry: child, - result: childResult, - } + const childResult = child.queryThroughChildren(predicate) + if (childResult.result !== null) { + return childResult } } return { @@ -105,7 +105,10 @@ export class ShadowDomEntry extends ShadowRootObserver { if (current === this) { return null } - return current.element.matches(selectors) + if (current.element.matches(selectors)) { + return current + } + return null }) return entry } From 882115fd8a0c4ddc9f3f2eabfa88f71fc46f305e Mon Sep 17 00:00:00 2001 From: the1812 Date: Wed, 4 Sep 2024 22:29:10 +0800 Subject: [PATCH 09/30] Fix replies not updating --- src/components/utils/comment/areas/v3.ts | 62 ++++++++++-------------- src/core/shadow-root/dom-entry.ts | 3 ++ src/core/shadow-root/dom-observer.ts | 3 ++ 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/components/utils/comment/areas/v3.ts b/src/components/utils/comment/areas/v3.ts index 71889c7cc..5691f5dd5 100644 --- a/src/components/utils/comment/areas/v3.ts +++ b/src/components/utils/comment/areas/v3.ts @@ -49,31 +49,26 @@ export class CommentAreaV3 extends CommentArea { ?.element as HTMLElement } + protected parseCommentReplyItem(replyEntry: ShadowDomEntry): CommentReplyItem { + const replyLitData = CommentAreaV3.getLitData(replyEntry) + return new CommentReplyItem({ + id: replyLitData.rpid_str, + element: replyEntry.element as HTMLElement, + userId: replyLitData.member.mid, + userName: replyLitData.member.uname, + content: replyLitData.content.message, + time: replyLitData.ctime * 1000, + likes: replyLitData.like, + frameworkSpecificProps: replyLitData, + }) + } + protected parseCommentItem(entry: ShadowDomEntry): CommentItem { const litData = CommentAreaV3.getLitData(entry) - const parseReplies = () => { - if (!litData.replies) { - return [] - } - return (litData.replies as any[]) - .map((r): CommentReplyItem | null => { - const element = this.getReplyItemElement(entry, r.rpid_str) - if (!element) { - return null - } - return new CommentReplyItem({ - id: r.rpid_str, - element, - userId: r.member.mid, - userName: r.member.uname, - content: r.content.message, - time: r.ctime * 1000, - likes: r.like, - frameworkSpecificProps: r, - }) - }) - .filter((r): r is CommentReplyItem => r !== null) - } + const getReplies = () => + entry + .querySelectorAllAsEntry(CommentAreaV3.commentReplyItemSelectors) + .map(replyEntry => this.parseCommentReplyItem(replyEntry)) const item = new CommentItem({ id: litData.rpid_str, element: entry.element as HTMLElement, @@ -85,23 +80,16 @@ export class CommentAreaV3 extends CommentArea { pictures: litData.content?.pictures?.map((img: { img_src: string }) => { return img.img_src }), - replies: parseReplies(), + replies: getReplies(), frameworkSpecificProps: litData, }) - if (item.replies.length < litData.rcount) { - const handler = (e: CustomEvent) => { - const replyEntry = e.detail - if (!replyEntry.element.matches(CommentAreaV3.commentReplyItemSelectors)) { - return - } - item.replies = parseReplies() + entry.addEventListener( + ShadowRootEvents.Updated, + lodash.debounce(() => { + item.replies = getReplies() item.dispatchRepliesUpdate(item.replies) - if (item.replies.length >= litData.rcount) { - entry.removeEventListener(ShadowRootEvents.Added, handler) - } - } - entry.addEventListener(ShadowRootEvents.Added, handler) - } + }), + ) return item } diff --git a/src/core/shadow-root/dom-entry.ts b/src/core/shadow-root/dom-entry.ts index de5fd9622..505f7b36b 100644 --- a/src/core/shadow-root/dom-entry.ts +++ b/src/core/shadow-root/dom-entry.ts @@ -38,6 +38,9 @@ export class ShadowDomEntry extends ShadowRootObserver { } protected mutationHandler(records: MutationRecord[]) { + if (records.length > 0) { + this.dispatchEvent(new CustomEvent(ShadowRootEvents.Updated, { detail: records })) + } records.forEach(record => { record.removedNodes.forEach(node => { if (node instanceof Element) { diff --git a/src/core/shadow-root/dom-observer.ts b/src/core/shadow-root/dom-observer.ts index e34728b2e..7d286310b 100644 --- a/src/core/shadow-root/dom-observer.ts +++ b/src/core/shadow-root/dom-observer.ts @@ -24,6 +24,9 @@ export class ShadowDomObserver extends ShadowRootObserver { } protected mutationHandler(records: MutationRecord[]) { + if (records.length > 0) { + this.dispatchEvent(new CustomEvent(ShadowRootEvents.Updated, { detail: records })) + } records.forEach(record => { record.removedNodes.forEach(node => { if (node instanceof Element) { From 83327912655845d0e4104c53ea32b82a21170571 Mon Sep 17 00:00:00 2001 From: the1812 Date: Wed, 4 Sep 2024 23:06:58 +0800 Subject: [PATCH 10/30] Fix addMenuItem not work with v3 reply --- src/components/utils/comment/areas/v3.ts | 33 +++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/components/utils/comment/areas/v3.ts b/src/components/utils/comment/areas/v3.ts index 5691f5dd5..bcb31f117 100644 --- a/src/components/utils/comment/areas/v3.ts +++ b/src/components/utils/comment/areas/v3.ts @@ -1,7 +1,7 @@ import { ShadowDomObserver, shadowDomObserver, ShadowRootEvents } from '@/core/shadow-root' import { CommentReplyItem } from '../reply-item' import { CommentArea } from './base' -import { ShadowDomEntry } from '@/core/shadow-root/dom-entry' +import { ShadowDomEntry, ShadowDomEntrySymbol } from '@/core/shadow-root/dom-entry' import { CommentItem } from '../comment-item' import { deleteValue } from '@/core/utils' import { select } from '@/core/spin-query' @@ -11,6 +11,7 @@ export class CommentAreaV3 extends CommentArea { protected static commentReplyItemSelectors = 'bili-comment-reply-renderer' protected static commentActionsSelectors = 'bili-comment-action-buttons-renderer' protected static commentMenuSelectors = 'bili-comment-menu' + protected static MenuItemConfigSymbol = Symbol.for('CommentAreaV3.MenuItemConfigSymbol') protected static getLitData(entry: ShadowDomEntry) { const host = entry.element as HTMLElement & { __data: any } // eslint-disable-next-line no-underscore-dangle @@ -86,8 +87,14 @@ export class CommentAreaV3 extends CommentArea { entry.addEventListener( ShadowRootEvents.Updated, lodash.debounce(() => { - item.replies = getReplies() - item.dispatchRepliesUpdate(item.replies) + const newReplies = getReplies() + const hasUpdate = + item.replies.length !== newReplies.length || + !item.replies.every(r => newReplies.some(newReply => newReply.id === r.id)) + if (hasUpdate) { + item.replies = newReplies + item.dispatchRepliesUpdate(item.replies) + } }), ) return item @@ -155,10 +162,8 @@ export class CommentAreaV3 extends CommentArea { item: CommentReplyItem, config: { className: string; text: string; action: (e: MouseEvent) => void }, ) { - const itemEntry = [...this.itemEntryMap.entries()].find( - ([, savedItem]) => savedItem === item, - )?.[0] - if (!itemEntry) { + const itemEntry = item.element[ShadowDomEntrySymbol] + if (itemEntry === undefined) { return } @@ -170,14 +175,24 @@ export class CommentAreaV3 extends CommentArea { } const menu = actions.querySelectorAsEntry(CommentAreaV3.commentMenuSelectors) - const list = menu?.querySelector('#options') + if (!menu) { + return + } + + const list = menu.querySelector('#options') + const alreadyAdded = list.querySelector(`li.${config.className}`) + if (alreadyAdded) { + return + } + const listItem = document.createElement('li') listItem.innerHTML = config.text listItem.className = config.className + listItem[CommentAreaV3.MenuItemConfigSymbol] = config listItem.addEventListener('click', e => { config.action(e) ;(menu.element as HTMLElement).style.setProperty('--bili-comment-menu-display', null) }) - list?.appendChild(listItem) + list.appendChild(listItem) } } From 93bcbed455e875fda81d70cab8357288eaa1ee59 Mon Sep 17 00:00:00 2001 From: the1812 Date: Wed, 4 Sep 2024 23:18:18 +0800 Subject: [PATCH 11/30] Add shadowDomEntry getter --- src/components/utils/comment/areas/v3.ts | 4 ++-- src/components/utils/comment/comment-item.ts | 4 +++- src/components/utils/comment/reply-item.ts | 9 ++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/utils/comment/areas/v3.ts b/src/components/utils/comment/areas/v3.ts index bcb31f117..eadd58bb8 100644 --- a/src/components/utils/comment/areas/v3.ts +++ b/src/components/utils/comment/areas/v3.ts @@ -1,7 +1,7 @@ import { ShadowDomObserver, shadowDomObserver, ShadowRootEvents } from '@/core/shadow-root' import { CommentReplyItem } from '../reply-item' import { CommentArea } from './base' -import { ShadowDomEntry, ShadowDomEntrySymbol } from '@/core/shadow-root/dom-entry' +import { ShadowDomEntry } from '@/core/shadow-root/dom-entry' import { CommentItem } from '../comment-item' import { deleteValue } from '@/core/utils' import { select } from '@/core/spin-query' @@ -162,7 +162,7 @@ export class CommentAreaV3 extends CommentArea { item: CommentReplyItem, config: { className: string; text: string; action: (e: MouseEvent) => void }, ) { - const itemEntry = item.element[ShadowDomEntrySymbol] + const itemEntry = item.shadowDomEntry if (itemEntry === undefined) { return } diff --git a/src/components/utils/comment/comment-item.ts b/src/components/utils/comment/comment-item.ts index 91dd561c1..d46cce276 100644 --- a/src/components/utils/comment/comment-item.ts +++ b/src/components/utils/comment/comment-item.ts @@ -13,7 +13,9 @@ export class CommentItem extends CommentReplyItem { /** 回复 */ replies: CommentReplyItem[] - constructor(initParams: Omit) { + constructor( + initParams: Omit, + ) { super(initParams) this.pictures = initParams.pictures ?? [] this.replies = initParams.replies diff --git a/src/components/utils/comment/reply-item.ts b/src/components/utils/comment/reply-item.ts index df2a80784..b50c0f4fd 100644 --- a/src/components/utils/comment/reply-item.ts +++ b/src/components/utils/comment/reply-item.ts @@ -1,3 +1,5 @@ +import { ShadowDomEntry, ShadowDomEntrySymbol } from '@/core/shadow-root/dom-entry' + /** 表示一条评论回复 */ export class CommentReplyItem extends EventTarget { /** 对应元素 */ @@ -19,7 +21,7 @@ export class CommentReplyItem extends EventTarget { /** 对应的框架特定 Props (可能是 Vue 或者 Lit) */ frameworkSpecificProps: any - constructor(initParams: Omit) { + constructor(initParams: Omit) { super() this.element = initParams.element this.id = initParams.id @@ -31,4 +33,9 @@ export class CommentReplyItem extends EventTarget { this.likes = initParams.likes this.frameworkSpecificProps = initParams.frameworkSpecificProps } + + /** 对应元素的 ShadowDomEntry */ + get shadowDomEntry(): ShadowDomEntry { + return this.element[ShadowDomEntrySymbol] + } } From 3bdf8354dc460c8f780273498b44ec7db555d3d4 Mon Sep 17 00:00:00 2001 From: the1812 Date: Wed, 4 Sep 2024 23:18:34 +0800 Subject: [PATCH 12/30] Add CommentAreaV3 support --- .../lib/components/utils/ip-show/index.ts | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/registry/lib/components/utils/ip-show/index.ts b/registry/lib/components/utils/ip-show/index.ts index d00ccb1aa..599dee567 100644 --- a/registry/lib/components/utils/ip-show/index.ts +++ b/registry/lib/components/utils/ip-show/index.ts @@ -2,6 +2,7 @@ /* eslint-disable yoda */ import { defineComponentMetadata } from '@/components/define' import { CommentItem, CommentReplyItem } from '@/components/utils/comment-apis' +import { select } from '@/core/spin-query' // 新版评论区IP属地获取 const getIpLocation = (item: CommentReplyItem) => { @@ -272,20 +273,37 @@ const observer = new MutationObserver(mutations => { observer.observe(document.head, { childList: true }) const processItems = (items: CommentReplyItem[]) => { - items.forEach(item => { + items.forEach(async item => { const location = getIpLocation(item) - if (location !== undefined) { - const replyTime = + if (location === undefined) { + return + } + const replyTime = await (() => { + if (item.shadowDomEntry) { + return select(() => item.shadowDomEntry.querySelector('#pubdate'), { + queryInterval: 100, + maxRetry: 30, + }) + } + return ( item.element.querySelector('.reply-info>.reply-time') ?? item.element.querySelector('.sub-reply-info>.sub-reply-time') - if (replyTime.childElementCount === 0) { - // 避免在评论更新的情况下重复添加 - const replyLocation = document.createElement('span') - replyLocation.style.marginLeft = `${marginLeft}px` - replyLocation.innerText = location - replyTime.appendChild(replyLocation) - } + ) + })() + if (replyTime === null) { + return } + const existingLocation = replyTime.querySelector('.ip-location') as HTMLElement | null + if (existingLocation !== null) { + existingLocation.innerText = location + return + } + + const replyLocation = document.createElement('span') + replyLocation.className = 'ip-location' + replyLocation.style.marginLeft = `${marginLeft}px` + replyLocation.innerText = location + replyTime.appendChild(replyLocation) }) } From e91b9e6c8314d05307bb937a5511f6ebaf6e3e0b Mon Sep 17 00:00:00 2001 From: the1812 Date: Wed, 4 Sep 2024 23:40:20 +0800 Subject: [PATCH 13/30] Fix fold comment button position (fix #4890) --- .../fold-comments/fold-comment-shadow.scss | 3 +++ .../components/feeds/fold-comments/index.ts | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 registry/lib/components/feeds/fold-comments/fold-comment-shadow.scss diff --git a/registry/lib/components/feeds/fold-comments/fold-comment-shadow.scss b/registry/lib/components/feeds/fold-comments/fold-comment-shadow.scss new file mode 100644 index 000000000..5ba124448 --- /dev/null +++ b/registry/lib/components/feeds/fold-comments/fold-comment-shadow.scss @@ -0,0 +1,3 @@ +#end .bottombar { + padding-bottom: 8px !important; +} diff --git a/registry/lib/components/feeds/fold-comments/index.ts b/registry/lib/components/feeds/fold-comments/index.ts index 26b688d33..38a22a4a0 100644 --- a/registry/lib/components/feeds/fold-comments/index.ts +++ b/registry/lib/components/feeds/fold-comments/index.ts @@ -6,6 +6,7 @@ import { select } from '@/core/spin-query' import { childListSubtree } from '@/core/observer' const entry = async () => { + const { ShadowRootStyles } = await import('@/core/shadow-root') const { forEachFeedsCard } = await import('@/components/feeds/api') const { childList } = await import('@/core/observer') const commentSelector = '.bb-comment, .bili-comment-container' @@ -30,20 +31,23 @@ const entry = async () => { commentBox.insertAdjacentElement('beforeend', button) } if (feedsCardsManager.managerType === 'v2') { - const existingComment = dq(card, commentSelector) as HTMLElement + const getExistingComment = () => dq(card, commentSelector) as HTMLElement + const isCommentAreaReady = () => { + const existingComment = getExistingComment() + return existingComment !== null && dq(existingComment, 'bili-comments') + } const handler = () => { const button = dq(card, '.bili-dyn-action.comment') as HTMLElement button?.click() } - if (!existingComment) { + if (!isCommentAreaReady()) { childListSubtree(card, () => { - const panel = dq(card, commentSelector) - if (panel) { + if (isCommentAreaReady()) { injectToComment(card, handler) } }) } else { - injectToComment(existingComment, handler) + injectToComment(getExistingComment(), handler) } return } @@ -75,6 +79,10 @@ const entry = async () => { forEachFeedsCard({ added: c => injectButton(c.element), }) + + const styles = await import('./fold-comment-shadow.scss').then(m => m.default) + const shadowRootStyles = new ShadowRootStyles() + shadowRootStyles.addStyle(styles) } export const component = defineComponentMetadata({ From 6db6b76572cd3b4d0b4a349517a63788307fe308 Mon Sep 17 00:00:00 2001 From: the1812 Date: Wed, 4 Sep 2024 23:40:35 +0800 Subject: [PATCH 14/30] Fix variable name --- registry/lib/components/style/simplify/comments/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/lib/components/style/simplify/comments/index.ts b/registry/lib/components/style/simplify/comments/index.ts index 002971a25..d2ede8aba 100644 --- a/registry/lib/components/style/simplify/comments/index.ts +++ b/registry/lib/components/style/simplify/comments/index.ts @@ -53,8 +53,8 @@ export const component = wrapSwitchOptions({ const { ShadowRootStyles } = await import('@/core/shadow-root') const v3Styles = await import('./comments-v3.scss').then(m => m.default) - const shadowDom = new ShadowRootStyles() - shadowDom.addStyle(v3Styles) + const shadowRootStyles = new ShadowRootStyles() + shadowRootStyles.addStyle(v3Styles) }, instantStyles: [ { From 10bb55b564811fca86fc52d7222f9f9236b7517f Mon Sep 17 00:00:00 2001 From: the1812 Date: Thu, 5 Sep 2024 23:27:30 +0800 Subject: [PATCH 15/30] Support async observing --- src/components/utils/comment/areas/base.ts | 6 ++--- src/components/utils/comment/areas/v3.ts | 22 +++++++++++-------- .../utils/comment/comment-area-manager.ts | 4 ++-- src/components/utils/comment/comment-area.ts | 1 + 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/components/utils/comment/areas/base.ts b/src/components/utils/comment/areas/base.ts index d1203f728..d4fc38bf7 100644 --- a/src/components/utils/comment/areas/base.ts +++ b/src/components/utils/comment/areas/base.ts @@ -11,8 +11,8 @@ export abstract class CommentArea { this.element = element } - abstract observe(): void - abstract disconnect(): void + abstract observe(): void | Promise + abstract disconnect(): void | Promise abstract addMenuItem( item: CommentReplyItem, config: { @@ -20,7 +20,7 @@ export abstract class CommentArea { text: string action: (e: MouseEvent) => void }, - ): void + ): void | Promise static resolveCallbackPair void>( input: CommentCallbackInput, diff --git a/src/components/utils/comment/areas/v3.ts b/src/components/utils/comment/areas/v3.ts index eadd58bb8..8d0b741e6 100644 --- a/src/components/utils/comment/areas/v3.ts +++ b/src/components/utils/comment/areas/v3.ts @@ -22,8 +22,9 @@ export class CommentAreaV3 extends CommentArea { return element.tagName.toLowerCase() === 'bili-comments' } + public commentAreaEntry: ShadowDomEntry + protected shadowDomObserver: ShadowDomObserver - protected commentAreaEntry: ShadowDomEntry protected itemEntryMap = new Map() private handleEntryAdded: (e: CustomEvent) => void @@ -141,14 +142,17 @@ export class CommentAreaV3 extends CommentArea { } observe() { - this.areaObserverDisposer = this.shadowDomObserver.watchShadowDom({ - added: shadowDom => { - if (shadowDom.element !== this.element) { - return - } - this.commentAreaEntry = shadowDom - this.observeCommentItems() - }, + return new Promise(resolve => { + this.areaObserverDisposer = this.shadowDomObserver.watchShadowDom({ + added: shadowDom => { + if (shadowDom.element !== this.element) { + return + } + this.commentAreaEntry = shadowDom + this.observeCommentItems() + resolve() + }, + }) }) } diff --git a/src/components/utils/comment/comment-area-manager.ts b/src/components/utils/comment/comment-area-manager.ts index 6a8b86175..1d2b00904 100644 --- a/src/components/utils/comment/comment-area-manager.ts +++ b/src/components/utils/comment/comment-area-manager.ts @@ -25,11 +25,11 @@ export class CommentAreaManager { dqa(CommentAreaManager.commentAreaSelectors).forEach(it => this.observeAreas(it)) } - observeAreas(node: Node) { + async observeAreas(node: Node) { if (node instanceof HTMLElement && node.matches(CommentAreaManager.commentAreaSelectors)) { const area = getCommentArea(node) this.commentAreas.push(area) - area.observe() + await area.observe() this.commentAreaCallbacks.forEach(c => c.added?.(area)) const [observer] = childList(area.element.parentElement, records => { records.forEach(r => { diff --git a/src/components/utils/comment/comment-area.ts b/src/components/utils/comment/comment-area.ts index 26be5071f..6193c2369 100644 --- a/src/components/utils/comment/comment-area.ts +++ b/src/components/utils/comment/comment-area.ts @@ -16,3 +16,4 @@ export const getCommentArea = (element: HTMLElement): CommentArea => { export * from './areas/base' export * from './areas/v1' export * from './areas/v2' +export * from './areas/v3' From 1fc893d03d91ca79f8d915ba2a197c78e14aedba Mon Sep 17 00:00:00 2001 From: the1812 Date: Thu, 5 Sep 2024 23:39:07 +0800 Subject: [PATCH 16/30] Add CommentAreaV3 support --- .../disable-search-link-shadow.scss | 8 +++ .../comments/disable-search-link/index.ts | 55 ++++++++++++++----- 2 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 registry/lib/components/utils/comments/disable-search-link/disable-search-link-shadow.scss diff --git a/registry/lib/components/utils/comments/disable-search-link/disable-search-link-shadow.scss b/registry/lib/components/utils/comments/disable-search-link/disable-search-link-shadow.scss new file mode 100644 index 000000000..58b64378d --- /dev/null +++ b/registry/lib/components/utils/comments/disable-search-link/disable-search-link-shadow.scss @@ -0,0 +1,8 @@ +#contents a[data-type="search"] { + color: inherit; + cursor: inherit; + display: contents !important; + img { + display: none; + } +} diff --git a/registry/lib/components/utils/comments/disable-search-link/index.ts b/registry/lib/components/utils/comments/disable-search-link/index.ts index 19395fec2..662603743 100644 --- a/registry/lib/components/utils/comments/disable-search-link/index.ts +++ b/registry/lib/components/utils/comments/disable-search-link/index.ts @@ -1,5 +1,6 @@ import { defineComponentMetadata } from '@/components/define' -import { forEachCommentArea } from '@/components/utils/comment-apis' +import { forEachCommentArea, CommentAreaV3 } from '@/components/utils/comment-apis' +import { ShadowRootEvents, ShadowRootStyles } from '@/core/shadow-root' import { preventEvent } from '@/core/utils' const name = 'disableCommentsSearchLink' @@ -17,21 +18,45 @@ export const component = defineComponentMetadata({ tags: [componentsTags.utils, componentsTags.style], entry: async () => { prevent = true - forEachCommentArea(area => { - preventEvent(area.element, 'click', e => { - if (!(e.target instanceof HTMLElement) || !prevent) { + forEachCommentArea(async area => { + const isV3Area = area instanceof CommentAreaV3 + if (isV3Area) { + const styles = await import('./disable-search-link-shadow.scss').then(m => m.default) + const shadowRootStyles = new ShadowRootStyles() + shadowRootStyles.addStyle(styles) + area.commentAreaEntry.addEventListener( + ShadowRootEvents.Updated, + (e: CustomEvent) => { + const records = e.detail + records.forEach(record => { + record.addedNodes.forEach(node => { + const isCommentLink = + node instanceof HTMLAnchorElement && node.getAttribute('data-type') === 'search' + if (!isCommentLink) { + return + } + node.removeAttribute('href') + node.removeAttribute('target') + }) + }) + }, + ) + } else { + preventEvent(area.element, 'click', e => { + if (!(e.target instanceof HTMLElement) || !prevent) { + return false + } + const element = e.target as HTMLElement + if ( + ['.jump-link.search-word', '.icon.search-word'].some(selector => + element.matches(selector), + ) + ) { + return true + } return false - } - const element = e.target as HTMLElement - if ( - ['.jump-link.search-word', '.icon.search-word'].some(selector => - element.matches(selector), - ) - ) { - return true - } - return false - }) + }) + } }) }, reload: () => { From 40e3bfa87a0305520f64ef4995e4061df547bb1a Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 8 Sep 2024 10:18:12 +0800 Subject: [PATCH 17/30] Add ShadowRootStyleDefinition --- .../fold-comments/fold-comment-shadow.scss | 6 +- .../components/feeds/fold-comments/index.ts | 10 ++- .../style/simplify/comments/comments-v3.scss | 85 +++++++++---------- .../style/simplify/comments/index.ts | 7 +- .../disable-search-link-shadow.scss | 14 +-- .../comments/disable-search-link/index.ts | 7 +- src/core/shadow-root/styles.ts | 27 ++++-- 7 files changed, 86 insertions(+), 70 deletions(-) diff --git a/registry/lib/components/feeds/fold-comments/fold-comment-shadow.scss b/registry/lib/components/feeds/fold-comments/fold-comment-shadow.scss index 5ba124448..5a42d49de 100644 --- a/registry/lib/components/feeds/fold-comments/fold-comment-shadow.scss +++ b/registry/lib/components/feeds/fold-comments/fold-comment-shadow.scss @@ -1,3 +1,5 @@ -#end .bottombar { - padding-bottom: 8px !important; +:host(bili-comments) { + #end .bottombar { + padding-bottom: 8px !important; + } } diff --git a/registry/lib/components/feeds/fold-comments/index.ts b/registry/lib/components/feeds/fold-comments/index.ts index 38a22a4a0..4ddce9bc2 100644 --- a/registry/lib/components/feeds/fold-comments/index.ts +++ b/registry/lib/components/feeds/fold-comments/index.ts @@ -6,7 +6,7 @@ import { select } from '@/core/spin-query' import { childListSubtree } from '@/core/observer' const entry = async () => { - const { ShadowRootStyles } = await import('@/core/shadow-root') + const { shadowRootStyles } = await import('@/core/shadow-root') const { forEachFeedsCard } = await import('@/components/feeds/api') const { childList } = await import('@/core/observer') const commentSelector = '.bb-comment, .bili-comment-container' @@ -80,9 +80,11 @@ const entry = async () => { added: c => injectButton(c.element), }) - const styles = await import('./fold-comment-shadow.scss').then(m => m.default) - const shadowRootStyles = new ShadowRootStyles() - shadowRootStyles.addStyle(styles) + const style = await import('./fold-comment-shadow.scss').then(m => m.default) + shadowRootStyles.addStyle({ + id: 'foldComments', + style, + }) } export const component = defineComponentMetadata({ diff --git a/registry/lib/components/style/simplify/comments/comments-v3.scss b/registry/lib/components/style/simplify/comments/comments-v3.scss index b9c428e9a..6075b9f39 100644 --- a/registry/lib/components/style/simplify/comments/comments-v3.scss +++ b/registry/lib/components/style/simplify/comments/comments-v3.scss @@ -2,58 +2,57 @@ $prefix: 'simplifyComments-switch'; -:host-context(.comment-m-v1) { - &:host(bili-rich-text) { - #contents img, #contents a i { - max-width: 1.4em; - max-height: 1.4em; - } +:host(bili-rich-text) { + #contents img, + #contents a i { + max-width: 1.4em; + max-height: 1.4em; } +} - &:host-context(body.#{$prefix}-replyEditor) { - &:host(bili-comment-box) { - #pub button { - font-size: 14px; - } - } - &:host(bili-checkbox) { - #label { - font-size: 15px; - } - } - &:host(bili-comment-textarea) { - #input { - font-size: 13px; - } - #input, - #input::placeholder { - line-height: normal; - } +:host-context(body.#{$prefix}-replyEditor) { + &:host(bili-comment-box) { + #pub button { + font-size: 14px; } } - &:host-context(body.#{$prefix}-userLevel):host(bili-comment-user-info) { - #user-level { - display: none; + &:host(bili-checkbox) { + #label { + font-size: 15px; } } - &:host-context(body.#{$prefix}-decorateAndTime):host(bili-comment-renderer) { - bili-comment-user-sailing-card { - display: none; + &:host(bili-comment-textarea) { + #input { + font-size: 13px; } - } - &:host-context(body.#{$prefix}-fansMedal):host(bili-comment-user-info) { - bili-comment-user-medal { - display: none; + #input, + #input::placeholder { + line-height: normal; } } - &:host-context(body.#{$prefix}-subReplyNewLine):host(bili-comment-reply-renderer) { - bili-comment-user-info { - display: block; - } +} +:host-context(body.#{$prefix}-userLevel):host(bili-comment-user-info) { + #user-level { + display: none; } - &:host-context(body.#{$prefix}-eventBanner):host(bili-comments-header-renderer) { - bili-comments-notice { - display: none; - } +} +:host-context(body.#{$prefix}-decorateAndTime):host(bili-comment-renderer) { + bili-comment-user-sailing-card { + display: none; + } +} +:host-context(body.#{$prefix}-fansMedal):host(bili-comment-user-info) { + bili-comment-user-medal { + display: none; + } +} +:host-context(body.#{$prefix}-subReplyNewLine):host(bili-comment-reply-renderer) { + bili-comment-user-info { + display: block; + } +} +:host-context(body.#{$prefix}-eventBanner):host(bili-comments-header-renderer) { + bili-comments-notice { + display: none; } } diff --git a/registry/lib/components/style/simplify/comments/index.ts b/registry/lib/components/style/simplify/comments/index.ts index d2ede8aba..ebe4b7d16 100644 --- a/registry/lib/components/style/simplify/comments/index.ts +++ b/registry/lib/components/style/simplify/comments/index.ts @@ -51,10 +51,9 @@ export const component = wrapSwitchOptions({ true, ) - const { ShadowRootStyles } = await import('@/core/shadow-root') - const v3Styles = await import('./comments-v3.scss').then(m => m.default) - const shadowRootStyles = new ShadowRootStyles() - shadowRootStyles.addStyle(v3Styles) + const { shadowRootStyles } = await import('@/core/shadow-root') + const v3Style = await import('./comments-v3.scss').then(m => m.default) + shadowRootStyles.addStyle({ id: name, style: v3Style }) }, instantStyles: [ { diff --git a/registry/lib/components/utils/comments/disable-search-link/disable-search-link-shadow.scss b/registry/lib/components/utils/comments/disable-search-link/disable-search-link-shadow.scss index 58b64378d..e8db5f41a 100644 --- a/registry/lib/components/utils/comments/disable-search-link/disable-search-link-shadow.scss +++ b/registry/lib/components/utils/comments/disable-search-link/disable-search-link-shadow.scss @@ -1,8 +1,10 @@ -#contents a[data-type="search"] { - color: inherit; - cursor: inherit; - display: contents !important; - img { - display: none; +:host(bili-rich-text) { + #contents a[data-type='search'] { + color: inherit; + cursor: inherit; + display: contents !important; + img { + display: none; + } } } diff --git a/registry/lib/components/utils/comments/disable-search-link/index.ts b/registry/lib/components/utils/comments/disable-search-link/index.ts index 662603743..384e5b194 100644 --- a/registry/lib/components/utils/comments/disable-search-link/index.ts +++ b/registry/lib/components/utils/comments/disable-search-link/index.ts @@ -1,6 +1,6 @@ import { defineComponentMetadata } from '@/components/define' import { forEachCommentArea, CommentAreaV3 } from '@/components/utils/comment-apis' -import { ShadowRootEvents, ShadowRootStyles } from '@/core/shadow-root' +import { ShadowRootEvents, shadowRootStyles } from '@/core/shadow-root' import { preventEvent } from '@/core/utils' const name = 'disableCommentsSearchLink' @@ -21,9 +21,8 @@ export const component = defineComponentMetadata({ forEachCommentArea(async area => { const isV3Area = area instanceof CommentAreaV3 if (isV3Area) { - const styles = await import('./disable-search-link-shadow.scss').then(m => m.default) - const shadowRootStyles = new ShadowRootStyles() - shadowRootStyles.addStyle(styles) + const style = await import('./disable-search-link-shadow.scss').then(m => m.default) + shadowRootStyles.addStyle({ id: name, style }) area.commentAreaEntry.addEventListener( ShadowRootEvents.Updated, (e: CustomEvent) => { diff --git a/src/core/shadow-root/styles.ts b/src/core/shadow-root/styles.ts index 31ce8f752..c18990044 100644 --- a/src/core/shadow-root/styles.ts +++ b/src/core/shadow-root/styles.ts @@ -1,11 +1,17 @@ import { deleteValue, getRandomId } from '../utils' +import { ShadowDomEntry } from './dom-entry' import { ShadowDomObserver, shadowDomObserver } from './dom-observer' -interface ShadowRootStyleEntry { +export interface ShadowRootStyleEntry { id: string styleSheet: CSSStyleSheet + adoptedShadowDoms: ShadowDomEntry[] remove: () => void } +export interface ShadowRootStyleDefinition { + id?: string + style: string +} export class ShadowRootStyles { observer: ShadowDomObserver = shadowDomObserver protected stylesMap = new Map() @@ -20,25 +26,30 @@ export class ShadowRootStyles { } } - async addStyle(text: string) { + async addStyle(definition: ShadowRootStyleDefinition) { + const { id, style } = definition this.observer.observe() - const id = `shadow-dom-style-${getRandomId()}` + const entryId = `shadow-dom-style-${id !== undefined ? lodash.kebabCase(id) : getRandomId()}` const styleSheet = new CSSStyleSheet() - await styleSheet.replace(text) - document.adoptedStyleSheets.push(styleSheet) + await styleSheet.replace(style) + const adoptedShadowDoms: ShadowDomEntry[] = [] const destroy = this.observer.watchShadowDom({ added: async shadowDom => { shadowDom.shadowRoot.adoptedStyleSheets.push(styleSheet) + adoptedShadowDoms.push(shadowDom) }, }) const entry: ShadowRootStyleEntry = { - id, + id: entryId, styleSheet, + adoptedShadowDoms, remove: () => { destroy() - deleteValue(document.adoptedStyleSheets, it => it === styleSheet) + adoptedShadowDoms.forEach(it => + deleteValue(it.shadowRoot.adoptedStyleSheets, sheet => sheet === styleSheet), + ) this.removeEntry(id) }, } @@ -46,3 +57,5 @@ export class ShadowRootStyles { return entry } } + +export const shadowRootStyles = new ShadowRootStyles() From 1709bd70326f201d862db8103252b2cf9469c59b Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 8 Sep 2024 10:43:48 +0800 Subject: [PATCH 18/30] Add container queries --- .../style/simplify/comments/comments-v3.scss | 48 +++++++++++-------- src/components/switch-options.ts | 4 +- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/registry/lib/components/style/simplify/comments/comments-v3.scss b/registry/lib/components/style/simplify/comments/comments-v3.scss index 6075b9f39..7a1c5b5b7 100644 --- a/registry/lib/components/style/simplify/comments/comments-v3.scss +++ b/registry/lib/components/style/simplify/comments/comments-v3.scss @@ -10,18 +10,18 @@ $prefix: 'simplifyComments-switch'; } } -:host-context(body.#{$prefix}-replyEditor) { - &:host(bili-comment-box) { +@container style(--#{$prefix}-replyEditor: true) { + :host(bili-comment-box) { #pub button { font-size: 14px; } } - &:host(bili-checkbox) { + :host(bili-checkbox) { #label { font-size: 15px; } } - &:host(bili-comment-textarea) { + :host(bili-comment-textarea) { #input { font-size: 13px; } @@ -31,28 +31,38 @@ $prefix: 'simplifyComments-switch'; } } } -:host-context(body.#{$prefix}-userLevel):host(bili-comment-user-info) { - #user-level { - display: none; +@container style(--#{$prefix}-userLevel: true) { + :host(bili-comment-user-info) { + #user-level { + display: none; + } } } -:host-context(body.#{$prefix}-decorateAndTime):host(bili-comment-renderer) { - bili-comment-user-sailing-card { - display: none; +@container style(--#{$prefix}-decorateAndTime: true) { + :host(bili-comment-renderer) { + bili-comment-user-sailing-card { + display: none; + } } } -:host-context(body.#{$prefix}-fansMedal):host(bili-comment-user-info) { - bili-comment-user-medal { - display: none; +@container style(--#{$prefix}-fansMedal: true) { + :host(bili-comment-user-info) { + bili-comment-user-medal { + display: none; + } } } -:host-context(body.#{$prefix}-subReplyNewLine):host(bili-comment-reply-renderer) { - bili-comment-user-info { - display: block; +@container style(--#{$prefix}-subReplyNewLine: true) { + :host(bili-comment-reply-renderer) { + bili-comment-user-info { + display: block; + } } } -:host-context(body.#{$prefix}-eventBanner):host(bili-comments-header-renderer) { - bili-comments-notice { - display: none; +@container style(--#{$prefix}-eventBanner: true) { + :host(bili-comments-header-renderer) { + bili-comments-notice { + display: none; + } } } diff --git a/src/components/switch-options.ts b/src/components/switch-options.ts index 12e573bb5..9435293b4 100644 --- a/src/components/switch-options.ts +++ b/src/components/switch-options.ts @@ -249,7 +249,9 @@ const newSwitchEntry = { - document.body.classList.toggle(`${component.name}-${key}`, value) + const id = `${component.name}-${key}` + document.body.classList.toggle(id, value) + document.documentElement.style.setProperty(`--${id}`, value.toString()) }, true, ) From 773504a551d3062e2e2bb9134ffe1d6441b9fd77 Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 8 Sep 2024 11:09:58 +0800 Subject: [PATCH 19/30] Fix root observer cannot be disconnected --- src/core/shadow-root/dom-observer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/shadow-root/dom-observer.ts b/src/core/shadow-root/dom-observer.ts index 7d286310b..8d9184d6b 100644 --- a/src/core/shadow-root/dom-observer.ts +++ b/src/core/shadow-root/dom-observer.ts @@ -16,6 +16,7 @@ export class ShadowDomObserver extends ShadowRootObserver { } private observing = false + private rootObserver: MutationObserver | undefined = undefined entries: ShadowDomEntry[] = [] @@ -110,11 +111,12 @@ export class ShadowDomObserver extends ShadowRootObserver { } const existingRoots = ShadowRootObserver.queryAllShadowRoots() existingRoots.forEach(root => this.addEntry(root)) - childListSubtree(document.body, records => this.mutationHandler(records)) + ;[this.rootObserver] = childListSubtree(document.body, records => this.mutationHandler(records)) this.observing = true } disconnect() { + this.rootObserver.disconnect() this.entries.forEach(entry => entry.disconnect()) this.entries = [] this.observing = false From be627b3ae904f0e37b44fede3273476d82839a7e Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 8 Sep 2024 13:30:49 +0800 Subject: [PATCH 20/30] Add container query feature detection --- src/client/compatibility.ts | 3 +++ src/core/container-query.ts | 15 +++++++++++++++ src/core/core-apis.ts | 3 +++ 3 files changed, 21 insertions(+) create mode 100644 src/core/container-query.ts diff --git a/src/client/compatibility.ts b/src/client/compatibility.ts index 883e33e82..c8b2d2996 100644 --- a/src/client/compatibility.ts +++ b/src/client/compatibility.ts @@ -1,6 +1,7 @@ /* eslint-disable no-underscore-dangle */ import { contentLoaded, fullyLoaded } from '@/core/life-cycle' import { select } from '@/core/spin-query' +import { setupContainerQueryFeatureDetection } from '@/core/container-query' export const compatibilityPatch = () => { contentLoaded(async () => { @@ -22,6 +23,8 @@ export const compatibilityPatch = () => { const { playerPolyfill } = await import('@/components/video/player-adaptor') playerPolyfill() } + + await setupContainerQueryFeatureDetection() }) fullyLoaded(() => { select('meta[name=spm_prefix]').then(spm => { diff --git a/src/core/container-query.ts b/src/core/container-query.ts new file mode 100644 index 000000000..69ef9a287 --- /dev/null +++ b/src/core/container-query.ts @@ -0,0 +1,15 @@ +export const setupContainerQueryFeatureDetection = async () => { + document.documentElement.style.setProperty('--container-query-feature-detection', 'true') + const bodyStyle = new CSSStyleSheet() + await bodyStyle.replace( + '@container style(--container-query-feature-detection) { body { --container-query-supported: true } }', + ) + document.adoptedStyleSheets.push(bodyStyle) +} + +export const isContainerStyleQuerySupported = lodash.once(() => { + return ( + window.getComputedStyle(document.body).getPropertyValue('--container-query-supported') === + 'true' + ) +}) diff --git a/src/core/core-apis.ts b/src/core/core-apis.ts index 2e1fea54d..29246cec7 100644 --- a/src/core/core-apis.ts +++ b/src/core/core-apis.ts @@ -1,5 +1,6 @@ import * as ajax from '@/core/ajax' import * as cdnTypes from '@/core/cdn-types' +import * as containerQuery from '@/core/container-query' import * as download from '@/core/download' import * as dialog from '@/core/dialog' import * as externalInput from '@/core/external-input' @@ -41,6 +42,7 @@ import { pluginApis } from '@/plugins/api' export const coreApis = { ajax, cdnTypes, + containerQuery, download, dialog, externalInput, @@ -83,6 +85,7 @@ export type CoreApis = typeof coreApis export const externalApis = { ajax, ...cdnTypes, + ...containerQuery, ...download, ...dialog, ...externalInput, From 68181f8f81b0a659bb3e927fd511123ac0b7683b Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 8 Sep 2024 13:57:56 +0800 Subject: [PATCH 21/30] Add ShadowRootStyles.removeStyle --- src/core/shadow-root/styles.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/shadow-root/styles.ts b/src/core/shadow-root/styles.ts index c18990044..f33615352 100644 --- a/src/core/shadow-root/styles.ts +++ b/src/core/shadow-root/styles.ts @@ -56,6 +56,14 @@ export class ShadowRootStyles { this.addEntry(id, entry) return entry } + + removeStyle(id: string) { + if (this.stylesMap.has(id)) { + const style = this.stylesMap.get(id) + style.remove() + this.removeEntry(id) + } + } } export const shadowRootStyles = new ShadowRootStyles() From 62e1ab2b8237d87c175bcbb09150afc8a97795f7 Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 8 Sep 2024 13:58:08 +0800 Subject: [PATCH 22/30] Add support for Firefox --- .../comments/comments-v3-firefox/base.scss | 7 +++ .../decorate-and-time.scss | 5 ++ .../comments-v3-firefox/event-banner.scss | 5 ++ .../comments-v3-firefox/fans-medal.scss | 5 ++ .../comments-v3-firefox/reply-editor.scss | 19 ++++++++ .../sub-reply-new-line.scss | 5 ++ .../comments-v3-firefox/user-level.scss | 5 ++ .../style/simplify/comments/comments-v3.scss | 2 - .../style/simplify/comments/index.ts | 46 +++++++++++++++++-- 9 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 registry/lib/components/style/simplify/comments/comments-v3-firefox/base.scss create mode 100644 registry/lib/components/style/simplify/comments/comments-v3-firefox/decorate-and-time.scss create mode 100644 registry/lib/components/style/simplify/comments/comments-v3-firefox/event-banner.scss create mode 100644 registry/lib/components/style/simplify/comments/comments-v3-firefox/fans-medal.scss create mode 100644 registry/lib/components/style/simplify/comments/comments-v3-firefox/reply-editor.scss create mode 100644 registry/lib/components/style/simplify/comments/comments-v3-firefox/sub-reply-new-line.scss create mode 100644 registry/lib/components/style/simplify/comments/comments-v3-firefox/user-level.scss diff --git a/registry/lib/components/style/simplify/comments/comments-v3-firefox/base.scss b/registry/lib/components/style/simplify/comments/comments-v3-firefox/base.scss new file mode 100644 index 000000000..48b783918 --- /dev/null +++ b/registry/lib/components/style/simplify/comments/comments-v3-firefox/base.scss @@ -0,0 +1,7 @@ +:host(bili-rich-text) { + #contents img, + #contents a i { + max-width: 1.4em; + max-height: 1.4em; + } +} diff --git a/registry/lib/components/style/simplify/comments/comments-v3-firefox/decorate-and-time.scss b/registry/lib/components/style/simplify/comments/comments-v3-firefox/decorate-and-time.scss new file mode 100644 index 000000000..36c478517 --- /dev/null +++ b/registry/lib/components/style/simplify/comments/comments-v3-firefox/decorate-and-time.scss @@ -0,0 +1,5 @@ +:host(bili-comment-renderer) { + bili-comment-user-sailing-card { + display: none; + } +} diff --git a/registry/lib/components/style/simplify/comments/comments-v3-firefox/event-banner.scss b/registry/lib/components/style/simplify/comments/comments-v3-firefox/event-banner.scss new file mode 100644 index 000000000..52e807658 --- /dev/null +++ b/registry/lib/components/style/simplify/comments/comments-v3-firefox/event-banner.scss @@ -0,0 +1,5 @@ +:host(bili-comments-header-renderer) { + bili-comments-notice { + display: none; + } +} diff --git a/registry/lib/components/style/simplify/comments/comments-v3-firefox/fans-medal.scss b/registry/lib/components/style/simplify/comments/comments-v3-firefox/fans-medal.scss new file mode 100644 index 000000000..272ccf823 --- /dev/null +++ b/registry/lib/components/style/simplify/comments/comments-v3-firefox/fans-medal.scss @@ -0,0 +1,5 @@ +:host(bili-comment-user-info) { + bili-comment-user-medal { + display: none; + } +} diff --git a/registry/lib/components/style/simplify/comments/comments-v3-firefox/reply-editor.scss b/registry/lib/components/style/simplify/comments/comments-v3-firefox/reply-editor.scss new file mode 100644 index 000000000..4c80fd8d1 --- /dev/null +++ b/registry/lib/components/style/simplify/comments/comments-v3-firefox/reply-editor.scss @@ -0,0 +1,19 @@ +:host(bili-comment-box) { + #pub button { + font-size: 14px; + } +} +:host(bili-checkbox) { + #label { + font-size: 15px; + } +} +:host(bili-comment-textarea) { + #input { + font-size: 13px; + } + #input, + #input::placeholder { + line-height: normal; + } +} diff --git a/registry/lib/components/style/simplify/comments/comments-v3-firefox/sub-reply-new-line.scss b/registry/lib/components/style/simplify/comments/comments-v3-firefox/sub-reply-new-line.scss new file mode 100644 index 000000000..bcc3f0a72 --- /dev/null +++ b/registry/lib/components/style/simplify/comments/comments-v3-firefox/sub-reply-new-line.scss @@ -0,0 +1,5 @@ +:host(bili-comment-reply-renderer) { + bili-comment-user-info { + display: block; + } +} diff --git a/registry/lib/components/style/simplify/comments/comments-v3-firefox/user-level.scss b/registry/lib/components/style/simplify/comments/comments-v3-firefox/user-level.scss new file mode 100644 index 000000000..0f928b419 --- /dev/null +++ b/registry/lib/components/style/simplify/comments/comments-v3-firefox/user-level.scss @@ -0,0 +1,5 @@ +:host(bili-comment-user-info) { + #user-level { + display: none; + } +} diff --git a/registry/lib/components/style/simplify/comments/comments-v3.scss b/registry/lib/components/style/simplify/comments/comments-v3.scss index 7a1c5b5b7..d66962ff2 100644 --- a/registry/lib/components/style/simplify/comments/comments-v3.scss +++ b/registry/lib/components/style/simplify/comments/comments-v3.scss @@ -1,5 +1,3 @@ -@import 'common'; - $prefix: 'simplifyComments-switch'; :host(bili-rich-text) { diff --git a/registry/lib/components/style/simplify/comments/index.ts b/registry/lib/components/style/simplify/comments/index.ts index ebe4b7d16..c63576814 100644 --- a/registry/lib/components/style/simplify/comments/index.ts +++ b/registry/lib/components/style/simplify/comments/index.ts @@ -41,7 +41,9 @@ export const component = wrapSwitchOptions({ })({ name, displayName: '简化评论区', - entry: async ({ metadata }) => { + entry: async ({ metadata, settings }) => { + const { addStyle, getDefaultStyleID } = await import('@/core/style') + const { isContainerStyleQuerySupported } = await import('@/core/container-query') const { addComponentListener } = await import('@/core/settings') addComponentListener( metadata.name, @@ -51,9 +53,45 @@ export const component = wrapSwitchOptions({ true, ) - const { shadowRootStyles } = await import('@/core/shadow-root') - const v3Style = await import('./comments-v3.scss').then(m => m.default) - shadowRootStyles.addStyle({ id: name, style: v3Style }) + // 等 Firefox 支持 [Container Style Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_size_and_style_queries#container_style_queries_2) 可去除此判断 + if (isContainerStyleQuerySupported()) { + const { shadowRootStyles } = await import('@/core/shadow-root') + const v3Style = await import('./comments-v3.scss').then(m => m.default) + shadowRootStyles.addStyle({ id: name, style: v3Style }) + } else { + const { shadowRootStyles } = await import('@/core/shadow-root') + const firefoxStyles = require.context('./comments-v3-firefox', false, /\.scss$/) + + Object.keys(settings.options).forEach(key => { + if (!key.startsWith('switch-')) { + return + } + const id = `${component.name}.${key}` + const styleName = lodash.kebabCase(key.replace(/^switch-/, '')) + const path = `./${styleName}.scss` + if (!firefoxStyles.keys().includes(path)) { + return + } + + addComponentListener( + id, + (value: boolean) => { + if (value) { + const style = firefoxStyles(path) as string + addStyle(style, styleName) + shadowRootStyles.addStyle({ + id, + style, + }) + } else { + document.getElementById(getDefaultStyleID(styleName))?.remove() + shadowRootStyles.removeStyle(id) + } + }, + true, + ) + }) + } }, instantStyles: [ { From fe8314a29d42f4692ce87586f236362859f1d375 Mon Sep 17 00:00:00 2001 From: the1812 Date: Sun, 8 Sep 2024 15:05:15 +0800 Subject: [PATCH 23/30] Add Shadow DOM support for instantStyles --- .../style/simplify/comments/index.ts | 2 +- .../comments/disable-search-link/index.ts | 9 ++-- .../lib/components/utils/dev-client/client.ts | 6 +-- src/components/types.ts | 24 ++++++--- src/components/user-component.ts | 4 +- src/core/shadow-root/styles.ts | 15 ++++++ src/core/style.ts | 50 +++++++++++++++---- 7 files changed, 84 insertions(+), 26 deletions(-) diff --git a/registry/lib/components/style/simplify/comments/index.ts b/registry/lib/components/style/simplify/comments/index.ts index c63576814..4a395d89e 100644 --- a/registry/lib/components/style/simplify/comments/index.ts +++ b/registry/lib/components/style/simplify/comments/index.ts @@ -57,7 +57,7 @@ export const component = wrapSwitchOptions({ if (isContainerStyleQuerySupported()) { const { shadowRootStyles } = await import('@/core/shadow-root') const v3Style = await import('./comments-v3.scss').then(m => m.default) - shadowRootStyles.addStyle({ id: name, style: v3Style }) + shadowRootStyles.toggleWithComponent(metadata.name, { id: name, style: v3Style }) } else { const { shadowRootStyles } = await import('@/core/shadow-root') const firefoxStyles = require.context('./comments-v3-firefox', false, /\.scss$/) diff --git a/registry/lib/components/utils/comments/disable-search-link/index.ts b/registry/lib/components/utils/comments/disable-search-link/index.ts index 384e5b194..e69253caf 100644 --- a/registry/lib/components/utils/comments/disable-search-link/index.ts +++ b/registry/lib/components/utils/comments/disable-search-link/index.ts @@ -1,6 +1,6 @@ import { defineComponentMetadata } from '@/components/define' import { forEachCommentArea, CommentAreaV3 } from '@/components/utils/comment-apis' -import { ShadowRootEvents, shadowRootStyles } from '@/core/shadow-root' +import { ShadowRootEvents } from '@/core/shadow-root' import { preventEvent } from '@/core/utils' const name = 'disableCommentsSearchLink' @@ -14,6 +14,11 @@ export const component = defineComponentMetadata({ style: () => import('./disable-search-link.scss'), important: true, }, + { + name, + style: () => import('./disable-search-link-shadow.scss'), + shadowDom: true, + }, ], tags: [componentsTags.utils, componentsTags.style], entry: async () => { @@ -21,8 +26,6 @@ export const component = defineComponentMetadata({ forEachCommentArea(async area => { const isV3Area = area instanceof CommentAreaV3 if (isV3Area) { - const style = await import('./disable-search-link-shadow.scss').then(m => m.default) - shadowRootStyles.addStyle({ id: name, style }) area.commentAreaEntry.addEventListener( ShadowRootEvents.Updated, (e: CustomEvent) => { diff --git a/registry/lib/components/utils/dev-client/client.ts b/registry/lib/components/utils/dev-client/client.ts index 5b69dcadf..1a2aec3ed 100644 --- a/registry/lib/components/utils/dev-client/client.ts +++ b/registry/lib/components/utils/dev-client/client.ts @@ -1,7 +1,7 @@ import type { ItemStopPayload, Payload } from 'dev-tools/dev-server/payload' import { useScopedConsole } from '@/core/utils/log' import { ComponentMetadata, componentsMap } from '@/components/component' -import { loadInstantStyle, removeStyle } from '@/core/style' +import { loadInstantStyle, removeInstantStyle } from '@/core/style' import { autoUpdateOptions, getDevClientOptions } from './options' import { RefreshMethod, HotReloadMethod } from './update-method' import { monkey } from '@/core/ajax' @@ -161,10 +161,10 @@ export class DevClient extends EventTarget { } const reloadInstantStyles = () => { if (oldInstantStyles.length > 0 || newInstantStyles.length > 0) { - loadInstantStyle(newComponent) oldInstantStyles.forEach(style => { - removeStyle(style.name) + removeInstantStyle(style) }) + loadInstantStyle(newComponent) // 修改旧的引用, 否则之前设的事件监听还是用旧样式 oldComponent.instantStyles = newInstantStyles return true diff --git a/src/components/types.ts b/src/components/types.ts index 3eaee6ebc..a63f61faa 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -163,6 +163,21 @@ export type ComponentEntry, ) => T | Promise +export interface InstantStyleDefinition { + /** 样式ID */ + name: string + /** 样式内容, 可以是一个导入样式的函数 */ + style: string | (() => Promise<{ default: string }>) +} +export interface DomInstantStyleDefinition extends InstantStyleDefinition { + /** 设为 `true` 则注入到 `document.body` 末尾, 否则注入到 `document.head` 末尾 */ + important?: boolean +} +export interface ShadowDomInstantStyleDefinition extends InstantStyleDefinition { + /** 设为 `true` 则注入到 Shadow DOM 中 */ + shadowDom?: boolean +} + /** 带有函数/复杂对象的组件信息 */ export interface FunctionalMetadata { /** 主入口, 重新开启时不会再运行 */ @@ -170,14 +185,7 @@ export interface FunctionalMetadata { /** 导出小组件 */ widget?: Omit /** 首屏样式, 会尽快注入 (before DCL) */ - instantStyles?: { - /** 样式ID */ - name: string - /** 样式内容, 可以是一个导入样式的函数 */ - style: string | (() => Promise<{ default: string }>) - /** 设为`true`则注入到`document.body`末尾, 否则注入到`document.head`末尾 */ - important?: boolean - }[] + instantStyles?: (DomInstantStyleDefinition | ShadowDomInstantStyleDefinition)[] /** 重新开启时执行 */ reload?: Executable /** 关闭时执行 */ diff --git a/src/components/user-component.ts b/src/components/user-component.ts index 357aef233..2eeb2a49b 100644 --- a/src/components/user-component.ts +++ b/src/components/user-component.ts @@ -91,8 +91,8 @@ export const uninstallComponent = async (nameOrDisplayName: string) => { // 移除可能的 instantStyles const { instantStyles } = components[index] if (instantStyles) { - const { removeStyle } = await import('@/core/style') - instantStyles.forEach(s => removeStyle(s.name)) + const { removeInstantStyle } = await import('@/core/style') + instantStyles.forEach(s => removeInstantStyle(s)) } // 移除可能的 widgets componentSettings.enabled = false diff --git a/src/core/shadow-root/styles.ts b/src/core/shadow-root/styles.ts index f33615352..716dc5884 100644 --- a/src/core/shadow-root/styles.ts +++ b/src/core/shadow-root/styles.ts @@ -1,3 +1,4 @@ +import { addComponentListener, removeComponentListener } from '../settings' import { deleteValue, getRandomId } from '../utils' import { ShadowDomEntry } from './dom-entry' import { ShadowDomObserver, shadowDomObserver } from './dom-observer' @@ -64,6 +65,20 @@ export class ShadowRootStyles { this.removeEntry(id) } } + + toggleWithComponent(path: string, definition: ShadowRootStyleDefinition) { + let entry: ShadowRootStyleEntry | undefined + const handler = async (rawValue: unknown) => { + const value = Boolean(rawValue) + if (value) { + entry = await this.addStyle(definition) + } else if (entry !== undefined) { + this.removeStyle(entry.id) + } + } + addComponentListener(path, handler) + return () => removeComponentListener(path, handler) + } } export const shadowRootStyles = new ShadowRootStyles() diff --git a/src/core/style.ts b/src/core/style.ts index dc6afb9a3..777d31f5c 100644 --- a/src/core/style.ts +++ b/src/core/style.ts @@ -1,5 +1,10 @@ -import { ComponentMetadata } from '@/components/types' +import { + ComponentMetadata, + DomInstantStyleDefinition, + ShadowDomInstantStyleDefinition, +} from '@/components/types' import { contentLoaded } from './life-cycle' +import { shadowRootStyles } from './shadow-root' /** 为`