View layer for the Brazen user scripts framework
اعتبارا من
لا ينبغي أن لا يتم تثبيت هذا السكريت مباشرة. هو مكتبة لسكبتات لتشمل مع التوجيه الفوقية // @require https://update.sleazyfork.org/scripts/416104/1875055/Brazen%20Framework%20-%20View%20Layer.js
// ==UserScript==
// @name Brazen Framework - View Layer
// @namespace brazenvoid
// @version 3.4.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-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.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 = '')
{
return this.createFormGroup().addClass('bv-stat-group').append([
this.createFormGroupLabel((label === '' ? statisticsType : 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}
}
}