From 2eaf0fc0899ccf8df2beb557a3d8753dcdad118a Mon Sep 17 00:00:00 2001 From: Erek Speed Date: Sat, 2 Apr 2022 10:16:21 +0900 Subject: [PATCH] feat(ui): Isolate rikaikun popup from host page via shadow Dom and `all: revert` - Creates a div inside of shadow root to contain all styles and modifications. - Updates CSS to use `all: revert` instead of previous adhoc reset. - Installs CSS directly into the shadow DOM as per best practices. - Fallsback to `all: initial` for older Chrome browsers. - Removes special check for text files since chrome renders them as normal HTML pages. Fixes #221 --- extension/css/popup.css | 109 ++++++++++++++-------------- extension/rikaicontent.ts | 78 ++++++++------------ extension/test/e2e_visual_test.ts | 4 +- extension/test/rikaicontent_test.ts | 26 ------- 4 files changed, 88 insertions(+), 129 deletions(-) diff --git a/extension/css/popup.css b/extension/css/popup.css index e61effbe2..a0b45abb4 100644 --- a/extension/css/popup.css +++ b/extension/css/popup.css @@ -1,5 +1,5 @@ /* Theme definitions */ -#rikaichan-window[data-theme='blue'] { +#rikaikun-shadow[data-theme='blue'] { --primary-color: #fff; --primary-background-color: #5c73b8; --kanji-color: #b7e7ff; @@ -13,7 +13,7 @@ --data-alternate-color: #b7e7ff; } -#rikaichan-window[data-theme='lightblue'] { +#rikaikun-shadow[data-theme='lightblue'] { --primary-color: #000; --primary-background-color: #e6f4ff; --kanji-color: #3082bf; @@ -27,7 +27,7 @@ --data-alternate-color: #003d6b; } -#rikaichan-window[data-theme='black'] { +#rikaikun-shadow[data-theme='black'] { --primary-color: #fff; --primary-background-color: #000; --kanji-color: #7070e0; @@ -41,7 +41,7 @@ --data-alternate-color: #f0f0f0; } -#rikaichan-window[data-theme='yellow'] { +#rikaikun-shadow[data-theme='yellow'] { --primary-color: #000; --primary-background-color: #ffffbf; --kanji-color: #7070e0; @@ -57,32 +57,31 @@ /* End theme definitions */ -/* this is to reset almost everything under the popup to a sane value */ -#rikaichan-window, -#rikaichan-window * { - background: transparent; - border: none !important; - border-spacing: 0; - color: var(--primary-color); - font: normal 14px sans-serif; - height: auto; - letter-spacing: normal; - margin: 0; - padding: 0; - text-align: left; - text-decoration: none; - text-indent: 0; - text-transform: none; - visibility: visible; - white-space: normal; - width: auto; - word-spacing: normal; -} - -#rikaichan-window { +/* In quirks mode, table color is reset to black as per Chrome so reset + it to inherit properly here. */ +table { + color: inherit; +} + +#rikaikun-shadow { + all: initial; +} + +/* Supported starting in Chrome 84 */ +@supports (all: revert) { + #rikaikun-shadow { + all: revert; + } +} + +/* Need duplicate selector for gracefully handling reset. */ +/* stylelint-disable-next-line no-duplicate-selectors */ +#rikaikun-shadow { background: var(--primary-background-color); border: 1px solid #d0d0d0 !important; border-radius: 5px; + color: var(--primary-color); + font: normal 14px sans-serif; left: 5px; min-width: 100px; padding: 4px; @@ -94,83 +93,83 @@ /* used for word definitions */ -#rikaichan-window .w-kanji { +.w-kanji { color: var(--kanji-color); font-size: 18px; margin-right: 0.7em; } -#rikaichan-window .w-kana { +.w-kana { color: var(--kana-color); font-size: 18px; } -#rikaichan-window .w-conj { +.w-conj { color: var(--conjugation-color); font-size: 12px; } -#rikaichan-window .w-def { +.w-def { font-size: 13px; } -#rikaichan-window .w-title { +.w-title { background: var(--title-background-color); color: var(--primary-color); font-size: 10px; padding: 2px; } -#rikaichan-window .w-na-tb td { +.w-na-tb td { padding-right: 0.8em; vertical-align: top; } /* used for kanji information */ -#rikaichan-window .k-main-tb { +.k-main-tb { width: 300px; } -#rikaichan-window .k-kanji { +.k-kanji { color: var(--kanji-color); font-family: serif; font-size: 48px; } -#rikaichan-window .k-eigo { +.k-eigo { font-size: 12px; } /* kanji: readings */ -#rikaichan-window .k-yomi { +.k-yomi { color: var(--kana-color); font-size: 14px; margin: 4px 0; } -#rikaichan-window .k-yomi-hi { +.k-yomi-hi { border: 1px solid red; color: var(--kanji-readings-highlight-color); } -#rikaichan-window .k-yomi-ti { +.k-yomi-ti { color: var(--primary-color); font-size: 11px; } /* kanji radical, grade, freq, strokes box */ -#rikaichan-window .k-abox-tb { +.k-abox-tb { clear: right; float: right; margin-bottom: 4px; width: 120px; } -#rikaichan-window .k-abox-r, -#rikaichan-window .k-abox-s { +.k-abox-r, +.k-abox-s { background: var(--data-background-color); color: var(--data-color); font-size: 12px; @@ -178,8 +177,8 @@ text-align: center; } -#rikaichan-window .k-abox-g, -#rikaichan-window .k-abox-f { +.k-abox-g, +.k-abox-f { background: var(--data-background-alternate-color); color: var(--data-alternate-color); font-size: 12px; @@ -189,7 +188,7 @@ /* kanji components box */ -#rikaichan-window .k-bbox-tb { +.k-bbox-tb { clear: right; float: right; margin-bottom: 4px; @@ -197,8 +196,8 @@ } /* Two sets of colors for separating rows */ -#rikaichan-window .k-bbox-0a, -#rikaichan-window .k-bbox-0b { +.k-bbox-0a, +.k-bbox-0b { background: var(--data-background-color); color: var(--data-color); font-size: 11px; @@ -206,8 +205,8 @@ vertical-align: top; } -#rikaichan-window .k-bbox-1a, -#rikaichan-window .k-bbox-1b { +.k-bbox-1a, +.k-bbox-1b { background: var(--data-background-alternate-color); color: var(--data-alternate-color); font-size: 11px; @@ -217,23 +216,23 @@ /* kanji: misc index */ -#rikaichan-window .k-mix-tb { +.k-mix-tb { width: 100%; } -#rikaichan-window .k-mix-td0 { +.k-mix-td0 { background: var(--data-background-color); color: var(--data-color); font-size: 12px; } -#rikaichan-window .k-mix-td1 { +.k-mix-td1 { background: var(--data-background-alternate-color); color: var(--data-alternate-color); font-size: 12px; } -#rikaichan-window .q-w { +.q-w { /* unused */ border-right: 1px dotted #b0b0b0 !important; min-width: 300px; @@ -241,11 +240,11 @@ vertical-align: top; } -#rikaichan-window .q-k { +.q-k { padding: 0 2px; vertical-align: top; } -#rikaichan-window .small-info { +.small-info { font: small-caption; } diff --git a/extension/rikaicontent.ts b/extension/rikaicontent.ts index 336bf8095..890e7369b 100644 --- a/extension/rikaicontent.ts +++ b/extension/rikaicontent.ts @@ -114,17 +114,6 @@ class RcxContent { } } - getContentType(tDoc: Document) { - const m = tDoc.getElementsByTagName('meta'); - for (const i in m) { - if (m[i].httpEquiv === 'Content-Type') { - const con = m[i].content.split(';'); - return con[0]; - } - } - return null; - } - showPopup( text: string, elem?: HTMLElement, @@ -140,20 +129,16 @@ class RcxContent { let popup = topdoc.getElementById('rikaichan-window'); if (!popup) { - const css = topdoc.createElementNS( - 'http://www.w3.org/1999/xhtml', - 'link' - ); + const css = topdoc.createElement('link'); css.setAttribute('rel', 'stylesheet'); - css.setAttribute('type', 'text/css'); css.setAttribute('href', chrome.extension.getURL('css/popup.css')); - css.setAttribute('id', 'rikaichan-css'); - topdoc.getElementsByTagName('head')[0].appendChild(css); popup = topdoc.createElementNS('http://www.w3.org/1999/xhtml', 'div'); popup.setAttribute('id', 'rikaichan-window'); popup.setAttribute('lang', 'ja'); - topdoc.documentElement.appendChild(popup); + popup.attachShadow({ mode: 'open' }); + + topdoc.body.appendChild(popup); popup.addEventListener( 'dblclick', @@ -163,35 +148,30 @@ class RcxContent { }, true ); - } - popup.setAttribute('data-theme', window.rikaichan!.config.popupcolor); - - popup.style.width = 'auto'; - popup.style.height = 'auto'; - popup.style.maxWidth = looseWidth ? '' : '600px'; - if (this.getContentType(topdoc) === 'text/plain') { - const docFragment = document.createDocumentFragment(); - docFragment.appendChild( - document.createElementNS('http://www.w3.org/1999/xhtml', 'span') - ); - (docFragment.firstChild! as HTMLElement).innerHTML = text; - - while (popup.firstChild) { - popup.removeChild(popup.firstChild); - } - popup.appendChild(docFragment.firstChild!); - } else { - popup.innerHTML = text; + const shadowcontainer = topdoc.createElement('div'); + shadowcontainer.setAttribute('id', 'rikaikun-shadow'); + popup.shadowRoot!.appendChild(css); + popup.shadowRoot!.appendChild(shadowcontainer); } + const shadowcontainer = this.getRikaikunPopup(popup); + shadowcontainer.setAttribute( + 'data-theme', + window.rikaichan!.config.popupcolor + ); + + shadowcontainer.style.width = 'auto'; + shadowcontainer.style.height = 'auto'; + shadowcontainer.style.maxWidth = looseWidth ? '' : '600px'; + shadowcontainer.innerHTML = text; if (elem) { - popup.style.top = '-1000px'; - popup.style.left = '0px'; + shadowcontainer.style.top = '-1000px'; + shadowcontainer.style.left = '0px'; popup.style.display = ''; - let pW = popup.offsetWidth; - let pH = popup.offsetHeight; + let pW = shadowcontainer.offsetWidth; + let pH = shadowcontainer.offsetHeight; // guess! if (pW <= 0) { @@ -231,9 +211,9 @@ class RcxContent { y -= elem.offsetTop; } - if (x + popup.offsetWidth > window.innerWidth) { + if (x + shadowcontainer.offsetWidth > window.innerWidth) { // too much to the right, go left - x -= popup.offsetWidth + 5; + x -= shadowcontainer.offsetWidth + 5; if (x < 0) { x = 0; } @@ -279,8 +259,8 @@ class RcxContent { y += window.scrollY; } - popup.style.left = x + 'px'; - popup.style.top = y + 'px'; + shadowcontainer.style.left = x + 'px'; + shadowcontainer.style.top = y + 'px'; popup.style.display = ''; } @@ -288,10 +268,14 @@ class RcxContent { const popup = document.getElementById('rikaichan-window'); if (popup) { popup.style.display = 'none'; - popup.innerHTML = ''; + this.getRikaikunPopup(popup).innerHTML = ''; } } + private getRikaikunPopup(popup: HTMLElement): HTMLDivElement { + return popup.shadowRoot!.querySelector('#rikaikun-shadow')!; + } + isVisible() { const popup = document.getElementById('rikaichan-window'); return popup && popup.style.display !== 'none'; diff --git a/extension/test/e2e_visual_test.ts b/extension/test/e2e_visual_test.ts index 7f3c54a0e..4a4cd8853 100644 --- a/extension/test/e2e_visual_test.ts +++ b/extension/test/e2e_visual_test.ts @@ -253,7 +253,9 @@ async function takeSnapshot(name: string) { }); } await visualDiff( - document.querySelector('#rikaichan-window')!, + document + .querySelector('#rikaichan-window')! + .shadowRoot!.querySelector('#rikaikun-shadow')!, name ); } diff --git a/extension/test/rikaicontent_test.ts b/extension/test/rikaicontent_test.ts index baca58f51..16004405e 100644 --- a/extension/test/rikaicontent_test.ts +++ b/extension/test/rikaicontent_test.ts @@ -728,32 +728,6 @@ describe('RcxContent', function () { }); }); }); - - describe('showPopup', function () { - it('sets data-theme attribute of rikaikun window to config popupcolor value', function () { - rcxContent.enableTab({ popupcolor: 'redtest' } as Config); - - rcxContent.showPopup(''); - - // expect rikaikun window to have data-theme attribute set to config popupcolor value - expect( - document.querySelector('#rikaichan-window')!.dataset - .theme - ).to.equal('redtest'); - }); - - it('adds link tag pointing to "css/popup.css" to ', function () { - chrome.extension.getURL.callsFake((path: string) => { - return `http://fakebaseurl/${path}`; - }); - - rcxContent.showPopup(''); - - expect( - document.querySelector('head link#rikaichan-css')!.href - ).to.equal('http://fakebaseurl/css/popup.css'); - }); - }); }); // Required if testing downstream methods which expect a proper hover event to have