View layer for the Brazen user scripts framework
Version vom
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.sleazyfork.org/scripts/416104/1876591/Brazen%20Framework%20-%20View%20Layer.js
// ==UserScript==
// @name Brazen Framework - View Layer
// @namespace brazenvoid
// @version 3.5.0
// @author brazenvoid
// @license GPL-3.0-only
// @description View layer for the Brazen user scripts framework
// @grant GM_addStyle
// ==/UserScript==
/**
* @function GM_addStyle
* @param {string} style
*/
GM_addStyle(
`@keyframes brazen-fade { from { opacity: 0 } to { opacity: 1 } } #bv-resizer { align-items: center; background-color: white; border-radius: 3px; box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.5); cursor: ew-resize; display: flex; height: 18px; justify-content: center; position: absolute; right: 10px; transition: background 0.3s ease; width: 25px; z-index: 5; } #bv-resizer:hover { background-color: lightgrey; } #bv-resizer-icon { color: black; font-size: 100%; pointer-events: none; } #bv-ui { backdrop-filter: blur(10px); background: rgba(255, 255, 255, 0.1); border-bottom-right-radius: 8px; border-left: 0; border-top-right-radius: 8px; bottom: 5vh; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); box-sizing: border-box; opacity: 0.3; overflow: auto; top: 5vh; z-index: 1001; } #bv-ui:hover { opacity: 1; } #restore-settings.bv-input { margin-bottom: 1rem } .show-settings.bv-section { border: 2px solid white; border-radius: 5px; box-shadow: 0 0 20px white; padding: 8px; top: 5vh; } .bv-actions { display: inline-flex; justify-content: center; padding: 0 0.25rem; text-align: center } .bv-actions .bv-button { width: auto } .bv-bg-colour { background-color: #4f535b } .bv-border-primary { border: 1px solid black } .bv-break { margin: 0.5rem 0 } .bv-button { background-color: revert; padding: 0.5rem 1rem; width: 100% } .bv-flex-column { flex-direction: column } .bv-font-primary { color: white } .bv-font-secondary { color: black } .bv-group { align-items: center; display: flex; min-height: 20px } .bv-group + .bv-group { margin-top: 1rem } .bv-group.bv-range-group, .bv-group.bv-text-group { align-items: center } .bv-group.bv-range-group > input { width: 75px } .bv-group.bv-range-group > input + input { margin-left: 5px } .bv-group.bv-textarea-group { align-items: start; flex-direction: column; overflow: hidden } .bv-group.bv-textarea-group > textarea.bv-input { margin-top: 0.5rem; resize: vertical; width: 100% } input.bv-input, select.bv-input, textarea.bv-input { box-sizing: border-box; margin: 0; padding: 0.5rem } .bv-input.bv-checkbox-radio { margin-right: 5px; scale: 2 } .bv-input.bv-text { width: 100% } .bv-label { flex-grow: 1; text-align: start } .bv-label.bv-text + .bv-input.bv-text { width: 40% } .bv-section, #bv-ui, .bv-modal-dialog, .show-settings.bv-section { --bv-font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family: var(--bv-font-family); } .bv-section :is(button, input, select, textarea, .bv-button, .bv-tab-button, .bv-label, .bv-title, .bv-token-btn, .bv-bookmark-link), #bv-ui :is(button, input, select, textarea, .bv-button, .bv-tab-button, .bv-label, .bv-title, .bv-token-btn, .bv-bookmark-link), .bv-modal-dialog :is(button, input, select, textarea, .bv-button, .bv-modal-title, h3, p, span), .show-settings.bv-section :is(button, input) { font-family: var(--bv-font-family); } .bv-section { display: flex; flex-direction: column; font-size: 1rem; font-weight: normal; left: 0; padding: 1rem; position: fixed; z-index: 1000 } .bv-section > .bv-bottom-section { display: flex; flex-direction: column; margin-top: auto; } .bv-section > div + div { margin-top: 1rem } .bv-section hr { border: 1px solid white; margin: 1rem 0 } .bv-section button + button { margin-left: 0.25rem } .bv-section .bv-title { display: block; height: 20px; margin-bottom: 1rem; text-align: center; width: 100% } .bv-show-settings { border: 0; font-size: 0.7rem; height: 90vh; left: 0; margin: 0; padding: 0; position: fixed; top: 5vh; width: 0.2vw; writing-mode: sideways-lr; z-index: 999 } .bv-show-settings .bv-title { display: block; height: 20px; width: 100% } .bv-tab-button { background-color: inherit; border-bottom: 0; border-top-left-radius: 3px; border-top-right-radius: 3px; cursor: pointer; outline: none; padding: 0.5rem 0.75rem; transition: 0.3s } .bv-tab-button.bv-active, .bv-tab-button:hover { background-color: white; color: black; } .bv-tab-panel { animation: brazen-fade 1s; display: none; flex-direction: column; padding: 1rem } .bv-tab-panel.bv-active { display: flex } .bv-tabs-nav { display: flex; flex-wrap: wrap; overflow: hidden } .bv-bookmarks-group { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; width: 100%; } .bv-bookmark-columns { column-gap: 0.25rem; display: grid; grid-template-columns: minmax(0, 1fr) var(--bv-bookmark-action-size, 1.5rem); min-width: 0; row-gap: 0.25rem; scrollbar-gutter: stable; width: 100%; } .bv-bookmark-toolbar { align-items: center; display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; } .bv-bookmark-scroll { display: grid; grid-auto-rows: min-content; grid-column: 1 / -1; grid-template-columns: subgrid; max-height: 14rem; overflow-y: auto; row-gap: 0.25rem; } .bv-bookmark-list { display: contents; } .bv-bookmark-row { align-items: center; display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; } .bv-bookmark-search { box-sizing: border-box; grid-column: 1; height: var(--bv-bookmark-action-size, 1.5rem); margin-top: 0; min-height: var(--bv-bookmark-action-size, 1.5rem); min-width: 0; width: auto; } .bv-bookmark-search.bv-input.bv-text { width: auto; } .bv-bookmark-sort { box-sizing: border-box; grid-column: 2; height: var(--bv-bookmark-action-size, 1.5rem); margin: 0; min-height: var(--bv-bookmark-action-size, 1.5rem); min-width: 0; padding: 0.25rem 0.5rem; width: var(--bv-bookmark-action-size, 1.5rem); } .bv-bookmark-main { align-items: center; display: flex; gap: 0.2rem; grid-column: 1; min-width: 0; } .bv-bookmark-main > .bv-bookmark-link { flex: 1 1 auto; } .bv-bookmark-action { align-items: center; box-sizing: border-box; display: inline-flex; flex: 0 0 auto; height: var(--bv-bookmark-action-size, 1.5rem); justify-content: center; margin: 0; min-height: var(--bv-bookmark-action-size, 1.5rem); min-width: 0; padding: 0; width: var(--bv-bookmark-action-size, 1.5rem); } .bv-bookmark-link { box-sizing: border-box; display: block; height: var(--bv-bookmark-action-size, 1.5rem); line-height: var(--bv-bookmark-action-size, 1.5rem); margin: 0; min-height: var(--bv-bookmark-action-size, 1.5rem); min-width: 0; overflow: hidden; padding: 0 0.38rem; text-align: left; text-overflow: ellipsis; white-space: nowrap; width: auto; } .bv-bookmark-remove { align-items: center; box-sizing: border-box; display: inline-flex; grid-column: 2; height: var(--bv-bookmark-action-size, 1.5rem); justify-content: center; line-height: 1; margin: 0; min-height: var(--bv-bookmark-action-size, 1.5rem); min-width: 0; padding: 0.25rem 0.5rem; width: var(--bv-bookmark-action-size, 1.5rem); } .bv-bookmark-empty { font-size: 0.85rem; grid-column: 1 / -1; margin-top: 0.15rem; } .bv-bookmark-toolbar button + button, .bv-bookmark-row button + button { margin-left: 0; } @supports not (grid-template-columns: subgrid) { .bv-bookmark-columns { scrollbar-gutter: auto; } .bv-bookmark-toolbar, .bv-bookmark-row { column-gap: 0.25rem; display: grid; grid-column: 1 / -1; grid-template-columns: minmax(0, 1fr) var(--bv-bookmark-action-size, 1.5rem); } .bv-bookmark-scroll { display: block; } .bv-bookmark-list { display: flex; flex-direction: column; gap: 0.25rem; } .bv-bookmark-toolbar { box-sizing: border-box; padding-inline-end: var(--bv-bookmark-scroll-inset, 0px); } } .bv-modal-overlay { align-items: center; background: rgba(0, 0, 0, 0.55); display: flex; inset: 0; justify-content: center; position: fixed; z-index: 10002; } .bv-modal-dialog { background: #4f535b; border: 1px solid black; border-radius: 4px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); color: white; max-height: 80vh; max-width: 90vw; min-width: 280px; overflow: auto; padding: 1rem; } .bv-modal-header { align-items: center; display: flex; gap: 0.5rem; justify-content: space-between; margin-bottom: 0.75rem; } .bv-modal-title { font-size: 1rem; font-weight: 500; margin: 0; } .bv-modal-close { padding: 0.25rem 0.5rem; width: auto; } .bv-modal-dialog .bv-button { background-color: #3a3f47; border: 1px solid #222; color: #fff; cursor: pointer; width: auto; } .bv-modal-dialog .bv-button:hover { background-color: #4a505a; } .bv-modal-body ul:not(.bv-compliance-rule-list) { margin: 0 0 1rem; padding-left: 1.25rem; } .bv-modal-body h3 { font-size: 0.9rem; margin: 0 0 0.35rem; } .bv-compliance-rule-list { list-style: none; margin: 0 0 1rem; padding: 0; } .bv-compliance-rule-row { align-items: center; display: flex; gap: 0.5rem; justify-content: space-between; margin-bottom: 0.35rem; } .bv-compliance-rule-label { flex: 1 1 auto; min-width: 0; } .bv-compliance-rule-remove { flex: 0 0 auto; padding: 0.2rem 0.45rem; width: auto; } .bv-dock-slide-panel { box-sizing: border-box; display: flex; flex-direction: column; position: fixed; z-index: 1001; } .bv-dock-slide-panel-head { align-items: center; display: flex; flex-shrink: 0; gap: 0.5rem; justify-content: space-between; margin-bottom: 0.35rem; } .bv-dock-slide-panel-title { font-size: 0.85rem; font-weight: 500; margin: 0; } .bv-dock-slide-panel-close { padding: 0.2rem 0.45rem; width: auto; } .bv-dock-slide-panel-body { flex: 1 1 auto; min-height: 0; overflow: auto; } .bv-dock-slide-panel-body h3 { font-size: 0.78rem; margin: 0.5rem 0 0.25rem; } .bv-dock-slide-panel-body .bv-compliance-rule-list { list-style: none; margin: 0 0 0.5rem; padding: 0; } .bv-dock-slide-panel-body .bv-compliance-rule-row { align-items: center; display: flex; gap: 0.35rem; justify-content: space-between; margin-bottom: 0.25rem; } .bv-dock-slide-panel-body .bv-compliance-rule-label { flex: 1 1 auto; min-width: 0; } .bv-dock-slide-panel-body .bv-compliance-rule-remove { flex: 0 0 auto; margin-left: 0; width: auto; } .bv-dock-slide-panel.bv-dock-panel-left { border-bottom-left-radius: 0; border-top-left-radius: 0; } .bv-dock-slide-panel.bv-dock-panel-right { border-bottom-right-radius: 0; border-top-right-radius: 0; } .bv-dock-slide-panel.bv-dock-panel-bottom { border-bottom-left-radius: 0; border-bottom-right-radius: 0; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); } .bv-dock-slide-panel.bv-dock-panel-animate { transition: transform 0.25s ease, opacity 0.25s ease; } .bv-dock-slide-panel.bv-dock-panel-offscreen { pointer-events: none; } .bv-dock-slide-panel.bv-dock-panel-hidden { display: none !important; } .bv-tag-actions-slot { display: inline-flex; position: relative; } .bv-tag-actions-slot .tag-count[hidden] { display: none; } .bv-tag-actions { align-items: center; display: inline-flex; gap: 0.15rem; } .bv-tag-actions[hidden] { display: none; } .bv-dm-selected { box-shadow: inset 0 0 0 2px var(--bv-theme-color, #778c77); border-radius: 2px; } .bv-dock-progress-slot { align-items: stretch; display: flex; flex: 0 0 auto; flex-direction: column; gap: 0.15rem; justify-content: center; max-width: calc(var(--bv-dock-btn-size) * 2.2); min-width: calc(var(--bv-dock-btn-size) * 1.6); padding: 0.15rem 0.2rem; } .bv-dock-progress-slot.bv-dock-progress-hidden { display: none; } .bv-dock-progress-bar { background: rgba(0, 0, 0, 0.25); border-radius: 3px; height: 0.35rem; overflow: hidden; width: 100%; } .bv-dock-progress-bar > span { background: var(--bv-dock-btn-active-bg, #5a6f5a); display: block; height: 100%; transition: width 0.2s ease; } .bv-dock-progress-label { color: #e8efe8; font-size: 0.58rem; line-height: 1.2; text-align: right; white-space: nowrap; } .bv-tag-discovery-list { list-style: none; margin: 0; padding: 0; } .bv-tag-discovery-row { align-items: center; display: flex; gap: 0.35rem; justify-content: space-between; margin-bottom: 0.35rem; } .bv-tag-discovery-row.bv-tag-muted .bv-tag-discovery-name { color: #8a8a8a; } .bv-tag-discovery-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bv-tag-discovery-actions { align-items: center; display: inline-flex; flex: 0 0 auto; gap: 0.1rem; } .bv-dock { --bv-dock-bg: var(--bv-theme-color, #4f535b); --bv-dock-btn-bg: #3a3f47; --bv-dock-btn-hover-bg: #4a505a; --bv-dock-btn-active-bg: #5a6f5a; --bv-dock-btn-size: 2.75rem; --bv-dock-child-btn-size: 2.15rem; --bv-dock-gap: 0.2rem; --bv-dock-slideout-bg: rgba(0, 0, 0, 0.22); --bv-dock-slideout-gap: 0.12rem; align-items: stretch; background: var(--bv-dock-bg); border: 1px solid rgba(0, 0, 0, 0.35); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45); box-sizing: border-box; color: #e8efe8; display: flex; font-family: var(--bv-font-family); gap: var(--bv-dock-gap); padding: 0.35rem; position: fixed; z-index: 1002; } .bv-dock-left { align-items: flex-start; flex-direction: row; left: 0; top: 5vh; } .bv-dock-right { align-items: flex-start; flex-direction: row-reverse; right: 0; top: 5vh; } .bv-dock-bottom { bottom: 0; flex-direction: column-reverse; left: 50%; max-width: 96vw; overflow: visible; transform: translateX(-50%); } .bv-dock-name { align-items: center; display: flex; font-size: 0.68rem; font-weight: 500; justify-content: center; letter-spacing: 0.02em; line-height: 1.2; min-width: 1.1rem; opacity: 0.9; padding: 0.15rem 0.1rem; text-align: center; white-space: nowrap; } .bv-dock-left .bv-dock-name, .bv-dock-right .bv-dock-name { writing-mode: sideways-rl; } .bv-dock-rail { align-items: stretch; display: flex; flex: 1 1 auto; gap: var(--bv-dock-gap); min-height: 0; min-width: 0; } .bv-dock-rail-head, .bv-dock-rail-body, .bv-dock-rail-foot { display: contents; } .bv-dock-separator { background: rgba(255, 255, 255, 0.12); flex: 0 0 auto; } .bv-dock-left .bv-dock-separator, .bv-dock-right .bv-dock-separator { height: 1px; width: 100%; } .bv-dock-bottom .bv-dock-separator { align-self: stretch; width: 1px; } .bv-dock-status { align-items: center; background: var(--bv-dock-btn-hover-bg, #4a505a); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; box-sizing: border-box; color: #e8efe8; display: inline-flex; font-size: 0.62rem; font-weight: 500; gap: 0.3rem; line-height: 1.2; max-width: calc(var(--bv-dock-btn-size) * 2.5); opacity: 0.95; padding: 0.3rem 0.35rem; pointer-events: none; text-align: center; user-select: none; white-space: normal; } .bv-dock-status-spinner { animation: bv-dock-spin 0.9s linear infinite; border: 2px solid rgba(255, 255, 255, 0.25); border-radius: 50%; border-top-color: rgba(255, 255, 255, 0.9); flex: 0 0 auto; height: 0.75rem; width: 0.75rem; } @keyframes bv-dock-spin { to { transform: rotate(360deg); } } .bv-dock-left .bv-dock-rail { align-self: flex-start; flex-direction: column; } .bv-dock-left .bv-dock-slot { align-items: stretch; display: flex; flex-direction: column; flex: 0 0 auto; gap: 0; width: 100%; } .bv-dock-left .bv-dock-slot:hover, .bv-dock-left .bv-dock-slot:focus-within { gap: var(--bv-dock-slideout-gap); } .bv-dock-left .bv-dock-slot > .bv-dock-btn { width: 100%; } .bv-dock-left .bv-dock-slideout { flex-direction: column; max-height: 0; transition: max-height 0.2s ease, opacity 0.2s ease, padding 0.2s ease; } .bv-dock-left .bv-dock-slot:hover .bv-dock-slideout, .bv-dock-left .bv-dock-slot:focus-within .bv-dock-slideout { max-height: calc(var(--bv-dock-child-btn-size) * 6 + 0.75rem); opacity: 1; padding: 0.12rem; pointer-events: auto; } .bv-dock-right .bv-dock-rail { align-self: flex-start; flex-direction: column; } .bv-dock-right .bv-dock-slot { align-items: stretch; display: flex; flex-direction: column; flex: 0 0 auto; gap: 0; width: 100%; } .bv-dock-right .bv-dock-slot:hover, .bv-dock-right .bv-dock-slot:focus-within { gap: var(--bv-dock-slideout-gap); } .bv-dock-right .bv-dock-slot > .bv-dock-btn { width: 100%; } .bv-dock-right .bv-dock-slideout { flex-direction: column; max-height: 0; transition: max-height 0.2s ease, opacity 0.2s ease, padding 0.2s ease; } .bv-dock-right .bv-dock-slot:hover .bv-dock-slideout, .bv-dock-right .bv-dock-slot:focus-within .bv-dock-slideout { max-height: calc(var(--bv-dock-child-btn-size) * 6 + 0.75rem); opacity: 1; padding: 0.12rem; pointer-events: auto; } .bv-dock-bottom .bv-dock-rail { flex: 0 0 auto; flex-direction: row; flex-wrap: nowrap; overflow: visible; } .bv-dock-bottom .bv-dock-slot { align-items: center; display: flex; flex-direction: row; flex: 0 0 auto; gap: 0; } .bv-dock-bottom .bv-dock-slot:hover, .bv-dock-bottom .bv-dock-slot:focus-within { gap: var(--bv-dock-slideout-gap); } .bv-dock-bottom .bv-dock-slideout { flex-direction: row; max-height: none; max-width: 0; transition: max-width 0.2s ease, opacity 0.2s ease, padding 0.2s ease; } .bv-dock-bottom .bv-dock-slot:hover .bv-dock-slideout, .bv-dock-bottom .bv-dock-slot:focus-within .bv-dock-slideout { max-width: calc(var(--bv-dock-child-btn-size) * 6 + 0.75rem); opacity: 1; padding: 0.12rem; pointer-events: auto; } .bv-dock-btn { align-items: center; background: var(--bv-dock-btn-bg); border: 1px solid rgba(0, 0, 0, 0.35); border-radius: 4px; box-sizing: border-box; color: inherit; cursor: pointer; display: inline-flex; flex: 0 0 auto; height: var(--bv-dock-btn-size); justify-content: center; margin: 0; padding: 0.35rem; width: var(--bv-dock-btn-size); } .bv-dock-btn:hover { background: var(--bv-dock-btn-hover-bg); } .bv-dock-btn.bv-dock-btn-active { background: var(--bv-dock-btn-active-bg); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); } .bv-dock-btn-icon { display: block; height: 1.35rem; pointer-events: none; width: 1.35rem; } .bv-dock-slideout { align-items: center; background: var(--bv-dock-slideout-bg); border-radius: 4px; box-sizing: border-box; display: flex; flex-wrap: nowrap; gap: var(--bv-dock-slideout-gap); opacity: 0; overflow: hidden; padding: 0; pointer-events: none; } .bv-dock-slideout > .bv-dock-btn { height: var(--bv-dock-child-btn-size); padding: 0.2rem; width: var(--bv-dock-child-btn-size); } .bv-dock-slideout > .bv-dock-btn .bv-dock-btn-icon { height: 1rem; width: 1rem; } .bv-dock-position-btn { flex: 0 0 auto; } #bv-ui.bv-dock-panel-left #bv-resizer, #bv-ui.bv-dock-panel-bottom #bv-resizer { left: auto; right: 10px; } #bv-ui.bv-dock-panel-right #bv-resizer { left: 10px; right: auto; } #bv-ui.bv-dock-panel-bottom { box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); }`
)
class BrazenViewLayer
{
/**
* @typedef {{id: string, label: string, tags: string, url: string}} BrazenBookmarkEntry
*/
/**
* @typedef {{
* getCurrentUrl?: function(): string,
* normalizeUrl?: function(string): string,
* onMatchChange: function(boolean): void,
* watchSelectors?: string[],
* isActive?: function(): boolean,
* }} BrazenBookmarksPageMatchOptions
*/
/**
* @typedef {{
* showAddButton?: boolean,
* addCaption?: string,
* addHelpText?: string,
* searchPlaceholder?: string,
* searchHelpText?: string,
* sortHelpText?: string,
* emptyMessage?: string,
* noMatchMessage?: string,
* pageMatch?: BrazenBookmarksPageMatchOptions,
* onAdd?: Function,
* onSort?: Function,
* onRemove?: function(string): void,
* onNavigate?: function(string): void,
* getRowActions?: function(BrazenBookmarkEntry): (HTMLElement|JQuery)[],
* }} BrazenBookmarksPanelOptions
*/
/**
* @typedef {{
* element: JQuery,
* render: function(BrazenBookmarkEntry[]): void,
* getFilterQuery: function(): string,
* isCurrentPageBookmarked: function(string=): boolean,
* checkPageMatch: function(): void,
* }} BrazenBookmarksPanel
*/
/**
* @typedef {{
* sidebarSelector?: string,
* rowSelector?: string,
* countSelector?: string,
* hideDelayMs?: number,
* extractTag?: function(Element): *,
* renderActions?: function(Element, *, Element, *): void,
* isBookmarked?: function(string): boolean,
* }} TagSidebarCountEnhancementOptions
*/
// -------------------------------------------------------------------------
// Static public methods
// -------------------------------------------------------------------------
/**
* @param {JQuery} nodes
*/
static appendToBody(nodes)
{
$('body').append(nodes)
}
/**
* @param {string} name
* @return {SVGSVGElement}
*/
static getDockIcon(name)
{
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('viewBox', '0 0 24 24')
svg.setAttribute('class', 'bv-dock-btn-icon')
svg.setAttribute('aria-hidden', 'true')
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('fill', 'none')
path.setAttribute('stroke', 'currentColor')
path.setAttribute('stroke-width', '1.75')
path.setAttribute('stroke-linecap', 'round')
path.setAttribute('stroke-linejoin', 'round')
switch (name) {
case 'menu':
path.setAttribute('d', 'M4 7h16M4 12h16M4 17h16')
break
case 'bookmark':
path.setAttribute('d', 'M6 4h12v16l-6-4-6 4V4z')
break
case 'download':
path.setAttribute('d', 'M12 4v10m0 0l4-4m-4 4l-4-4M5 20h14')
break
case 'next':
path.setAttribute('d', 'M6 7l5 5-5 5M13 7l5 5-5 5')
break
case 'explore':
svg.innerHTML = ''
let ring = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
ring.setAttribute('cx', '12')
ring.setAttribute('cy', '12')
ring.setAttribute('r', '8')
ring.setAttribute('fill', 'none')
ring.setAttribute('stroke', 'currentColor')
ring.setAttribute('stroke-width', '1.5')
let needle = document.createElementNS('http://www.w3.org/2000/svg', 'path')
needle.setAttribute('d', 'M12 6.5L14.5 12L12 17.5L9.5 12Z')
needle.setAttribute('fill', 'none')
needle.setAttribute('stroke', 'currentColor')
needle.setAttribute('stroke-width', '1.5')
needle.setAttribute('stroke-linejoin', 'round')
svg.append(ring, needle)
return svg
case 'clear':
path.setAttribute('d', 'M4 7h16M6 7l1 14h10l1-14M9 7V5h6v2')
break
case 'filter':
path.setAttribute('d', 'M4 6h16M7 12h10M10 18h4')
break
case 'tags':
path.setAttribute('d', 'M7 7h.01M4 6l8.5 8.5a2 2 0 0 0 2.8 0l5.2-5.2a2 2 0 0 0 0-2.8L12 2 4 10a2 2 0 0 0 0 2.8z')
break
case 'inject':
path.setAttribute('d', 'M11 6H4M11 10H4M11 14H7M18 9v6m-3-3h6')
break
case 'block':
path.setAttribute('d', 'M6 6l12 12M12 3a9 9 0 1 1 0 18 9 9 0 0 1 0-18')
break
case 'resolution':
path.setAttribute('d', 'M5 6h14v12H5z M8 10h8M8 14h5')
break
case 'rules':
path.setAttribute('d', 'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01')
break
case 'history':
path.setAttribute('d', 'M12 8v5l3 2M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z')
break
case 'target':
path.setAttribute('d', 'M12 3v3m0 12v3m9-9h-3M6 12H3m14.3-5.3l-2.1 2.1M8.8 15.2l-2.1 2.1m12.6 0l-2.1-2.1M8.8 8.8L6.7 6.7')
break
case 'dock-left':
path.setAttribute('d', 'M5 5v14M9 7h10M9 12h10M9 17h10')
break
case 'dock-right':
path.setAttribute('d', 'M19 5v14M15 7H5M15 12H5M15 17H5')
break
case 'dock-bottom':
path.setAttribute('d', 'M5 19h14M7 15V5M12 15V5M17 15V5')
break
default:
path.setAttribute('d', 'M12 12h.01')
break
}
svg.append(path)
return svg
}
// -------------------------------------------------------------------------
// Private class variables
// -------------------------------------------------------------------------
/**
* @type {boolean}
* @private
*/
_resizing = false
/**
* @type {JQuery}
* @private
*/
_section = null
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
/**
* @param {string} selectorPrefix
*/
constructor(selectorPrefix)
{
this._selectorGenerator = new SelectorGenerator(selectorPrefix)
this._selectorPrefix = selectorPrefix
}
// -------------------------------------------------------------------------
// Public class methods
// -------------------------------------------------------------------------
/**
* @param {JQuery[]} children
* @return {JQuery}
*/
createBottomSection(children)
{
return $('<div class="bv-bottom-section">').append(...children)
}
/**
* @return {JQuery}
*/
createBreakSeparator()
{
return $('<br class="bv-break"/>')
}
/**
* @return {JQuery}
*/
createContainer()
{
this._section = $('<section class="bv-section bv-font-primary">')
return this._section
}
/**
* @param {JQuery|JQuery[]} children
* @param {string} wrapperClasses
* @return {JQuery}
*/
createFormActions(children, wrapperClasses = '')
{
return $('<div class="bv-actions"/>').addClass(wrapperClasses).append(children)
}
/**
* @param {string} caption
* @param {JQuery.EventHandler} onClick
* @param {string} hoverHelp
* @return {JQuery}
*/
createFormButton(caption, hoverHelp, onClick)
{
return $('<button class="bv-button">').attr('title', hoverHelp).text(caption).on('click', onClick)
}
createFormCheckBoxesGroupSection(label, keyValuePairs, hoverHelp)
{
let section = this.createFormSection(label).addClass('bv-checkboxes-group').attr('title', hoverHelp)
for (const element of keyValuePairs) {
section.append(
this.createFormGroup().append([
this.createFormGroupLabel(element[0], 'checkbox'),
this.createFormGroupInput('checkbox').attr('data-value', element[1]),
]),
)
}
return section
}
/**
* @return {JQuery}
*/
createFormGroup()
{
return $('<div class="bv-group"/>')
}
/**
* @param {string} id
* @param {Array} keyValuePairs
*
* @return {JQuery}
*/
createFormGroupDropdown(id, keyValuePairs)
{
let dropdown = $('<select>').attr('id', id).addClass('bv-input')
for (let i = 0; i < keyValuePairs.length; i++) {
dropdown.append($('<option>').attr('value', keyValuePairs[i][0]).text(keyValuePairs[i][1]).prop('selected', (i === 0)))
}
return dropdown
}
/**
* @param {string} type
*
* @return {JQuery}
*/
createFormGroupInput(type)
{
let input = $('<input class="bv-input">').attr('type', type)
switch (type) {
case 'number':
case 'text':
input.addClass('bv-text')
break
case 'radio':
case 'checkbox':
input.addClass('bv-checkbox-radio')
break
}
return input
}
/**
* @param {string} label
* @param {string} inputType
* @return {JQuery}
*/
createFormGroupLabel(label, inputType = '')
{
let labelFormGroup = $('<label class="bv-label">').text(label)
if (inputType !== '') {
switch (inputType) {
case 'number':
case 'text':
labelFormGroup.addClass('bv-text')
labelFormGroup.text(labelFormGroup.text() + ': ')
break
case 'radio':
case 'checkbox':
labelFormGroup.addClass('bv-checkbox-radio')
break
}
}
return labelFormGroup
}
/**
* @param {string} statisticType
* @return {JQuery}
*/
createFormGroupStatLabel(statisticType)
{
return $('<label class="bv-stat-label">').attr('id', this._selectorGenerator.getStatLabelSelector(statisticType)).text('0')
}
/**
* @param {string} label
* @param {string} inputType
* @param {string} hoverHelp
* @return {JQuery}
*/
createFormInputGroup(label, inputType, hoverHelp = '')
{
return this.createFormGroup().attr('title', hoverHelp).append([
this.createFormGroupLabel(label, inputType),
this.createFormGroupInput(inputType),
])
}
createFormRadiosGroupSection(label, keyValuePairs, hoverHelp)
{
let section = this.createFormSection(label).addClass('bv-radios-group').attr('title', hoverHelp)
for (let i = 0; i < keyValuePairs.length; i++) {
section.append(
this.createFormGroup().append([
this.createFormGroupLabel(keyValuePairs[i][0], 'radio'),
this.createFormGroupInput('radio').prop('checked', i === 0).attr('data-value', keyValuePairs[i][1]).on('change', (event) => {
$(event.currentTarget).parents('.bv-radios-group').first().find('input').each((index, element) => {
if (!element.isSameNode(event.currentTarget)) {
$(element).prop('checked', false)
}
})
}),
]),
)
}
return section
}
/**
* @param {string} label
* @param {string} inputsType
* @param {number} minimum
* @param {number} maximum
* @param {string} hoverHelp
* @return {JQuery}
*/
createFormRangeInputGroup(label, inputsType, minimum, maximum, hoverHelp)
{
return this.createFormGroup().addClass('bv-range-group').attr('title', hoverHelp).append([
this.createFormGroupLabel(label, inputsType),
this.createFormGroupInput(inputsType).attr('min', minimum).attr('max', maximum),
this.createFormGroupInput(inputsType).attr('min', minimum).attr('max', maximum),
])
}
/**
* @param {string} title
* @return {JQuery}
*/
createFormSection(title = '')
{
return $('<div>').append($('<label class="bv-title">').text(title))
}
/**
* @param {string} label
* @param {int} rows
* @param {string} hoverHelp
* @return {JQuery}
*/
createFormTextAreaGroup(label, rows, hoverHelp = '')
{
return this.createFormGroup().attr('title', hoverHelp).addClass('bv-textarea-group').append([
this.createFormGroupLabel(label),
$('<textarea class="bv-input" spellcheck="false">').attr('rows', rows),
])
}
/**
* Searchable bookmark list with add, sort, navigate, and remove controls.
*
* @param {BrazenBookmarksPanelOptions} options
* @return {BrazenBookmarksPanel}
*/
createBookmarksPanel(options = {})
{
let settings = {
showAddButton: true,
addCaption: 'Add Bookmark',
addHelpText: '',
searchPlaceholder: 'Search bookmarks…',
searchHelpText: 'Filter bookmarks by label or tags',
sortHelpText: 'Sort bookmarks A–Z',
emptyMessage: 'No bookmarks yet.',
noMatchMessage: 'No matching bookmarks.',
onAdd: () => {},
onSort: () => {},
onRemove: () => {},
onNavigate: (url) => {
location.href = url
},
getRowActions: () => [],
...options,
}
let filterQuery = ''
let lastBookmarks = []
let listEl = $('<div class="bv-bookmark-list">')
let emptyEl = $('<div class="bv-bookmark-empty">').text(settings.emptyMessage)
let searchEl = $('<input type="search" class="bv-input bv-text bv-bookmark-search">').
attr('placeholder', settings.searchPlaceholder).
attr('title', settings.searchHelpText)
searchEl.on('input', () => {
filterQuery = String(searchEl.val() ?? '')
widget.render(lastBookmarks)
})
let sortButton = $('<button type="button" class="bv-bookmark-sort bv-button">').
text('⇅').
attr('title', settings.sortHelpText).
on('click', () => settings.onSort())
let toolbar = $('<div class="bv-bookmark-toolbar">').append(searchEl, sortButton)
let scrollEl = $('<div class="bv-bookmark-scroll">')
scrollEl.append(listEl)
let columnsEl = $('<div class="bv-bookmark-columns">')
columnsEl.append(toolbar, emptyEl, scrollEl)
let syncBookmarkColumnInset = () => {
let scrollNode = scrollEl[0]
if (!scrollNode) {
return
}
columnsEl[0].style.setProperty('--bv-bookmark-scroll-inset', `${scrollNode.offsetWidth - scrollNode.clientWidth}px`)
}
if (typeof ResizeObserver !== 'undefined') {
new ResizeObserver(() => syncBookmarkColumnInset()).observe(scrollEl[0])
}
let elementChildren = []
if (settings.showAddButton) {
elementChildren.push(this.createFormButton(settings.addCaption, settings.addHelpText, () => settings.onAdd()))
}
elementChildren.push(columnsEl)
let element = $('<div class="bv-bookmarks-group">').append(elementChildren)
let widget = {
element: element,
getFilterQuery: () => filterQuery,
isCurrentPageBookmarked: (url) => {
let match = settings.pageMatch
if (!match) {
return false
}
let normalize = match.normalizeUrl ?? ((value) => String(value ?? '').trim())
let currentUrl = normalize(url ?? (match.getCurrentUrl?.() ?? location.href))
if (!currentUrl) {
return false
}
return lastBookmarks.some((bookmark) => normalize(bookmark.url) === currentUrl)
},
checkPageMatch: () => {
let match = settings.pageMatch
if (!match) {
return
}
if (match.isActive && !match.isActive()) {
match.onMatchChange(false)
return
}
match.onMatchChange(widget.isCurrentPageBookmarked())
},
render: (bookmarks) => {
lastBookmarks = Array.isArray(bookmarks) ? bookmarks : []
let query = filterQuery.trim().toLowerCase()
let visible = query ? lastBookmarks.filter((bookmark) =>
bookmark.label.toLowerCase().includes(query) || bookmark.tags.toLowerCase().includes(query)) : lastBookmarks
listEl.empty()
if (lastBookmarks.length === 0) {
emptyEl.text(settings.emptyMessage).toggle(true)
} else if (visible.length === 0) {
emptyEl.text(settings.noMatchMessage).toggle(true)
} else {
emptyEl.toggle(false)
}
for (let bookmark of visible) {
let row = $('<div class="bv-bookmark-row">')
let main = $('<div class="bv-bookmark-main">')
let link = $('<button type="button" class="bv-bookmark-link">').text(bookmark.label).attr('title', bookmark.tags)
link.on('click', () => settings.onNavigate(bookmark.url))
main.append(link)
for (let action of settings.getRowActions(bookmark) ?? []) {
main.append(action)
}
let remove = $('<button type="button" class="bv-bookmark-remove">').attr('title', 'Remove bookmark').text('×')
remove.on('click', (event) => {
event.stopPropagation()
settings.onRemove(bookmark.entryId ?? bookmark.id)
})
row.append(main, remove)
listEl.append(row)
}
syncBookmarkColumnInset()
widget.checkPageMatch()
},
}
syncBookmarkColumnInset()
requestAnimationFrame(() => requestAnimationFrame(syncBookmarkColumnInset))
if (settings.pageMatch) {
let refresh = () => widget.checkPageMatch()
for (let selector of settings.pageMatch.watchSelectors ?? []) {
for (let element of document.querySelectorAll(selector)) {
element.addEventListener('input', refresh)
}
}
refresh()
}
return widget
}
createResizer()
{
return $('<div id="bv-resizer" title="Resize"><span id="bv-resizer-icon">↔</span></div>').
on('mousedown', (event) => {
event.preventDefault()
this._resizing = true
let resizeHorizontal = (event) => {
this._section[0].style.width = Math.max(150, event.clientX - this._section[0].getBoundingClientRect().left) + `px`
}
let stopResize = () => {
removeEventListener('mousemove', resizeHorizontal)
removeEventListener('mouseup', stopResize)
}
addEventListener('mousemove', resizeHorizontal)
addEventListener('mouseup', stopResize)
}).
on('mouseup', (event) => {
this._resizing = false
})
}
createModal(title, bodyNodes)
{
let body = $('<div class="bv-modal-body">')
let nodes = Array.isArray(bodyNodes) ? bodyNodes : [bodyNodes]
for (let node of nodes) {
body.append(node instanceof $ ? node : $(node))
}
let dialog = $('<div class="bv-modal-dialog">').append([
$('<div class="bv-modal-header">').append([
$('<h2 class="bv-modal-title">').text(title),
$('<button type="button" class="bv-modal-close bv-button" aria-label="Close">').text('×'),
]),
body,
])
return $('<div class="bv-modal-overlay">').append(dialog)
}
/**
* @param {JQuery} modal
*/
showModal(modal)
{
let $modal = $(modal)
let hide = () => this.hideModal($modal)
$modal.on('click', (event) => {
if (event.target === $modal[0]) {
hide()
}
})
$modal.find('.bv-modal-close').on('click', hide)
$(document).on('keydown.bvModal', (event) => {
if (event.key === 'Escape') {
hide()
}
})
BrazenViewLayer.appendToBody($modal)
}
/**
* @param {JQuery} modal
*/
hideModal(modal)
{
$(document).off('keydown.bvModal')
$(modal).remove()
}
/**
* Wraps sidebar tag counts with a hover-reveal action slot.
*
* @param {TagSidebarCountEnhancementOptions} options
*/
enhanceTagSidebarCounts(options = {})
{
let settings = {
sidebarSelector: '#tag-sidebar',
rowSelector: 'li[class*="tag-type-"]',
countSelector: 'span.tag-count',
hideDelayMs: 2000,
extractTag: () => ({name: ''}),
renderActions: () => {},
isBookmarked: () => false,
...options,
}
let sidebar = document.querySelector(settings.sidebarSelector)
if (!sidebar) {
return
}
let reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
let hideDelayMs = reducedMotion ? 0 : settings.hideDelayMs
for (let row of sidebar.querySelectorAll(settings.rowSelector)) {
let countSpan = row.querySelector(settings.countSelector)
if (!countSpan) {
continue
}
let slot = countSpan.closest('.bv-tag-actions-slot')
let actions
if (!slot) {
slot = document.createElement('span')
slot.className = 'bv-tag-actions-slot'
countSpan.parentNode.insertBefore(slot, countSpan)
slot.appendChild(countSpan)
actions = document.createElement('span')
actions.className = 'bv-tag-actions'
actions.hidden = true
slot.appendChild(actions)
let hideTimer = null
let showActions = () => {
clearTimeout(hideTimer)
hideTimer = null
actions.hidden = false
countSpan.hidden = true
row.classList.add('bv-tag-row-active')
}
let hideActions = () => {
actions.hidden = true
countSpan.hidden = false
row.classList.remove('bv-tag-row-active')
}
let scheduleHide = () => {
clearTimeout(hideTimer)
hideTimer = setTimeout(hideActions, hideDelayMs)
}
row.addEventListener('pointerenter', showActions)
row.addEventListener('pointerleave', scheduleHide)
} else {
actions = slot.querySelector('.bv-tag-actions')
}
if (!actions) {
continue
}
let tag = settings.extractTag(row)
actions.replaceChildren()
settings.renderActions(row, tag, actions, {isBookmarked: settings.isBookmarked})
}
}
/**
* @return {JQuery}
*/
createSeparator()
{
return $('<hr/>')
}
/**
* @return {JQuery}
*/
createSettingsHideButton()
{
return this.createFormButton('<< Hide', '', () => this._section.css('display', 'none'))
}
/**
* @return {JQuery}
*/
createSettingsSection()
{
return this.createContainer().
attr('id', 'bv-ui').
addClass('bv-dock-slide-panel bv-bg-colour').
hide().
append(this.createResizer())
}
/**
* @return {JQuery}
*/
createComplianceRulesSlidePanel()
{
return this.createContainer().
attr('id', 'bv-compliance-rules').
addClass('bv-dock-slide-panel bv-bg-colour bv-dock-panel-hidden').
css('display', 'none').
append([
$('<div class="bv-dock-slide-panel-head">').append([
$('<h2 class="bv-dock-slide-panel-title">').text('Active Hide Rules'),
$('<button type="button" class="bv-dock-slide-panel-close bv-button" aria-label="Close">').text('×'),
]),
$('<div class="bv-dock-slide-panel-body">'),
])
}
/**
* @param {string} caption
* @param {JQuery} settingsSection
*
* @return {JQuery}
*/
createSettingsShowButton(caption, settingsSection)
{
return $('<button class="show-settings bv-section bv-bg-colour">').
text(caption).
on('click', () => settingsSection.slideDown(300))
}
/**
* @param {string} orientation
* @param {string} nameBarText
* @return {JQuery}
*/
createDock(orientation, nameBarText)
{
return $('<div class="bv-dock bv-dock-' + orientation + '">').
append($('<div class="bv-dock-name">').text(nameBarText)).
append($('<div class="bv-dock-rail">').
append($('<div class="bv-dock-rail-head">')).
append($('<div class="bv-dock-rail-body">')).
append($('<div class="bv-dock-rail-foot">')))
}
/**
* @return {JQuery}
*/
createDockSeparator()
{
return $('<div class="bv-dock-separator" aria-hidden="true">')
}
/**
* @param {string} message
* @return {JQuery}
*/
createDockMigrationStatus(message)
{
return $('<div class="bv-dock-status" role="status" aria-live="polite">').
append($('<span class="bv-dock-status-spinner" aria-hidden="true">')).
append($('<span class="bv-dock-status-text">').text(message))
}
/**
* @param {JQuery} dock
* @param {string|null} message
*/
setDockMigrationStatus(dock, message)
{
if (!dock?.length) {
return
}
let status = dock.find('.bv-dock-status')
if (!message) {
status.remove()
return
}
if (!status.length) {
dock.find('.bv-dock-rail-body').prepend(this.createDockMigrationStatus(message))
return
}
status.find('.bv-dock-status-text').text(message)
}
/**
* @param {{icon?: string|HTMLElement|SVGElement, tooltip?: string, stateClass?: string, onClick?: Function}} options
* @return {JQuery}
*/
createDockButton(options = {})
{
let button = $('<button type="button" class="bv-dock-btn">')
if (options.stateClass) {
button.addClass(options.stateClass)
}
if (options.tooltip) {
button.attr('title', options.tooltip)
}
if (options.icon) {
if (typeof options.icon === 'string') {
button.append(BrazenViewLayer.getDockIcon(options.icon))
} else {
button.append(options.icon)
}
}
if (options.onClick) {
button.on('click', (event) => {
event.preventDefault()
options.onClick(event)
})
}
return button
}
/**
* @param {JQuery} parentButton
* @param {JQuery[]} childButtons
* @return {JQuery}
*/
createDockSlideOut(parentButton, childButtons)
{
return $('<div class="bv-dock-slot">').
append(parentButton).
append($('<div class="bv-dock-slideout">').append(childButtons))
}
/**
* @param {{orientations: string[], value: string, onCycle?: Function}} options
* @return {JQuery}
*/
createDockPositionButton(options = {})
{
let orientationIcons = {left: 'dock-left', right: 'dock-right', bottom: 'dock-bottom'}
let orientationLabels = {left: 'Left', right: 'Right', bottom: 'Bottom'}
let value = options.value ?? options.orientations?.[0] ?? 'right'
let label = orientationLabels[value] ?? value
return this.createDockButton({
icon: orientationIcons[value] ?? 'dock-right',
tooltip: 'Dock position: ' + label + ' (click to change)',
onClick: () => options.onCycle?.(),
}).addClass('bv-dock-position-btn')
}
/**
* @param {string} statisticsType
* @param {string} label
* @return {JQuery}
*/
createStatisticsFormGroup(statisticsType, label = '')
{
if (label === '') {
label = statisticsType.split('-').map((word) => word ? word[0].toUpperCase() + word.slice(1) : '').join(' ')
}
return this.createFormGroup().addClass('bv-stat-group').append([
this.createFormGroupLabel(label + ' Filter'),
this.createFormGroupStatLabel(statisticsType),
])
}
/**
* @return {JQuery}
*/
createStatisticsTotalsGroup()
{
return this.createFormGroup().append([
this.createFormGroupLabel('Total'),
this.createFormGroupStatLabel('Total'),
])
}
/**
* @param {string} tabName
* @param {boolean} isFirst
* @return {JQuery}
*/
createTabButton(tabName, isFirst)
{
let tabButton = $('<button class="bv-tab-button bv-border-primary">').
text(tabName).
on('click', (event) => {
let button = $(event.currentTarget)
let tabSection = button.parents('.bv-tabs-section:first')
tabSection.find('.bv-tab-button').removeClass('bv-active bv-font-secondary').addClass('bv-font-primary')
tabSection.find('.bv-tab-panel').removeClass('bv-active')
button.removeClass('bv-font-primary').addClass('bv-active bv-font-secondary')
$('#' + Utilities.toKebabCase(button.text())).addClass('bv-active')
let root = tabSection.closest('#bv-ui')[0]
root?.userScript?._configurationManager?.updateInterface()
}).
on('mouseenter', (event) => $(event.currentTarget).addClass('bv-font-secondary')).
on('mouseleave', (event) => $(event.currentTarget).removeClass('bv-font-secondary'))
return isFirst ? tabButton.addClass('bv-active bv-font-secondary') : tabButton.addClass('bv-font-primary')
}
/**
* @param {string} tabName
* @param {boolean} isFirst
* @return {JQuery}
*/
createTabPanel(tabName, isFirst = false)
{
let tabPanel = $('<div class="bv-tab-panel bv-border-primary">').attr('id', Utilities.toKebabCase(tabName))
if (isFirst) {
tabPanel.addClass('bv-active')
}
return tabPanel
}
/**
* @param {string[]} tabNames
* @param {JQuery[]} tabPanels
* @return {JQuery}
*/
createTabsSection(tabNames, tabPanels)
{
let tabButtons = []
for (let i = 0; i < tabNames.length; i++) {
tabButtons.push(this.createTabButton(tabNames[i], i === 0))
}
let nav = $('<div class="bv-tabs-nav">').append(tabButtons)
return $('<div class="bv-tabs-section">').append(nav).append(...tabPanels)
}
/**
* @param {string} title
* @return {JQuery}
*/
createTitle(title)
{
return $('<label class="bv-title">' + title + '</label>')
}
/**
* @return {JQuery}
*/
getSelectedSection()
{
return this._section
}
isSettingsPaneBeingResized()
{
return this._resizing
}
/**
* @param {HTMLInputElement} input
* @param {string} text
*/
insertIntoPatternInput(input, text)
{
let start = input.selectionStart ?? input.value.length
let end = input.selectionEnd ?? input.value.length
let next = input.value.slice(0, start) + text + input.value.slice(end)
$(input).val(next).trigger('input')
input.focus()
let caret = start + text.length
input.setSelectionRange(caret, caret)
input.classList.add('bv-pattern-insert-flash')
setTimeout(() => input.classList.remove('bv-pattern-insert-flash'), 300)
}
/**
* @param {{token?: string, insert?: string, label: string, title: string, separator?: boolean, folderOnly?: boolean}} chip
* @return {JQuery}
*/
createPatternTokenChip(chip)
{
let label = chip.label ?? chip.insert ?? ''
let insert = chip.insert ?? chip.label ?? ''
let button = $('<button type="button" class="bv-token-btn">').text(label).attr('title', chip.title)
if (insert) {
button.attr('data-insert', insert)
}
if (chip.separator) {
button.addClass('bv-token-sep')
}
if (chip.folderOnly) {
button.addClass('bv-token-folder-sep').hide()
}
return button
}
/**
* @param {{label: string, chips: {token?: string, insert?: string, label: string, title: string}[]}[]} sections
* @param {{insert: string, label: string, title: string}[]} separators
* @return {JQuery}
*/
createSharedPatternTokenBuilder(sections, separators)
{
let builder = $('<div class="bv-pattern-builder bv-pattern-builder-shared">')
let folderSeparatorButtons = $()
for (let section of sections) {
let row = $('<div class="bv-token-row">')
row.append($('<span class="bv-token-row-label">').text(section.label))
let bar = $('<div class="bv-token-bar">')
for (let chip of section.chips) {
bar.append(this.createPatternTokenChip(chip))
}
row.append(bar)
builder.append(row)
}
let row = $('<div class="bv-token-row">')
row.append($('<span class="bv-token-row-label">').text('Join'))
let bar = $('<div class="bv-token-bar">')
for (let chip of separators) {
let button = this.createPatternTokenChip({...chip, separator: true, folderOnly: chip.insert === '/'})
if (chip.insert === '/') {
folderSeparatorButtons = folderSeparatorButtons.add(button)
}
bar.append(button)
}
row.append(bar)
builder.append(row)
builder.data('folderSeparatorButtons', folderSeparatorButtons)
return builder
}
/**
* @param {HTMLInputElement} filenameInput
* @param {HTMLInputElement} subfolderInput
* @param {JQuery} builder
* @param {string} [eventNamespace]
* @return {{ok: boolean, reason?: string, filename?: boolean, subfolder?: boolean, builder?: boolean}}
*/
patchPatternTokenBuilder(filenameInput, subfolderInput, builder, eventNamespace = 'bvpattern')
{
if (!filenameInput || !subfolderInput || !builder) {
return {
ok: false,
reason: 'pattern-builder-missing',
filename: !!filenameInput,
subfolder: !!subfolderInput,
builder: !!builder,
}
}
let folderSeparatorButtons = $(builder).find('.bv-token-folder-sep')
let activeInput = filenameInput
let activate = (input) => {
activeInput = input
filenameInput.classList.toggle('bv-pattern-active', input === filenameInput)
subfolderInput.classList.toggle('bv-pattern-active', input === subfolderInput)
folderSeparatorButtons.toggle(input === subfolderInput)
}
$(filenameInput).off('focusin.' + eventNamespace).on('focusin.' + eventNamespace, () => activate(filenameInput))
$(subfolderInput).off('focusin.' + eventNamespace).on('focusin.' + eventNamespace, () => activate(subfolderInput))
activate(filenameInput)
$(builder).off('click.' + eventNamespace).on('click.' + eventNamespace, '.bv-token-btn', (event) => {
event.preventDefault()
let button = event.currentTarget
let buttonText = button.textContent.trim()
let insert = button.classList.contains('bv-token-sep')
? button.getAttribute('data-insert')
: buttonText
if (insert === null || insert === '') {
return
}
let target = activeInput ?? filenameInput
this.insertIntoPatternInput(target, insert)
$(button).addClass('bv-token-flash')
setTimeout(() => $(button).removeClass('bv-token-flash'), 180)
})
return {ok: true}
}
/**
* @param {BrazenConfigurationManager} configurationManager
* @param {string[]} configKeys
* @return {{ok: boolean, stacked: string[]}}
*/
patchStackedPatternFieldGroups(configurationManager, configKeys)
{
let stacked = []
for (let key of configKeys) {
let group = configurationManager.getField(key)?.element?.closest('.bv-group')?.[0]
if (!group) {
continue
}
group.classList.add('bv-pattern-group')
stacked.push(key)
}
return {ok: true, stacked}
}
/**
* @return {{ok: boolean, wrapped: number}}
*/
patchRangeInputLayout()
{
let wrapped = 0
for (let group of document.querySelectorAll('#bv-ui .bv-group.bv-range-group')) {
if (group.querySelector('.bv-range-inputs')) {
continue
}
let inputs = group.querySelectorAll(':scope > .bv-input.bv-text')
if (inputs.length !== 2) {
continue
}
let wrapper = document.createElement('div')
wrapper.className = 'bv-range-inputs'
group.insertBefore(wrapper, inputs[0])
wrapper.append(inputs[0], inputs[1])
wrapped++
}
return {ok: true, wrapped}
}
/**
* @param {{onConfirm: Function}} options
* @return {HTMLElement}
*/
static createTagDiscoveryPanel(options = {})
{
let panel = BrazenViewLayer.createComplianceRulesSlidePanel()[0]
panel.id = 'bv-tag-discovery-panel'
panel.querySelector('.bv-dock-slide-panel-title').textContent = 'Tag Discovery'
panel.querySelector('.bv-dock-slide-panel-close').addEventListener('click', () => {
panel.classList.add('bv-dock-panel-hidden')
})
let body = panel.querySelector('.bv-dock-slide-panel-body')
body.innerHTML = ''
body.append(
$('<p class="bv-tag-discovery-intro">').text('New tags were found. Review actions, then confirm to continue.')[0],
$('<ul class="bv-tag-discovery-list">')[0],
$('<div class="bv-tag-discovery-footer">').append(
$('<button type="button" class="bv-button">').text('Confirm mappings').on('click', () => options.onConfirm?.()),
)[0],
)
return panel
}
/**
* @param {HTMLElement} panel
* @param {Array<{name: string, type: string|null}>} tags
* @param {function(HTMLElement, {name: string, type: string|null}, HTMLElement): void} renderActions
*/
static renderTagDiscoveryPanelContent(panel, tags, renderActions)
{
let list = panel.querySelector('.bv-tag-discovery-list')
if (!list) {
return
}
list.innerHTML = ''
for (let tag of tags) {
let row = document.createElement('li')
row.className = 'bv-tag-discovery-row'
let name = document.createElement('span')
name.className = 'bv-tag-discovery-name'
name.textContent = tag.name
let actions = document.createElement('span')
actions.className = 'bv-tag-discovery-actions'
row.append(name, actions)
renderActions(row, tag, actions)
list.append(row)
}
}
/**
* @param {HTMLElement} row
* @param {{name: string, type: string|null}} tag
* @param {HTMLElement} actionsElement
* @param {{renderButtons: function(HTMLElement, {name: string, type: string|null}, HTMLElement, object): void, getRowState?: function({name: string, type: string|null}): {muted?: boolean, hidden?: boolean}}} config
*/
static renderTagActions(row, tag, actionsElement, config)
{
actionsElement.innerHTML = ''
config.renderButtons(row, tag, actionsElement, config.helpers ?? {})
let state = config.getRowState?.(tag) ?? {}
row.classList.toggle('bv-tag-muted', !!state.muted)
row.classList.toggle('bv-tag-hidden', !!state.hidden)
}
/**
* @return {JQuery}
*/
static createDownloadManagerProgressSlot()
{
return $('<div class="bv-dock-progress-slot bv-dock-progress-hidden">').append([
$('<div class="bv-dock-progress-bar">').append($('<span>')),
$('<div class="bv-dock-progress-label">'),
])
}
/**
* @param {JQuery} slot
* @param {{resolution: {current: number, total: number}, download: {current: number, total: number}}} progress
*/
static updateDownloadManagerProgressSlot(slot, progress)
{
let download = progress.download ?? {current: 0, total: 0}
let resolution = progress.resolution ?? {current: 0, total: 0}
let total = Math.max(download.total, resolution.total)
let current = download.current + resolution.current
let percent = total > 0 ? Math.round((current / total) * 100) : 0
slot.find('.bv-dock-progress-bar > span').css('width', percent + '%')
slot.find('.bv-dock-progress-label').text(`${current}/${total}`)
}
}