View layer for the Brazen userscripts framework
Този скрипт не може да бъде инсталиран директно. Това е библиотека за други скриптове и може да бъде използвана с мета-директива // @require https://update.sleazyfork.org/scripts/416104/1847306/Brazen%20Framework%20-%20View%20Layer.js
// ==UserScript==
// @name Brazen Framework - View Layer
// @namespace brazenvoid
// @version 3.1.1
// @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-link{box-sizing:border-box;display:block;grid-column:1;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-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}`)
class BrazenViewLayer
{
/**
* @type {boolean}
* @private
*/
_resizing = false
/**
* @type {JQuery}
* @private
*/
_section = null
/**
* @type {SelectorGenerator}
* @private
*/
_selectorGenerator
/**
* @type {string}
* @private
*/
_selectorPrefix
/**
* @param {string} selectorPrefix
*/
constructor(selectorPrefix)
{
this._selectorGenerator = new SelectorGenerator(selectorPrefix)
this._selectorPrefix = selectorPrefix
}
/**
* @param {JQuery} nodes
*/
static appendToBody(nodes)
{
$('body').append(nodes)
}
/**
* @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),
])
}
/**
* @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,
* }} BrazenBookmarksPanelOptions
*/
/**
* @typedef {{
* element: JQuery,
* render: function(BrazenBookmarkEntry[]): void,
* getFilterQuery: function(): string,
* isCurrentPageBookmarked: function(string=): boolean,
* checkPageMatch: function(): void,
* }} BrazenBookmarksPanel
*/
/**
* 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
},
...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 link = $('<button type="button" class="bv-bookmark-link">').text(bookmark.label).attr('title', bookmark.tags)
link.on('click', () => settings.onNavigate(bookmark.url))
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(link, 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()
}
/**
* @typedef {{
* sidebarSelector?: string,
* rowSelector?: string,
* countSelector?: string,
* hideDelayMs?: number,
* extractTag?: function(Element): *,
* renderActions?: function(Element, *, Element, *): void,
* isBookmarked?: function(string): boolean,
* }} TagSidebarCountEnhancementOptions
*/
/**
* 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-bg-colour').
hide().
append(this.createResizer())
}
/**
* @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} 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
}
}