diff --git a/popup/popup.css b/popup/popup.css index 490d0d8b..60776ab5 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -1,822 +1,837 @@ -:root { - --header-width: 280px; - --checkbox-width: 24px; - --name-padding-left: 40px; - --name-padding-right: 40px; - --actions-width: 75px; - --onoffswitch-width: 60px; - --outer-padding: 9px; -} - -html { - /* Chrome 66-?? adds a gap equal to the scrollbar width, - which looks like a bug, see https://crbug.com/821143 */ - overflow: overlay; -} - -html, body { - height: min-content; - max-height: 600px; -} - -body { - width: 252px; - font-size: 12px; - font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; - margin: 0; -} - -html, body:not(.search-results-shown) { - overflow: hidden; -} - -.firefox body { - color: #000; - background-color: #fff; -} - -body > div:not(#installed):not(#message-box):not(.colorpicker-popup) { - margin-left: var(--outer-padding); - margin-right: var(--outer-padding); -} - -.firefox .chromium-only { - display: none; -} -/************ checkbox ************/ - -.style-name:hover input[type="checkbox"]:checked { - border-color: hsl(0, 0%, 32%); - background-color: hsl(0, 0%, 82%); -} - -.style-name:hover input[type="checkbox"] { - border-color: hsl(0, 0%, 32%); - background-color: hsl(0, 0%, 82%); -} - -#disable-all-wrapper input[type="checkbox"]:not(.slider):checked + .svg-icon.checked { - position: absolute; - top: 0; - left: 0; - pointer-events: none; -} - -#installed.disabled + .actions #disableAll:checked + .svg-icon.checked { - fill: hsl(0, 68%, 42%); -} - -#installed.disabled + .actions #disableAll:checked { - border-color: hsl(0, 68%, 50%); -} - -#installed.disabled + .actions #disableAll:checked:hover + .svg-icon.checked { - fill: #fff; -} - -#disableAll:hover { - border-color: hsl(0, 68%, 50%); - background-color: hsl(20, 70%, 75%); -} - -#disableAll-label:hover { - color: hsl(0, 68%, 42%); -} - -#installed.disabled + .actions #disableAll:checked:hover { - border-color: hsl(0, 50%, 56%); - background-color: hsl(0, 50%, 56%); -} - -#installed .style-name .checker, -#installed .style-name .svg-icon.checked { - position: absolute; - top: 7px; - left: var(--outer-padding); - pointer-events: none; -} - -#disable-all-wrapper { - padding: 0.3em 0 0.6em; -} - -#disable-all-wrapper .main-controls { - display: flex; - position: relative; -} - -#disable-all-wrapper .main-controls label { - padding-left: 16px; - position: relative; - transition: color .25s; - font-size: 12px; -} - -#no-styles { - padding: 4px var(--outer-padding) 6px; - font-weight: bold; -} - -#find-styles-link { - cursor: pointer; - margin-right: .5em; -} - -.checker { - display: inline; -} - -a { - color: #000; - transition: color .5s; -} - -a:hover { - color: #666; -} - -.actions > .main-controls { - padding-left: 16px; -} - -.main-controls { - display: table-cell; -} - -body > DIV:last-of-type, -body.blocked > DIV { - border-bottom: none; -} - -#installed { - border-bottom: 1px solid black; - padding-bottom: 2px; - padding-top: 2px; - max-height: 445px; - overflow-y: auto; - counter-reset: style-number; - position: relative; -} - -#installed.disabled .style-name { - text-decoration: line-through; -} - -#installed.disabled + .actions #disableAll-label { - font-weight: bold; - color: firebrick; -} - -#installed .actions { - cursor: default; -} - -#installed .actions a { - cursor: pointer; - text-decoration: none; -} - -/* entry */ -.entry { - position: relative; -} -.entry-content { - display: flex; - align-items: center; - height: 26px; - padding: 0 14px 0 0; -} - -html[style] .entry-content { - padding: 0 16px 0 0; -} - -#no-styles.entry { - padding: 0 14px; -} - -.entry .actions { - display: inline-flex; - align-items: center; -} - -.style-name { - height: 100%; - width: 100%; - line-height: 28px; - cursor: default; - font-weight: bold; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-left: 26px; - position: relative; -} - -.entry .style-name::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(to right, hsla(180, 50%, 30%, 0.2), hsla(180, 20%, 10%, 0.05) 50%, transparent); - pointer-events: none; - opacity: 0; - transition: opacity .1s; - will-change: opacity; -} - -.entry .style-name:hover::before { - opacity: 1; -} - -.entry .main-controls { - height: 100%; - display: inline-flex; - flex-grow: 1; - overflow: hidden; - align-items: center; - padding-right: 5px; -} - -.entry:nth-child(even) { - background-color: rgba(0, 0, 0, 0.05); -} - -.entry:nth-child(-n+10):before, -.entry:nth-child(11):before { - counter-increment: style-number; - content: counter(style-number); - position: absolute; - top: .9ex; - right: 5px; - color: #aaa; -} - -.entry:nth-child(11):before { - content: "0"; -} - -html[style*="border"] .entry:nth-child(-n+10):before, -html[style*="border"] .entry:nth-child(11):before { - /* the "show side borders" option adds 2px */ - right: 7px; -} - -.entry .actions { - margin-left: -1px; - margin-right: -1px; -} - -.entry .actions > * { - height: 26px; - width: 18px; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.entry .actions > .menu-button { - width: 14px; -} - -.entry .actions > a.configure { - padding-right: 2px; -} - -.entry .actions > a.configure[target="_blank"] { - width: 20px; -} - -.svg-icon.config { - height: 14px; - width: 14px; -} - -a.configure[target="_blank"] .svg-icon.config { - height: 20px; - width: 20px; - margin-top: 1px; -} - -.not-applied .checker, -.not-applied .style-name, -.not-applied .actions > * { - opacity: .2; - transition: opacity .5s ease-in-out .25s, color .5s ease-in-out .25s; -} - -.not-applied:hover .checker, -.not-applied:hover .style-name, -.not-applied:hover .actions > * { - opacity: 1; -} - -.not-applied:hover .style-name { - color: darkred; -} - -/* entry menu */ -.entry .menu { - display: flex; - flex-direction: column; - top: 100%; - width: 100%; - z-index: 1; - box-sizing: border-box; - height: 0; - transition: height .25s ease-out, opacity .5s ease-in; - overflow: hidden; - opacity: 0; -} -.entry.menu-active .menu { - height: var(--menu-height, 0px); - opacity: 1; -} -/* accessibility */ -.menu-item { - display: none; - border: none; - align-items: center; - padding: 3px 0 3px 20px; - background: none; - text-decoration: none; - flex: none; -} -.entry.menu-active .menu-item { - display: flex; -} -.entry .menu-item.delete { - cursor: pointer; -} -.entry .menu-item.delete:hover { - color: #000; -} -.entry .menu-item > span { - margin-top: 1px; -} -.entry .menu-item:hover, -.entry .menu-item:active { - background-color: rgba(0, 0, 0, 0.1); - transition: background-color .25s; -} -.entry .menu-icon { - width: 26px; - flex-shrink: 0; -} -.entry .menu-icon > * { - display: block; - margin: 0 auto; -} -.entry .menu-item.disabled { - opacity: 0.5; - background-color: transparent; - cursor: help; -} - -/* checkbox */ -.checkbox-container { - position: relative; - display: inline-block; - width: 12px; - height: 12px; -} - -.regexp-problem-indicator { - background-color: #d00; - width: 14px; - height: 14px; - line-height: 15px; - border-radius: 8px; - margin-right: 6px; - margin-left: 6px; - text-align: center; - color: white; - font-weight: bold; - box-sizing: border-box; - cursor: pointer; - font-size: 90%; - display: none; -} -.regexp-partial .regexp-problem-indicator { - display: block; -} - -.regexp-partial .actions, -.regexp-invalid .actions { - order: 2; -} - -#regexp-explanation { - position: fixed; - background-color: white; - top: 50%; - transform: translateY(-50%); - left: 0; - right: 0; - padding: .5rem; - font-size: 90%; - border-top: 2px solid black; - border-bottom: 2px solid black; - box-shadow: 0 0 100px black; - display: flex; - flex-direction: column; - z-index: 999999; -} - -#regexp-explanation > div { - display: none; - list-style-type: none; - padding: 0; - margin: 0; -} - -.regexp-partial #regexp-partial, -.regexp-invalid #regexp-invalid { - display: block; -} - -#regexp-explanation > div:not(:last-child) { - margin-bottom: .5rem; -} - -.svg-icon { - pointer-events: none; - transition: fill .5s; - width: 14px; - height: 16px; - fill: #666; -} - -a:hover .svg-icon { - fill: #000; -} - -body > .actions { - margin-top: 0.5em; - /* raise the actions above the hotkey-info */ - position: relative; - z-index: 4; -} - -.actions > div:not(:last-child):not(#disable-all-wrapper), -.actions > .main-controls > div:not(:last-child) { - margin-bottom: 0.75em; -} - -.actions input, -.actions label { - vertical-align: middle; -} - -body.blocked #installed > *, -body.blocked .actions > .main-controls { - display: none; -} - -/* Never shown, but can be enabled with a style */ - -.entry .actions > .enable, -.entry .actions > .disable { - display: none; -} - -/* 'New style' links */ - -#write-style { - display: flex; - flex-direction: row; - flex-wrap: wrap; -} - -#write-style-for { - margin-right: .6ex -} - -.write-style-link { - margin-left: .6ex -} - -.write-style-link::before { - content: "\00ad"; /* "soft" hyphen */ -} - -#match { - overflow-wrap: break-word; - display: block; - flex-grow: 9; - min-width: 200px; -} - -/* "breadcrumbs" 'new style' links */ -.breadcrumbs > .write-style-link { - margin-left: 0 -} - -.breadcrumbs:hover a { - color: #bbb; - text-decoration: none -} - -/* "dot" after each subdomain name */ -.breadcrumbs > .write-style-link[subdomain]::after { - content: "." -} - -/* no "dot" after top-level domain */ -.breadcrumbs > .write-style-link:nth-last-child(2)::after { - content: none -} - -/* "forward slash" before path ("this URL") */ -.breadcrumbs > .write-style-link:last-child::before { - content: "\200b/" -} - -.breadcrumbs > .write-style-link:last-child:first-child::before, -.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before { - content: none -} - -/* suppress TLD-only link */ -.breadcrumbs > .write-style-link[subdomain=""] { - display: none -} - -/* :hover style */ -.breadcrumbs.url\(\) > .write-style-link, - -/* :hover or :focus on "this URL" sets class="url()" */ -.breadcrumbs > .write-style-link:hover, -.breadcrumbs > .write-style-link:focus, -.breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain], -.breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] { - color: inherit; - text-decoration: underline; -} - -/* action buttons */ - -#popup-options { - display: flex; - flex-direction: row; - padding: var(--outer-padding) 1px; -} - -#popup-options button { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding: 2px 4px; - margin-right: 4px; - /* several languages have labels of wildly different lengths so we try to maintain the proportion */ - flex: 1 1 auto; - min-width: 2em; -} - -#popup-options button:last-child { - margin-right: 0; -} - -/* confirm */ - -#confirm { - align-items: center; - justify-content: center; - z-index: 2147483647; - display: none; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - margin: 0 !important; - box-sizing: border-box; - background-color: rgba(0, 0, 0, 0.4); - animation: lights-off .5s cubic-bezier(.03, .67, .08, .94); - animation-fill-mode: both; -} - -#confirm.lights-on { - animation: lights-on .25s ease-in-out; - animation-fill-mode: both; -} - -#confirm.lights-on, -#confirm.lights-on > div { - display: none; -} - -#confirm[data-display=true] { - display: flex; -} - -#confirm > div { - width: 80%; - max-height: 80%; - min-height: 6em; - padding: 1em; - background-color: #fff; - display: flex; - flex-direction: column; - border: solid 2px rgba(0, 0, 0, 0.5); -} - -#confirm > div > *:not(:last-child) { - padding-bottom: .5em; -} - -#confirm > div > div { - text-align: center; -} - -.non-windows #confirm > div > div { - direction: rtl; - text-align: right; -} - -#confirm > button { - /* add a gap between buttons both for horizontal - or vertical (when the label is wide) layout */ - margin: 0 .25em .25em 0; -} - -.unreachable .entry { - opacity: .25; -} - -.unreachable .blocked-info { - border-bottom: 1px solid black; -} - -.blocked-info { - hyphens: none; - word-wrap: break-word; -} - -.blocked-info label { - padding: 5px 0; - display: block; - font-weight: bold; -} - -.blocked-info p { - padding: 1px 0 var(--outer-padding); - display: block; - font-size: 90%; - margin: 0; -} - -/******************************************/ - -#hotkey-info { - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 16px; - cursor: help; - margin: 0; - padding: 0; - z-index: 1; - hyphens: auto; -} - -#hotkey-info:not([data-active]) > * { - display: none; -} - -#hotkey-info[data-active] { - position: fixed; - left: 6ex; - bottom: unset; - width: auto; - cursor: auto; - display: flex; - flex-direction: column; - border-left: 2px solid white; - box-shadow: 0 0 90px rgba(0, 0, 0, .5); - z-index: 5; -} - -#hotkey-info div:first-child { - flex-grow: 1; - padding: 0 1em; - font-size: 11px; - overflow-y: auto; -} - -#hotkey-info div { - padding: 1em; - border-top: 1px solid #ddd; - background-color: white; -} - -#hotkey-info div:last-child { - box-shadow: 0 0 90px rgba(0, 0, 0, .25); - position: relative; -} - -#hotkey-info p { - text-indent: -3px; -} - -#hotkey-info p:last-child { - margin-bottom: 0; -} - -#hotkey-info mark { - display: inline-block; - background: linear-gradient(#ccc, #fff); - padding: 1px 6px 0; - margin: 2px; - border: 1px solid white; - border-radius: 4px; - box-shadow: 1px 1px 4px rgba(0, 0, 0, .3); - font-weight: bold; - white-space: nowrap; -} - -/******************************************/ - -@keyframes lights-off { - from { - background-color: transparent; - } - to { - background-color: rgba(0, 0, 0, 0.4); - } -} - -@keyframes lights-on { - from { - background-color: rgba(0, 0, 0, 0.4); - } - to { - background-color: transparent; - } -} - -/* Popup adjustments for common zoom levels */ - -@media (-webkit-min-device-pixel-ratio: 1.05) { - #installed { - max-height: 420px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.1) { - #installed { - max-height: 393px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.15) { - #installed { - max-height: 371px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.2) { - #installed { - max-height: 348px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.25) { - #installed { - max-height: 326px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.30) { - #installed { - max-height: 306px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.35) { - #installed { - max-height: 288px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.4) { - #installed { - max-height: 271px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.45) { - #installed { - max-height: 256px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.5) { - #installed { - max-height: 244px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.55) { - #installed { - max-height: 199px; - } -} -@media (-webkit-min-device-pixel-ratio: 1.75) { - #installed { - max-height: 144px; - } -} +:root { + --header-width: 280px; + --checkbox-width: 24px; + --name-padding-left: 40px; + --name-padding-right: 40px; + --actions-width: 75px; + --onoffswitch-width: 60px; + --outer-padding: 9px; +} + +html { + /* Chrome 66-?? adds a gap equal to the scrollbar width, + which looks like a bug, see https://crbug.com/821143 */ + overflow: overlay; +} + +html, body { + height: min-content; + max-height: 600px; +} + +body { + width: 252px; + font-size: 12px; + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + margin: 0; +} + +.firefox body { + color: #000; + background-color: #fff; +} + +body > div:not(#installed):not(#message-box):not(.colorpicker-popup) { + margin-left: var(--outer-padding); + margin-right: var(--outer-padding); +} + +.firefox .chromium-only { + display: none; +} +/************ checkbox ************/ + +.style-name:hover input[type="checkbox"]:checked { + border-color: hsl(0, 0%, 32%); + background-color: hsl(0, 0%, 82%); +} + +.style-name:hover input[type="checkbox"] { + border-color: hsl(0, 0%, 32%); + background-color: hsl(0, 0%, 82%); +} + +#disable-all-wrapper input[type="checkbox"]:not(.slider):checked + .svg-icon.checked { + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} + +#installed.disabled + .actions #disableAll:checked + .svg-icon.checked { + fill: hsl(0, 68%, 42%); +} + +#installed.disabled + .actions #disableAll:checked { + border-color: hsl(0, 68%, 50%); +} + +#installed.disabled + .actions #disableAll:checked:hover + .svg-icon.checked { + fill: #fff; +} + +#disableAll:hover { + border-color: hsl(0, 68%, 50%); + background-color: hsl(20, 70%, 75%); +} + +#disableAll-label:hover { + color: hsl(0, 68%, 42%); +} + +#installed.disabled + .actions #disableAll:checked:hover { + border-color: hsl(0, 50%, 56%); + background-color: hsl(0, 50%, 56%); +} + +#installed .style-name .checker, +#installed .style-name .svg-icon.checked { + position: absolute; + top: 7px; + left: var(--outer-padding); + pointer-events: none; +} + +#disable-all-wrapper { + padding: 0.3em 0 0.6em; +} + +#disable-all-wrapper .main-controls { + display: flex; + position: relative; +} + +#disable-all-wrapper .main-controls label { + padding-left: 16px; + position: relative; + transition: color .25s; + font-size: 12px; +} + +#no-styles { + padding: 4px var(--outer-padding) 6px; + font-weight: bold; +} + +#find-styles-link { + cursor: pointer; + margin-right: .5em; +} + +.checker { + display: inline; +} + +a { + color: #000; + transition: color .5s; +} + +a:hover { + color: #666; +} + +.actions > .main-controls { + padding-left: 16px; +} + +.main-controls { + display: table-cell; +} + +body > DIV:last-of-type, +body.blocked > DIV { + border-bottom: none; +} + +#installed { + border-bottom: 1px solid black; + padding-bottom: 2px; + padding-top: 2px; + max-height: 445px; + overflow-y: auto; + counter-reset: style-number; + position: relative; +} + +#installed.disabled .style-name { + text-decoration: line-through; +} + +#installed.disabled + .actions #disableAll-label { + font-weight: bold; + color: firebrick; +} + +#installed .actions { + cursor: default; +} + +#installed .actions a { + cursor: pointer; + text-decoration: none; +} + +/* entry */ +.entry { + position: relative; +} +.entry-content { + display: flex; + align-items: center; + height: 26px; + padding: 0 14px 0 0; +} + +html[style] .entry-content { + padding: 0 16px 0 0; +} + +#no-styles.entry { + padding: 0 14px; +} + +.entry .actions { + display: inline-flex; + align-items: center; +} + +.style-name { + height: 100%; + width: 100%; + line-height: 28px; + cursor: default; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-left: 26px; + position: relative; +} + +.entry .style-name::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(to right, hsla(180, 50%, 30%, 0.2), hsla(180, 20%, 10%, 0.05) 50%, transparent); + pointer-events: none; + opacity: 0; + transition: opacity .1s; + will-change: opacity; +} + +.entry .style-name:hover::before { + opacity: 1; +} + +.entry .main-controls { + height: 100%; + display: inline-flex; + flex-grow: 1; + overflow: hidden; + align-items: center; + padding-right: 5px; +} + +.entry:nth-child(even) { + background-color: rgba(0, 0, 0, 0.05); +} + +.entry:nth-child(-n+10):before, +.entry:nth-child(11):before { + counter-increment: style-number; + content: counter(style-number); + position: absolute; + top: .9ex; + right: 5px; + color: #aaa; +} + +.entry:nth-child(11):before { + content: "0"; +} + +html[style*="border"] .entry:nth-child(-n+10):before, +html[style*="border"] .entry:nth-child(11):before { + /* the "show side borders" option adds 2px */ + right: 7px; +} + +.entry .actions { + margin-left: -1px; + margin-right: -1px; +} + +.entry .actions > * { + height: 26px; + width: 18px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.entry .actions > .menu-button { + width: 14px; +} + +.entry .actions > a.configure { + padding-right: 2px; +} + +.entry .actions > a.configure[target="_blank"] { + width: 20px; +} + +.svg-icon.config { + height: 14px; + width: 14px; +} + +a.configure[target="_blank"] .svg-icon.config { + height: 20px; + width: 20px; + margin-top: 1px; +} + +.not-applied .checker, +.not-applied .style-name, +.not-applied .actions > * { + opacity: .2; + transition: opacity .5s ease-in-out .25s, color .5s ease-in-out .25s; +} + +.not-applied:hover .checker, +.not-applied:hover .style-name, +.not-applied:hover .actions > * { + opacity: 1; +} + +.not-applied:hover .style-name { + color: darkred; +} + +/* entry menu */ +.entry .menu { + display: none; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + align-items: center; + justify-content: center; + z-index: 2147483647; + box-sizing: border-box; + overflow: hidden; + background-color: rgba(0, 0, 0, 0.4); + outline: none; + animation: lights-off .5s cubic-bezier(.03, .67, .08, .94); + animation-fill-mode: both; +} +.menu-title, +#confirm > div > b { + padding-bottom: .5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.menu-items-wrapper { + width: 80%; + max-height: 80%; + min-height: 6em; + padding: 1em; + display: flex; + position: relative; + flex-direction: column; + background-color: #fff; + border: solid 2px rgba(0, 0, 0, 0.5); +} +.menu-buttons-wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 1em 0 0; +} +.menu-buttons-wrapper button { + margin: 0 .25em; +} +.menu-item { + display: flex; + border: none; + align-items: center; + padding: 3px 0; + background: none; + text-decoration: none; + flex: none; +} +.entry .menu-item > span { + margin: 1px 0 -1px; +} +.entry .menu-item:hover, +.entry .menu-item:active { + background-color: rgba(0, 0, 0, 0.1); + transition: background-color .25s; +} +.entry .menu-icon { + width: 26px; + flex-shrink: 0; +} +.entry .menu-icon > * { + display: block; + margin: 0 auto; +} + +/* checkbox */ +.checkbox-container { + position: relative; + display: inline-block; + width: 12px; + height: 12px; +} + +.regexp-problem-indicator { + background-color: #d00; + width: 14px; + height: 14px; + line-height: 15px; + border-radius: 8px; + margin-right: 6px; + margin-left: 6px; + text-align: center; + color: white; + font-weight: bold; + box-sizing: border-box; + cursor: pointer; + font-size: 90%; + display: none; +} +.regexp-partial .regexp-problem-indicator { + display: block; +} + +.regexp-partial .actions, +.regexp-invalid .actions { + order: 2; +} + +#regexp-explanation { + position: fixed; + background-color: white; + top: 50%; + transform: translateY(-50%); + left: 0; + right: 0; + padding: .5rem; + font-size: 90%; + border-top: 2px solid black; + border-bottom: 2px solid black; + box-shadow: 0 0 100px black; + display: flex; + flex-direction: column; + z-index: 999999; +} + +#regexp-explanation > div { + display: none; + list-style-type: none; + padding: 0; + margin: 0; +} + +.regexp-partial #regexp-partial, +.regexp-invalid #regexp-invalid { + display: block; +} + +#regexp-explanation > div:not(:last-child) { + margin-bottom: .5rem; +} + +.svg-icon { + pointer-events: none; + transition: fill .5s; + width: 14px; + height: 16px; + fill: #666; +} + +a:hover .svg-icon { + fill: #000; +} + +body > .actions { + margin-top: 0.5em; + /* raise the actions above the hotkey-info */ + position: relative; + z-index: 4; +} + +.actions > div:not(:last-child):not(#disable-all-wrapper), +.actions > .main-controls > div:not(:last-child) { + margin-bottom: 0.75em; +} + +.actions input, +.actions label { + vertical-align: middle; +} + +body.blocked #installed > *, +body.blocked .actions > .main-controls { + display: none; +} + +/* Never shown, but can be enabled with a style */ + +.entry .actions > .enable, +.entry .actions > .disable { + display: none; +} + +/* 'New style' links */ + +#write-style { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +#write-style-for { + margin-right: .6ex +} + +.write-style-link { + margin-left: .6ex +} + +.write-style-link::before { + content: "\00ad"; /* "soft" hyphen */ +} + +#match { + overflow-wrap: break-word; + display: block; + flex-grow: 9; + min-width: 200px; +} + +/* "breadcrumbs" 'new style' links */ +.breadcrumbs > .write-style-link { + margin-left: 0 +} + +.breadcrumbs:hover a { + color: #bbb; + text-decoration: none +} + +/* "dot" after each subdomain name */ +.breadcrumbs > .write-style-link[subdomain]::after { + content: "." +} + +/* no "dot" after top-level domain */ +.breadcrumbs > .write-style-link:nth-last-child(2)::after { + content: none +} + +/* "forward slash" before path ("this URL") */ +.breadcrumbs > .write-style-link:last-child::before { + content: "\200b/" +} + +.breadcrumbs > .write-style-link:last-child:first-child::before, +.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before { + content: none +} + +/* suppress TLD-only link */ +.breadcrumbs > .write-style-link[subdomain=""] { + display: none +} + +/* :hover style */ +.breadcrumbs.url\(\) > .write-style-link, + +/* :hover or :focus on "this URL" sets class="url()" */ +.breadcrumbs > .write-style-link:hover, +.breadcrumbs > .write-style-link:focus, +.breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain], +.breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] { + color: inherit; + text-decoration: underline; +} + +/* action buttons */ + +#popup-options { + display: flex; + flex-direction: row; + padding: var(--outer-padding) 1px; +} + +#popup-options button { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 2px 4px; + margin-right: 4px; + /* several languages have labels of wildly different lengths so we try to maintain the proportion */ + flex: 1 1 auto; + min-width: 2em; +} + +#popup-options button:last-child { + margin-right: 0; +} + +/* confirm */ + +#confirm { + align-items: center; + justify-content: center; + z-index: 2147483647; + display: none; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + margin: 0 !important; + box-sizing: border-box; + background-color: rgba(0, 0, 0, 0.4); +} + +#confirm.lights-on, +.menu.lights-on { + animation: lights-on .25s ease-in-out; + animation-fill-mode: both; +} + +#confirm.lights-on, +#confirm.lights-on > div, +.menu.lights-on > div { + display: none; +} + +#confirm[data-display=true], +.menu[data-display=true] { + display: flex; +} + +#confirm[data-display=true] + #installed .menu[data-display=true] { + opacity: 0; + pointer-events: none; +} + +#confirm > div { + width: 80%; + max-height: 80%; + min-height: 6em; + padding: 1em; + background-color: #fff; + display: flex; + flex-direction: column; + border: solid 2px rgba(0, 0, 0, 0.5); +} + +#confirm > div > *:not(:last-child) { + padding-bottom: .5em; +} + +#confirm > div > div { + text-align: center; +} + +.non-windows #confirm > div > div { + direction: rtl; + text-align: right; +} + +#confirm > div > div button { + /* add a gap between buttons both for horizontal + or vertical (when the label is wide) layout */ + margin: 0 .25em .25em 0; +} + +.unreachable .entry { + opacity: .25; +} + +.unreachable .blocked-info { + border-bottom: 1px solid black; +} + +.blocked-info { + hyphens: none; + word-wrap: break-word; +} + +.blocked-info label { + padding: 5px 0; + display: block; + font-weight: bold; +} + +.blocked-info p { + padding: 1px 0 var(--outer-padding); + display: block; + font-size: 90%; + margin: 0; +} + +/******************************************/ + +#hotkey-info { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 16px; + cursor: help; + margin: 0; + padding: 0; + z-index: 1; + hyphens: auto; +} + +#hotkey-info:not([data-active]) > * { + display: none; +} + +#hotkey-info[data-active] { + position: fixed; + left: 6ex; + bottom: unset; + width: auto; + cursor: auto; + display: flex; + flex-direction: column; + border-left: 2px solid white; + box-shadow: 0 0 90px rgba(0, 0, 0, .5); + z-index: 5; +} + +#hotkey-info div:first-child { + flex-grow: 1; + padding: 0 1em; + font-size: 11px; + overflow-y: auto; +} + +#hotkey-info div { + padding: 1em; + border-top: 1px solid #ddd; + background-color: white; +} + +#hotkey-info div:last-child { + box-shadow: 0 0 90px rgba(0, 0, 0, .25); + position: relative; +} + +#hotkey-info p { + text-indent: -3px; +} + +#hotkey-info p:last-child { + margin-bottom: 0; +} + +#hotkey-info mark { + display: inline-block; + background: linear-gradient(#ccc, #fff); + padding: 1px 6px 0; + margin: 2px; + border: 1px solid white; + border-radius: 4px; + box-shadow: 1px 1px 4px rgba(0, 0, 0, .3); + font-weight: bold; + white-space: nowrap; +} + +/******************************************/ + +@keyframes lights-off { + from { + background-color: transparent; + } + to { + background-color: rgba(0, 0, 0, 0.4); + } +} + +@keyframes lights-on { + from { + background-color: rgba(0, 0, 0, 0.4); + } + to { + background-color: transparent; + } +} + +/* Popup adjustments for common zoom levels */ + +@media (-webkit-min-device-pixel-ratio: 1.05) { + #installed { + max-height: 420px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.1) { + #installed { + max-height: 393px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.15) { + #installed { + max-height: 371px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.2) { + #installed { + max-height: 348px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.25) { + #installed { + max-height: 326px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.30) { + #installed { + max-height: 306px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.35) { + #installed { + max-height: 288px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.4) { + #installed { + max-height: 271px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.45) { + #installed { + max-height: 256px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.5) { + #installed { + max-height: 244px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.55) { + #installed { + max-height: 199px; + } +} +@media (-webkit-min-device-pixel-ratio: 1.75) { + #installed { + max-height: 144px; + } +} diff --git a/popup/popup.js b/popup/popup.js index 0adc0337..540be167 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -1,591 +1,647 @@ -/* global configDialog hotkeys onTabReady msg - getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs CHROME - setupLivePrefs template t $create tWordBreak animateElement - tryJSONparse debounce */ - -'use strict'; - -let installed; -let tabURL; -const handleEvent = {}; - -const ENTRY_ID_PREFIX_RAW = 'style-'; -const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW; - -toggleSideBorders(); - -getActiveTab() - .then(tab => - FIREFOX && tab.url === 'about:blank' && tab.status === 'loading' - ? getTabRealURLFirefox(tab) - : getTabRealURL(tab) - ) - .then(url => Promise.all([ - (tabURL = URLS.supported(url) ? url : '') && - API.getStylesByUrl(tabURL), - onDOMready().then(initPopup), - ])) - .then(([results]) => { - if (!results) { - // unsupported URL; - return; - } - showStyles(results.map(r => Object.assign(r.data, r))); - }) - .catch(console.error); - -msg.onExtension(onRuntimeMessage); - -prefs.subscribe(['popup.stylesFirst'], (key, stylesFirst) => { - const actions = $('body > .actions'); - const before = stylesFirst ? actions : actions.nextSibling; - document.body.insertBefore(installed, before); -}); -prefs.subscribe(['popupWidth'], (key, value) => setPopupWidth(value)); -prefs.subscribe(['popup.borders'], (key, value) => toggleSideBorders(value)); - -function onRuntimeMessage(msg) { - switch (msg.method) { - case 'styleAdded': - case 'styleUpdated': - if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return; - handleUpdate(msg); - break; - case 'styleDeleted': - handleDelete(msg.style.id); - break; - } - dispatchEvent(new CustomEvent(msg.method, {detail: msg})); -} - - -function setPopupWidth(width = prefs.get('popupWidth')) { - document.body.style.width = - Math.max(200, Math.min(800, width)) + 'px'; -} - - -function toggleSideBorders(state = prefs.get('popup.borders')) { - // runs before is parsed - const style = document.documentElement.style; - if (CHROME >= 3167 && state) { - style.cssText += - 'border-left: 2px solid white !important;' + - 'border-right: 2px solid white !important;'; - } else if (style.cssText) { - style.borderLeft = style.borderRight = ''; - } -} - - -function initPopup() { - installed = $('#installed'); - - setPopupWidth(); - - // action buttons - $('#disableAll').onchange = function () { - installed.classList.toggle('disabled', this.checked); - }; - setupLivePrefs(); - - Object.assign($('#popup-manage-button'), { - onclick: handleEvent.openManager, - onmouseup: handleEvent.openManager, - oncontextmenu: handleEvent.openManager, - }); - - $('#popup-options-button').onclick = () => { - chrome.runtime.openOptionsPage(); - window.close(); - }; - - $('#popup-wiki-button').onclick = handleEvent.openURLandHide; - - if (!prefs.get('popup.stylesFirst')) { - document.body.insertBefore( - $('body > .actions'), - installed); - } - - if (!tabURL) { - document.body.classList.add('blocked'); - document.body.insertBefore(template.unavailableInfo, document.body.firstChild); - return; - } - - getActiveTab().then(function ping(tab, retryCountdown = 10) { - msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0}) - .catch(() => false) - .then(pong => { - if (pong) { - return; - } - // FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand - // so we'll wait a bit to handle popup being invoked right after switching - if (retryCountdown > 0 && ( - tab.status !== 'complete' || - FIREFOX && tab.url === 'about:blank')) { - setTimeout(ping, 100, tab, --retryCountdown); - return; - } - const info = template.unreachableInfo; - if (!FIREFOX) { - // Chrome "Allow access to file URLs" in chrome://extensions message - info.appendChild($create('p', t('unreachableFileHint'))); - } - if (FIREFOX && tabURL.startsWith(URLS.browserWebStore)) { - $('label', info).textContent = t('unreachableAMO'); - const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) + - (FIREFOX < 60 ? '' : '\n' + t('unreachableAMOHintNewFF')); - const renderToken = s => s[0] === '<' ? $create('b', tWordBreak(s.slice(1, -1))) : s; - const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken)); - const noteNode = $create('fragment', note.split('\n').map(renderLine)); - info.appendChild(noteNode); - } - // Inaccessible locally hosted file type, e.g. JSON, PDF, etc. - if (tabURL.length - tabURL.lastIndexOf('.') <= 5) { - info.appendChild($create('p', t('InaccessibleFileHint'))); - } - document.body.classList.add('unreachable'); - document.body.insertBefore(info, document.body.firstChild); - }); - }); - - // Write new style links - const writeStyle = $('#write-style'); - const matchTargets = document.createElement('span'); - const matchWrapper = document.createElement('span'); - matchWrapper.id = 'match'; - matchWrapper.appendChild(matchTargets); - - // For this URL - const urlLink = template.writeStyle.cloneNode(true); - Object.assign(urlLink, { - href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL), - title: `url-prefix("${tabURL}")`, - textContent: prefs.get('popup.breadcrumbs.usePath') - ? new URL(tabURL).pathname.slice(1) - // this URL - : t('writeStyleForURL').replace(/ /g, '\u00a0'), - onclick: handleEvent.openLink, - }); - if (prefs.get('popup.breadcrumbs')) { - urlLink.onmouseenter = - urlLink.onfocus = () => urlLink.parentNode.classList.add('url()'); - urlLink.onmouseleave = - urlLink.onblur = () => urlLink.parentNode.classList.remove('url()'); - } - matchTargets.appendChild(urlLink); - - // For domain - const domains = getDomains(tabURL); - for (const domain of domains) { - const numParts = domain.length - domain.replace(/\./g, '').length + 1; - // Don't include TLD - if (domains.length > 1 && numParts === 1) { - continue; - } - const domainLink = template.writeStyle.cloneNode(true); - Object.assign(domainLink, { - href: 'edit.html?domain=' + encodeURIComponent(domain), - textContent: numParts > 2 ? domain.split('.')[0] : domain, - title: `domain("${domain}")`, - onclick: handleEvent.openLink, - }); - domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : ''); - matchTargets.appendChild(domainLink); - } - - if (prefs.get('popup.breadcrumbs')) { - matchTargets.classList.add('breadcrumbs'); - matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild)); - } - writeStyle.appendChild(matchWrapper); - - function getDomains(url) { - let d = /.*?:\/*([^/:]+)|$/.exec(url)[1]; - if (!d || url.startsWith('file:')) { - return []; - } - const domains = [d]; - while (d.indexOf('.') !== -1) { - d = d.substring(d.indexOf('.') + 1); - domains.push(d); - } - return domains; - } -} - - -function sortStyles(entries) { - const enabledFirst = prefs.get('popup.enabledFirst'); - entries.sort((a, b) => - enabledFirst && a.styleMeta.enabled !== b.styleMeta.enabled ? - (a.styleMeta.enabled ? -1 : 1) : - a.styleMeta.name.localeCompare(b.styleMeta.name) - ); -} - -function showStyles(styles) { - if (!styles) { - return; - } - if (!styles.length) { - installed.appendChild(template.noStyles.cloneNode(true)); - window.dispatchEvent(new Event('showStyles:done')); - return; - } - const entries = styles.map(createStyleElement); - sortStyles(entries); - entries.forEach(e => installed.appendChild(e)); - window.dispatchEvent(new Event('showStyles:done')); -} - -function sortStylesInPlace() { - if (!prefs.get('popup.autoResort')) { - return; - } - const entries = $$('.entry', installed); - if (!entries.length) { - return; - } - sortStyles(entries); - entries.forEach(e => installed.appendChild(e)); -} - - -function createStyleElement(style) { - let entry = $(ENTRY_ID_PREFIX + style.id); - if (!entry) { - entry = template.style.cloneNode(true); - entry.setAttribute('style-id', style.id); - Object.assign(entry, { - id: ENTRY_ID_PREFIX_RAW + style.id, - styleId: style.id, - styleIsUsercss: Boolean(style.usercssData), - onmousedown: handleEvent.maybeEdit, - styleMeta: style - }); - const checkbox = $('.checker', entry); - Object.assign(checkbox, { - id: ENTRY_ID_PREFIX_RAW + style.id, - // title: t('exclusionsPopupTip'), - onclick: handleEvent.toggle, - // oncontextmenu: handleEvent.openExcludeMenu - }); - const editLink = $('.style-edit-link', entry); - Object.assign(editLink, { - href: editLink.getAttribute('href') + style.id, - onclick: handleEvent.openLink, - }); - const styleName = $('.style-name', entry); - Object.assign(styleName, { - htmlFor: ENTRY_ID_PREFIX_RAW + style.id, - onclick: handleEvent.name, - }); - styleName.checkbox = checkbox; - styleName.appendChild(document.createTextNode(' ')); - - const config = $('.configure', entry); - config.onclick = handleEvent.configure; - if (!style.usercssData) { - if (style.updateUrl && style.updateUrl.includes('?') && style.url) { - config.href = style.url; - config.target = '_blank'; - config.title = t('configureStyleOnHomepage'); - config.dataset.sendMessage = JSON.stringify({method: 'openSettings'}); - $('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso'; - } else { - config.classList.add('hidden'); - } - } else if (Object.keys(style.usercssData.vars || {}).length === 0) { - config.classList.add('hidden'); - } - - $('.delete', entry).onclick = handleEvent.delete; - - const indicator = template.regexpProblemIndicator.cloneNode(true); - indicator.appendChild(document.createTextNode('!')); - indicator.onclick = handleEvent.indicator; - $('.main-controls', entry).appendChild(indicator); - - $('.menu-button', entry).onclick = handleEvent.toggleMenu; - - $('.exclude-by-domain-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'domain'); - $('.exclude-by-url-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'url'); - } - - style = Object.assign(entry.styleMeta, style); - - entry.classList.toggle('disabled', !style.enabled); - entry.classList.toggle('enabled', style.enabled); - $('.checker', entry).checked = style.enabled; - - const styleName = $('.style-name', entry); - styleName.lastChild.textContent = style.name; - setTimeout(() => { - styleName.title = entry.styleMeta.sloppy ? - t('styleNotAppliedRegexpProblemTooltip') : - styleName.scrollWidth > styleName.clientWidth + 1 ? - styleName.textContent : ''; - }); - - entry.classList.toggle('not-applied', style.excluded || style.sloppy); - entry.classList.toggle('regexp-partial', style.sloppy); - - $('.exclude-by-domain-checkbox', entry).checked = styleExcluded(style, 'domain'); - $('.exclude-by-url-checkbox', entry).checked = styleExcluded(style, 'url'); - - $('.exclude-by-domain', entry).title = getExcludeRule('domain'); - $('.exclude-by-url', entry).title = getExcludeRule('url'); - - return entry; -} - -function styleExcluded({exclusions}, type) { - if (!exclusions) { - return false; - } - const rule = getExcludeRule(type); - return exclusions.includes(rule); -} - -function getExcludeRule(type) { - const u = new URL(tabURL); - if (type === 'domain') { - return u.origin + '/*'; - } - // current page - return escapeGlob(u.origin + u.pathname); -} - -function escapeGlob(text) { - return text.replace(/\*/g, '\\*'); -} - -Object.assign(handleEvent, { - - getClickedStyleId(event) { - return (handleEvent.getClickedStyleElement(event) || {}).styleId; - }, - - getClickedStyleElement(event) { - return event.target.closest('.entry'); - }, - - name(event) { - this.checkbox.dispatchEvent(new MouseEvent('click')); - event.preventDefault(); - }, - - toggle(event) { - // when fired on checkbox, prevent the parent label from seeing the event, see #501 - event.stopPropagation(); - API - .toggleStyle(handleEvent.getClickedStyleId(event), this.checked) - .then(sortStylesInPlace); - }, - - toggleExclude(event, type) { - const entry = handleEvent.getClickedStyleElement(event); - if (event.target.checked) { - API.addExclusion(entry.styleMeta.id, getExcludeRule(type)); - } else { - API.removeExclusion(entry.styleMeta.id, getExcludeRule(type)); - } - }, - - toggleMenu(event) { - const entry = handleEvent.getClickedStyleElement(event); - entry.classList.toggle('menu-active'); - const menu = entry.querySelector('.menu'); - menu.style.setProperty('--menu-height', menu.scrollHeight + 'px'); - event.preventDefault(); - }, - - delete(event) { - const entry = handleEvent.getClickedStyleElement(event); - const id = entry.styleId; - const box = $('#confirm'); - const cancel = $('[data-cmd="cancel"]'); - box.dataset.display = true; - box.style.cssText = ''; - $('b', box).textContent = $('.style-name', entry).textContent; - $('[data-cmd="ok"]', box).focus(); - $('[data-cmd="ok"]', box).onclick = () => confirm(true); - $('[data-cmd="cancel"]', box).onclick = () => confirm(false); - window.onkeydown = event => { - const keyCode = event.keyCode || event.which; - if (document.activeElement !== cancel && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey - && (keyCode === 13 || keyCode === 27)) { - event.preventDefault(); - confirm(keyCode === 13); - } - }; - function confirm(ok) { - window.onkeydown = null; - animateElement(box, { - className: 'lights-on', - onComplete: () => (box.dataset.display = false), - }); - if (ok) API.deleteStyle(id); - } - }, - - configure(event) { - const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event); - if (styleIsUsercss) { - API.getStyle(styleId, true).then(style => { - hotkeys.setState(false); - configDialog(style).then(() => { - hotkeys.setState(true); - }); - }); - } else { - handleEvent.openURLandHide.call(this, event); - } - }, - - indicator(event) { - const entry = handleEvent.getClickedStyleElement(event); - const info = template.regexpProblemExplanation.cloneNode(true); - $.remove('#' + info.id); - $$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide)); - $$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation)); - entry.appendChild(info); - }, - - closeExplanation() { - $('#regexp-explanation').remove(); - }, - - openLink(event) { - if (!chrome.windows || !prefs.get('openEditInWindow', false)) { - handleEvent.openURLandHide.call(this, event); - return; - } - event.preventDefault(); - chrome.windows.create( - Object.assign({ - url: this.href - }, prefs.get('windowPosition', {})) - ); - close(); - }, - - maybeEdit(event) { - if (!( - event.button === 0 && (event.ctrlKey || event.metaKey) || - event.button === 1 || - event.button === 2)) { - return; - } - // open an editor on middleclick - if (event.target.matches('.entry, .style-name, .style-edit-link')) { - this.onmouseup = () => $('.style-edit-link', this).click(); - this.oncontextmenu = event => event.preventDefault(); - event.preventDefault(); - return; - } - // prevent the popup being opened in a background tab - // when an irrelevant link was accidentally clicked - if (event.target.closest('a')) { - event.preventDefault(); - return; - } - }, - - openURLandHide(event) { - event.preventDefault(); - const message = tryJSONparse(this.dataset.sendMessage); - getActiveTab() - .then(activeTab => API.openURL({ - url: this.href || this.dataset.href, - index: activeTab.index + 1 - })) - .then(tab => { - if (message) { - return onTabReady(tab) - .then(() => msg.sendTab(tab.id, message)); - } - }) - .then(window.close); - }, - - openManager(event) { - event.preventDefault(); - if (!this.eventHandled) { - this.eventHandled = true; - this.dataset.href += event.shiftKey || event.button === 2 ? - '?url=' + encodeURIComponent(tabURL) : ''; - handleEvent.openURLandHide.call(this, event); - } - }, -}); - - -function handleUpdate({style, reason}) { - if (!tabURL) return; - - fetchStyle() - .then(style => { - if (!style) { - return; - } - if ($(ENTRY_ID_PREFIX + style.id)) { - createStyleElement(style); - return; - } - document.body.classList.remove('blocked'); - $$.remove('.blocked-info, #no-styles'); - createStyleElement(style); - }) - .catch(console.error); - - function fetchStyle() { - if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) { - return Promise.resolve(style); - } - return API.getStylesByUrl(tabURL, style.id) - .then(([result]) => result && Object.assign(result.data, result)); - } -} - - -function handleDelete(id) { - $.remove(ENTRY_ID_PREFIX + id); - if (!$('.entry')) { - installed.appendChild(template.noStyles.cloneNode(true)); - } -} - -function getTabRealURLFirefox(tab) { - // wait for FF tab-on-demand to get a real URL (initially about:blank), 5 sec max - return new Promise(resolve => { - function onNavigation({tabId, url, frameId}) { - if (tabId === tab.id && frameId === 0) { - detach(); - resolve(url); - } - } - - function detach(timedOut) { - if (timedOut) { - resolve(tab.url); - } else { - debounce.unregister(detach); - } - chrome.webNavigation.onBeforeNavigate.removeListener(onNavigation); - chrome.webNavigation.onCommitted.removeListener(onNavigation); - chrome.tabs.onRemoved.removeListener(detach); - chrome.tabs.onReplaced.removeListener(detach); - } - - chrome.webNavigation.onBeforeNavigate.addListener(onNavigation); - chrome.webNavigation.onCommitted.addListener(onNavigation); - chrome.tabs.onRemoved.addListener(detach); - chrome.tabs.onReplaced.addListener(detach); - debounce(detach, 5000, {timedOut: true}); - }); -} +/* global configDialog hotkeys onTabReady msg + getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs CHROME + setupLivePrefs template t $create tWordBreak animateElement + tryJSONparse debounce */ + +'use strict'; + +let installed; +let tabURL; +const handleEvent = {}; + +const ENTRY_ID_PREFIX_RAW = 'style-'; +const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW; + +toggleSideBorders(); + +getActiveTab() + .then(tab => + FIREFOX && tab.url === 'about:blank' && tab.status === 'loading' + ? getTabRealURLFirefox(tab) + : getTabRealURL(tab) + ) + .then(url => Promise.all([ + (tabURL = URLS.supported(url) ? url : '') && + API.getStylesByUrl(tabURL), + onDOMready().then(initPopup), + ])) + .then(([results]) => { + if (!results) { + // unsupported URL; + return; + } + showStyles(results.map(r => Object.assign(r.data, r))); + }) + .catch(console.error); + +msg.onExtension(onRuntimeMessage); + +prefs.subscribe(['popup.stylesFirst'], (key, stylesFirst) => { + const actions = $('body > .actions'); + const before = stylesFirst ? actions : actions.nextSibling; + document.body.insertBefore(installed, before); +}); +prefs.subscribe(['popupWidth'], (key, value) => setPopupWidth(value)); +prefs.subscribe(['popup.borders'], (key, value) => toggleSideBorders(value)); + +function onRuntimeMessage(msg) { + switch (msg.method) { + case 'styleAdded': + case 'styleUpdated': + if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return; + handleUpdate(msg); + break; + case 'styleDeleted': + handleDelete(msg.style.id); + break; + } + dispatchEvent(new CustomEvent(msg.method, {detail: msg})); +} + + +function setPopupWidth(width = prefs.get('popupWidth')) { + document.body.style.width = + Math.max(200, Math.min(800, width)) + 'px'; +} + + +function toggleSideBorders(state = prefs.get('popup.borders')) { + // runs before is parsed + const style = document.documentElement.style; + if (CHROME >= 3167 && state) { + style.cssText += + 'border-left: 2px solid white !important;' + + 'border-right: 2px solid white !important;'; + } else if (style.cssText) { + style.borderLeft = style.borderRight = ''; + } +} + + +function initPopup() { + installed = $('#installed'); + + setPopupWidth(); + + // action buttons + $('#disableAll').onchange = function () { + installed.classList.toggle('disabled', this.checked); + }; + setupLivePrefs(); + + Object.assign($('#popup-manage-button'), { + onclick: handleEvent.openManager, + onmouseup: handleEvent.openManager, + oncontextmenu: handleEvent.openManager, + }); + + $('#popup-options-button').onclick = () => { + chrome.runtime.openOptionsPage(); + window.close(); + }; + + $('#popup-wiki-button').onclick = handleEvent.openURLandHide; + + if (!prefs.get('popup.stylesFirst')) { + document.body.insertBefore( + $('body > .actions'), + installed); + } + + if (!tabURL) { + document.body.classList.add('blocked'); + document.body.insertBefore(template.unavailableInfo, document.body.firstChild); + return; + } + + getActiveTab().then(function ping(tab, retryCountdown = 10) { + msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0}) + .catch(() => false) + .then(pong => { + if (pong) { + return; + } + // FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand + // so we'll wait a bit to handle popup being invoked right after switching + if (retryCountdown > 0 && ( + tab.status !== 'complete' || + FIREFOX && tab.url === 'about:blank')) { + setTimeout(ping, 100, tab, --retryCountdown); + return; + } + const info = template.unreachableInfo; + if (!FIREFOX) { + // Chrome "Allow access to file URLs" in chrome://extensions message + info.appendChild($create('p', t('unreachableFileHint'))); + } + if (FIREFOX && tabURL.startsWith(URLS.browserWebStore)) { + $('label', info).textContent = t('unreachableAMO'); + const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) + + (FIREFOX < 60 ? '' : '\n' + t('unreachableAMOHintNewFF')); + const renderToken = s => s[0] === '<' ? $create('b', tWordBreak(s.slice(1, -1))) : s; + const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken)); + const noteNode = $create('fragment', note.split('\n').map(renderLine)); + info.appendChild(noteNode); + } + // Inaccessible locally hosted file type, e.g. JSON, PDF, etc. + if (tabURL.length - tabURL.lastIndexOf('.') <= 5) { + info.appendChild($create('p', t('InaccessibleFileHint'))); + } + document.body.classList.add('unreachable'); + document.body.insertBefore(info, document.body.firstChild); + }); + }); + + // Write new style links + const writeStyle = $('#write-style'); + const matchTargets = document.createElement('span'); + const matchWrapper = document.createElement('span'); + matchWrapper.id = 'match'; + matchWrapper.appendChild(matchTargets); + + // For this URL + const urlLink = template.writeStyle.cloneNode(true); + Object.assign(urlLink, { + href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL), + title: `url-prefix("${tabURL}")`, + textContent: prefs.get('popup.breadcrumbs.usePath') + ? new URL(tabURL).pathname.slice(1) + // this URL + : t('writeStyleForURL').replace(/ /g, '\u00a0'), + onclick: handleEvent.openLink, + }); + if (prefs.get('popup.breadcrumbs')) { + urlLink.onmouseenter = + urlLink.onfocus = () => urlLink.parentNode.classList.add('url()'); + urlLink.onmouseleave = + urlLink.onblur = () => urlLink.parentNode.classList.remove('url()'); + } + matchTargets.appendChild(urlLink); + + // For domain + const domains = getDomains(tabURL); + for (const domain of domains) { + const numParts = domain.length - domain.replace(/\./g, '').length + 1; + // Don't include TLD + if (domains.length > 1 && numParts === 1) { + continue; + } + const domainLink = template.writeStyle.cloneNode(true); + Object.assign(domainLink, { + href: 'edit.html?domain=' + encodeURIComponent(domain), + textContent: numParts > 2 ? domain.split('.')[0] : domain, + title: `domain("${domain}")`, + onclick: handleEvent.openLink, + }); + domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : ''); + matchTargets.appendChild(domainLink); + } + + if (prefs.get('popup.breadcrumbs')) { + matchTargets.classList.add('breadcrumbs'); + matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild)); + } + writeStyle.appendChild(matchWrapper); + + function getDomains(url) { + let d = /.*?:\/*([^/:]+)|$/.exec(url)[1]; + if (!d || url.startsWith('file:')) { + return []; + } + const domains = [d]; + while (d.indexOf('.') !== -1) { + d = d.substring(d.indexOf('.') + 1); + domains.push(d); + } + return domains; + } +} + + +function sortStyles(entries) { + const enabledFirst = prefs.get('popup.enabledFirst'); + entries.sort((a, b) => + enabledFirst && a.styleMeta.enabled !== b.styleMeta.enabled ? + (a.styleMeta.enabled ? -1 : 1) : + a.styleMeta.name.localeCompare(b.styleMeta.name) + ); +} + +function showStyles(styles) { + if (!styles) { + return; + } + if (!styles.length) { + installed.appendChild(template.noStyles.cloneNode(true)); + window.dispatchEvent(new Event('showStyles:done')); + return; + } + const entries = styles.map(createStyleElement); + sortStyles(entries); + entries.forEach(e => installed.appendChild(e)); + window.dispatchEvent(new Event('showStyles:done')); +} + +function sortStylesInPlace() { + if (!prefs.get('popup.autoResort')) { + return; + } + const entries = $$('.entry', installed); + if (!entries.length) { + return; + } + sortStyles(entries); + entries.forEach(e => installed.appendChild(e)); +} + + +function createStyleElement(style) { + let entry = $(ENTRY_ID_PREFIX + style.id); + if (!entry) { + entry = template.style.cloneNode(true); + entry.setAttribute('style-id', style.id); + Object.assign(entry, { + id: ENTRY_ID_PREFIX_RAW + style.id, + styleId: style.id, + styleIsUsercss: Boolean(style.usercssData), + onmousedown: handleEvent.maybeEdit, + styleMeta: style + }); + const checkbox = $('.checker', entry); + Object.assign(checkbox, { + id: ENTRY_ID_PREFIX_RAW + style.id, + // title: t('exclusionsPopupTip'), + onclick: handleEvent.toggle, + // oncontextmenu: handleEvent.openExcludeMenu + }); + const editLink = $('.style-edit-link', entry); + Object.assign(editLink, { + href: editLink.getAttribute('href') + style.id, + onclick: handleEvent.openLink, + }); + const styleName = $('.style-name', entry); + Object.assign(styleName, { + htmlFor: ENTRY_ID_PREFIX_RAW + style.id, + onclick: handleEvent.name, + }); + styleName.checkbox = checkbox; + styleName.appendChild(document.createTextNode(' ')); + + const config = $('.configure', entry); + config.onclick = handleEvent.configure; + if (!style.usercssData) { + if (style.updateUrl && style.updateUrl.includes('?') && style.url) { + config.href = style.url; + config.target = '_blank'; + config.title = t('configureStyleOnHomepage'); + config.dataset.sendMessage = JSON.stringify({method: 'openSettings'}); + $('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso'; + } else { + config.classList.add('hidden'); + } + } else if (Object.keys(style.usercssData.vars || {}).length === 0) { + config.classList.add('hidden'); + } + + $('.delete', entry).onclick = handleEvent.delete; + + const indicator = template.regexpProblemIndicator.cloneNode(true); + indicator.appendChild(document.createTextNode('!')); + indicator.onclick = handleEvent.indicator; + $('.main-controls', entry).appendChild(indicator); + + $('.menu-button', entry).onclick = handleEvent.toggleMenu; + $('.menu-close', entry).onclick = handleEvent.toggleMenu; + + $('.exclude-by-domain-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'domain'); + $('.exclude-by-url-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'url'); + } + + style = Object.assign(entry.styleMeta, style); + + entry.classList.toggle('disabled', !style.enabled); + entry.classList.toggle('enabled', style.enabled); + $('.checker', entry).checked = style.enabled; + + const styleName = $('.style-name', entry); + styleName.lastChild.textContent = style.name; + setTimeout(() => { + styleName.title = entry.styleMeta.sloppy ? + t('styleNotAppliedRegexpProblemTooltip') : + styleName.scrollWidth > styleName.clientWidth + 1 ? + styleName.textContent : ''; + }); + + entry.classList.toggle('not-applied', style.excluded || style.sloppy); + entry.classList.toggle('regexp-partial', style.sloppy); + + $('.exclude-by-domain-checkbox', entry).checked = styleExcluded(style, 'domain'); + $('.exclude-by-url-checkbox', entry).checked = styleExcluded(style, 'url'); + + $('.exclude-by-domain', entry).title = getExcludeRule('domain'); + $('.exclude-by-url', entry).title = getExcludeRule('url'); + + return entry; +} + +function styleExcluded({exclusions}, type) { + if (!exclusions) { + return false; + } + const rule = getExcludeRule(type); + return exclusions.includes(rule); +} + +function getExcludeRule(type) { + const u = new URL(tabURL); + if (type === 'domain') { + return u.origin + '/*'; + } + // current page + return escapeGlob(u.origin + u.pathname); +} + +function escapeGlob(text) { + return text.replace(/\*/g, '\\*'); +} + +Object.assign(handleEvent, { + + getClickedStyleId(event) { + return (handleEvent.getClickedStyleElement(event) || {}).styleId; + }, + + getClickedStyleElement(event) { + return event.target.closest('.entry'); + }, + + name(event) { + this.checkbox.dispatchEvent(new MouseEvent('click')); + event.preventDefault(); + }, + + toggle(event) { + // when fired on checkbox, prevent the parent label from seeing the event, see #501 + event.stopPropagation(); + API + .toggleStyle(handleEvent.getClickedStyleId(event), this.checked) + .then(sortStylesInPlace); + }, + + toggleExclude(event, type) { + const entry = handleEvent.getClickedStyleElement(event); + if (event.target.checked) { + API.addExclusion(entry.styleMeta.id, getExcludeRule(type)); + } else { + API.removeExclusion(entry.styleMeta.id, getExcludeRule(type)); + } + }, + + toggleMenu(event) { + const entry = handleEvent.getClickedStyleElement(event); + const menu = $('.menu', entry); + const menuActive = $('.menu[data-display=true]'); + if (menuActive) { + // fade-out style menu + animateElement(menu, { + className: 'lights-on', + onComplete: () => (menu.dataset.display = false), + }); + window.onkeydown = null; + } else { + $('.menu-title', entry).textContent = $('.style-name', entry).textContent; + menu.dataset.display = true; + menu.style.cssText = ''; + window.onkeydown = event => { + const close = $('.menu-close', entry); + const checkbox = $('.exclude-by-domain-checkbox', entry); + const keyCode = event.keyCode || event.which; + if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) { + event.preventDefault(); + checkbox.focus(); + } + if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) { + event.preventDefault(); + close.focus(); + } + if (keyCode === 27) { + event.preventDefault(); + close.click(); + } + }; + } + event.preventDefault(); + }, + + delete(event) { + const entry = handleEvent.getClickedStyleElement(event); + const id = entry.styleId; + const box = $('#confirm'); + const menu = $('.menu', entry); + const cancel = $('[data-cmd="cancel"]', box); + const affirm = $('[data-cmd="ok"]', box); + box.dataset.display = true; + box.style.cssText = ''; + $('b', box).textContent = $('.style-name', entry).textContent; + affirm.focus(); + affirm.onclick = () => confirm(true); + cancel.onclick = () => confirm(false); + window.onkeydown = event => { + const close = $('.menu-close', entry); + const checkbox = $('.exclude-by-domain-checkbox', entry); + const confirmActive = $('#confirm[data-display="true"]'); + const keyCode = event.keyCode || event.which; + if (document.activeElement === cancel && (keyCode === 9)) { + event.preventDefault(); + affirm.focus(); + } + if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) { + event.preventDefault(); + checkbox.focus(); + } + if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) { + event.preventDefault(); + close.focus(); + } + if (keyCode === 27) { + event.preventDefault(); + if (confirmActive) { + box.dataset.display = false; + menu.focus(); + } else { + close.click(); + } + } + }; + function confirm(ok) { + if (ok) { + // fade-out deletion confirmation dialog + animateElement(box, { + className: 'lights-on', + onComplete: () => (box.dataset.display = false), + }); + window.onkeydown = null; + API.deleteStyle(id); + } else { + box.dataset.display = false; + menu.focus(); + } + } + }, + + configure(event) { + const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event); + if (styleIsUsercss) { + API.getStyle(styleId, true).then(style => { + hotkeys.setState(false); + configDialog(style).then(() => { + hotkeys.setState(true); + }); + }); + } else { + handleEvent.openURLandHide.call(this, event); + } + }, + + indicator(event) { + const entry = handleEvent.getClickedStyleElement(event); + const info = template.regexpProblemExplanation.cloneNode(true); + $.remove('#' + info.id); + $$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide)); + $$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation)); + entry.appendChild(info); + }, + + closeExplanation() { + $('#regexp-explanation').remove(); + }, + + openLink(event) { + if (!chrome.windows || !prefs.get('openEditInWindow', false)) { + handleEvent.openURLandHide.call(this, event); + return; + } + event.preventDefault(); + chrome.windows.create( + Object.assign({ + url: this.href + }, prefs.get('windowPosition', {})) + ); + close(); + }, + + maybeEdit(event) { + if (!( + event.button === 0 && (event.ctrlKey || event.metaKey) || + event.button === 1 || + event.button === 2)) { + return; + } + // open an editor on middleclick + if (event.target.matches('.entry, .style-name, .style-edit-link')) { + this.onmouseup = () => $('.style-edit-link', this).click(); + this.oncontextmenu = event => event.preventDefault(); + event.preventDefault(); + return; + } + // prevent the popup being opened in a background tab + // when an irrelevant link was accidentally clicked + if (event.target.closest('a')) { + event.preventDefault(); + return; + } + }, + + openURLandHide(event) { + event.preventDefault(); + const message = tryJSONparse(this.dataset.sendMessage); + getActiveTab() + .then(activeTab => API.openURL({ + url: this.href || this.dataset.href, + index: activeTab.index + 1 + })) + .then(tab => { + if (message) { + return onTabReady(tab) + .then(() => msg.sendTab(tab.id, message)); + } + }) + .then(window.close); + }, + + openManager(event) { + event.preventDefault(); + if (!this.eventHandled) { + this.eventHandled = true; + this.dataset.href += event.shiftKey || event.button === 2 ? + '?url=' + encodeURIComponent(tabURL) : ''; + handleEvent.openURLandHide.call(this, event); + } + }, +}); + + +function handleUpdate({style, reason}) { + if (!tabURL) return; + + fetchStyle() + .then(style => { + if (!style) { + return; + } + if ($(ENTRY_ID_PREFIX + style.id)) { + createStyleElement(style); + return; + } + document.body.classList.remove('blocked'); + $$.remove('.blocked-info, #no-styles'); + createStyleElement(style); + }) + .catch(console.error); + + function fetchStyle() { + if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) { + return Promise.resolve(style); + } + return API.getStylesByUrl(tabURL, style.id) + .then(([result]) => result && Object.assign(result.data, result)); + } +} + + +function handleDelete(id) { + $.remove(ENTRY_ID_PREFIX + id); + if (!$('.entry')) { + installed.appendChild(template.noStyles.cloneNode(true)); + } +} + +function getTabRealURLFirefox(tab) { + // wait for FF tab-on-demand to get a real URL (initially about:blank), 5 sec max + return new Promise(resolve => { + function onNavigation({tabId, url, frameId}) { + if (tabId === tab.id && frameId === 0) { + detach(); + resolve(url); + } + } + + function detach(timedOut) { + if (timedOut) { + resolve(tab.url); + } else { + debounce.unregister(detach); + } + chrome.webNavigation.onBeforeNavigate.removeListener(onNavigation); + chrome.webNavigation.onCommitted.removeListener(onNavigation); + chrome.tabs.onRemoved.removeListener(detach); + chrome.tabs.onReplaced.removeListener(detach); + } + + chrome.webNavigation.onBeforeNavigate.addListener(onNavigation); + chrome.webNavigation.onCommitted.addListener(onNavigation); + chrome.tabs.onRemoved.addListener(detach); + chrome.tabs.onReplaced.addListener(detach); + debounce(detach, 5000, {timedOut: true}); + }); +}