// ==UserScript==
// @name CICD Sort Tool
// @namespace lfyou
// @version 1.5
// @description i want sort!
// @author lfyou
// @include /^https?:.*?\/console-.*?/
// @icon https://cdn.icon-icons.com/icons2/2148/PNG/512/monkey_icon_132188.png
// @license MIT
// ==/UserScript==
(function() {
'use strict';
console.log('hello, sort monkey')
class LocalConfig {
storageKey = 'SORT_MONKEY'
getSuffix(){
return ''
}
constructor(getSuffix){
if(getSuffix) {
this.getSuffix = getSuffix
}
try{
this.config = JSON.parse(localStorage.getItem(this.storageKey)) || {}
}catch{
this.config = {}
}
}
getPageKey(){
const prefix = location.host + location.pathname
return prefix + '?' + this.getSuffix()
}
getConfig(){
return this.config[this.getPageKey()] || []
}
setConfig(key,index){
const pageKey = this.getPageKey()
const currentConfig = (this.config[pageKey] = this.config[pageKey] || [])
const originIdx = currentConfig.indexOf(key)
if(originIdx >= 0){
currentConfig.splice(originIdx,1)
}
if(Number.isInteger(index)){
currentConfig.splice(index,0,key)
}else{
currentConfig.push(key)
}
this.updateLocalStorage()
}
removeConfig(key){
const currentConfig = this.config[this.getPageKey()]
if(!currentConfig) return
const idx = currentConfig.indexOf(key)
if(idx < 0) return
currentConfig.splice(idx,1)
this.updateLocalStorage()
}
updateGoing = false
updateLocalStorage(){
if(!this.updateGoing){
setTimeout(() => {
localStorage.setItem(this.storageKey, JSON.stringify(this.config))
this.updateGoing = false
})
}
this.updateGoing = true
}
}
class Configure {
wrapper = document
panel = null
constructor(panel,getKey){
this.panel = panel
this.getKey = getKey
}
register(wrapper){
if(!wrapper) return
this.unRegisterEventListener()
this.wrapper = wrapper
this.registerEventListener()
}
getItem(t){
while(t && t.parentNode !== this.wrapper){
t = t.parentNode
}
return t
}
getActiveKey(wrapper){
return wrapper ? this.getKey?.(wrapper) : null
}
registerEventListener(){
this.wrapper.addEventListener('contextmenu',this.registedEvent)
}
unRegisterEventListener(){
this.wrapper?.removeEventListener('contextmenu',this.registedEvent)
}
registedEvent = (e) => {
if(e.ctrlKey) {
return
}
e.preventDefault()
const activeKey = this.getActiveKey(this.getItem(e.target))
if(!activeKey) return
this.panel?.create(e,activeKey)
}
}
class Panel {
constructor(localConfig,sort){
this.localConfig = localConfig
this.sort = sort
}
create(e, activeKey){
if(!(this.activeKey = activeKey)) return
const {pageX:x,pageY:y} = e
const {body} = document
const gp = this.generatePanel(x,y)
body.appendChild(gp)
}
generatePanel(x,y){
const generatePanel = document.createElement('div')
generatePanel.style.zIndex = '100'
generatePanel.style.width = '100px'
generatePanel.style.height = '100px'
generatePanel.style.backdropFilter = 'blur(3px)'
generatePanel.style.borderRadius = '8px'
generatePanel.style.boxShadow = 'rgba(0,0,0,.2) 0 1px 5px 0px'
generatePanel.style.position = 'absolute'
generatePanel.style.left = x - 10 + 'px'
generatePanel.style.top = y - 5 + 'px'
const content = this.generateContent()
generatePanel.appendChild(content)
this.removeListener(generatePanel)
return generatePanel
}
generateContent(){
const wrapper = document.createElement('div')
wrapper.style.width = '80%'
wrapper.style.height = '60%'
wrapper.style.paddingLeft = '6px'
wrapper.style.display = 'flex'
wrapper.style.flexDirection = 'column'
wrapper.style.justifyContent = 'space-around'
wrapper.style.position = 'absolute'
wrapper.style.top = '50%'
wrapper.style.left = '50%'
wrapper.style.transform = 'translate(-50%, -50%)'
const collectionControl = this.generateControl(this.generateLabel('收藏'),this.generateCheckbox())
wrapper.appendChild(collectionControl)
const indexControl = this.generateControl(this.generateLabel('排序'),this.generateSelect())
this.setIndexControlInitStatus(indexControl)
wrapper.appendChild(indexControl)
this.registerControlEvent(collectionControl,indexControl)
return wrapper
}
setIndexControlInitStatus(indexControl){
const config = this.localConfig.getConfig()
if(config.includes(this.activeKey)){
indexControl.hidden = false
}else{
indexControl.hidden = true
}
}
generateControl(...args){
const wrapper = document.createElement('div')
wrapper.style.display = 'flex'
args.forEach(a => wrapper.appendChild(a))
return wrapper
}
generateLabel(text){
const label = document.createElement('label')
label.style.marginRight = '8px'
label.innerText = text
return label
}
generateCheckbox(){
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
const config = this.localConfig.getConfig()
if(config.includes(this.activeKey)){
checkbox.checked = true
}
return checkbox
}
generateSelect(){
const select = document.createElement('select')
select.style.backgroundColor = 'inherit'
const config = this.localConfig.getConfig()
const length = config.length
for(let i = 0; i < length; i++){
select.appendChild(generateOption(i))
}
const index = config.indexOf(this.activeKey)
if(index >=0){
select.value = index
}else{
select.appendChild(generateOption(length))
select.value = length
}
function generateOption(i){
const option = document.createElement('option')
option.value = i
option.innerText = i
return option
}
return select
}
registerControlEvent(collectionControl,indexControl){
this.collectionControl = collectionControl
this.indexControl = indexControl
collectionControl.addEventListener('click',(e) => {
if(e.target.checked){
this.localConfig.setConfig(this.activeKey)
this.sort.manualSort()
indexControl.hidden = false
}else{
this.localConfig.removeConfig(this.activeKey)
this.sort.manualSort()
indexControl.hidden = true
}
})
indexControl.addEventListener('change',(e) => {
this.localConfig.setConfig(this.activeKey,Number(e.target.value))
this.sort.manualSort()
})
}
removeListener(panel){
panel.addEventListener('mouseleave',(e)=>{
// 修复firefox 的select下拉框回触发mouseleave的bug
if(e.explicitOriginalTarget === panel){
return
}
document.body.removeChild(panel)
})
}
}
class Sort {
canAutomaticallySelect(){
return true
}
constructor(localConfig,markHelper,getKey,canAutomaticallySelect){
this.localConfig = localConfig
this.markHelper = markHelper
this.getKey = getKey
if(canAutomaticallySelect){
this.canAutomaticallySelect = canAutomaticallySelect
}
}
register(wrapper){
if(!wrapper) return
this.wrapper = wrapper
this.sortGoing = false
this.initializedSort()
this.registerObservedSort()
}
initializedSort(){
this._sort(true)
}
registerObservedSort(){
const observe = new MutationObserver(() => {
this._sort()
})
observe.observe(this.wrapper,{childList:true})
}
// 更新收藏后手动触发sort
manualSort(){
this._sort()
}
sortGoing = false
_sort(selectFirst){
if(this.sortGoing) return
requestAnimationFrame(() => {
const sorted = this._sortCore()
if(selectFirst && this.canAutomaticallySelect()){
this.autoSelectFirst(sorted)
}
this.sortGoing = false
})
this.sortGoing = true
}
_sortCore(){
const children = Array.from(this.wrapper.childNodes)
const markedChild = []
const config = this.localConfig.getConfig()
// li未加载成功是,直接return
if(children.map(child => this.getKey(child)).every(_ => !_)) return
let empty = 0
for(let i = 0,l = config.length; i < l; i++){
for(let j = 0, ll = children.length; j < ll; j++){
const child = children[j]
if(config[i] === this.getKey(child)){
// 若key匹配,则更新标记
this.markHelper.mark(child,i)
markedChild.push(child)
// 若当前位置和预期位置不符,则更新dom位置
if(i - empty !== j){
this.wrapper.insertBefore(child,children[i - empty])
children.splice(j,1)
// (j < i ? -1 : 0),若当前位置在预期位置之前,则从children移除当前节点后,插入位置的index需要相应的 -1
children.splice(i - empty + (j < i ? -1 : 0),0,child)
}
break;
}
if(j === ll -1){
empty++
}
}
}
// 移除样式还存在问题
children.filter(child => !markedChild.includes(child)).forEach(item => this.markHelper.remove(item))
return children
}
autoSelectFirst(children){
// 自动选中第一项
children.at(0).click()
}
}
class MarkHelper {
markClass = 'marked'
constructor(showIndex){
this.showIndex = showIndex
}
mark(wrapper,index){
if(wrapper.marked) {
const sign = wrapper.querySelector(`.${this.markClass}`)
if(sign && sign.textContent !== index + ''){
sign.innerText = index
}
return
}
const sign = this.generateSign(index)
this.toggleWrapperStyle(wrapper, sign, true)
}
toggleWrapperStyle(wrapper,sign,isMark){
wrapper.marked = isMark
wrapper.style.position = isMark ? 'relative' : ''
wrapper.style.overflow = isMark ? 'hidden' : ''
if(isMark){
wrapper.appendChild(sign)
}else{
wrapper.removeChild(sign)
}
}
generateSign(index){
const sign = document.createElement('div')
sign.style.textAlign = "center"
sign.style.lineHeight = "14px"
sign.style.color = "white"
sign.style.fontSize = "12px"
sign.style.backgroundColor = "#ebeb0080"
sign.style.width = "66px"
sign.style.height = "14px"
sign.style.position = "absolute"
sign.style.top = "10px"
sign.style.right = "0px"
sign.style.transform = "rotate(43deg) translate(11px, -22px)"
sign.innerText = index
if(!this.showIndex){
sign.style.color = 'transparent'
}
sign.classList.add(this.markClass)
return sign
}
remove(wrapper){
if(wrapper.marked) {
const sign = wrapper.querySelector(`.${this.markClass}`)
if(!sign) return
this.toggleWrapperStyle(wrapper, sign, false)
}
}
}
class Register {
constructor({wrapperSelector,getKey,getSuffix,canAutomaticallySelect}){
// 容器的选择器
this.wrapperSelector = wrapperSelector
// item生成key的回调函数
this.getKey = getKey
// url中需要保留的search参数
this.getSuffix = getSuffix
// 是否允许自动选中第一项
this.canAutomaticallySelect = canAutomaticallySelect
this.showIndex = false
this.initPlugin()
this.initObserve()
}
initPlugin(){
this.markHelper = new MarkHelper(this.showIndex)
this.localConfig = new LocalConfig(this.getSuffix)
this.sort = new Sort(this.localConfig, this.markHelper, this.getKey, this.canAutomaticallySelect)
this.panel = new Panel(this.localConfig, this.sort)
this.configure = new Configure(this.panel, this.getKey)
}
initObserve(){
this.registerObserve = new MutationObserver(() => {
const wrapper = document.querySelector(this.wrapperSelector)
if(!wrapper || this.wrapperCache === wrapper ) return
this.wrapperCache = wrapper
this.sort.register(wrapper)
this.configure.register(wrapper)
})
}
register(){
this.registerObserve.observe(document.body,{childList:true, subtree:true})
return this
}
}
window.sortMonkey = new Register({
wrapperSelector: 'alo-cicd-page-state-wapper > ul',// 列表容器选择器
getKey: (item) => item?.firstChild?.firstChild?.textContent, // 列表项生成key的回调函数
getSuffix:() => {
const params= ['namespace','cluster']
const search = location.search.slice(1).split('&')
const res = []
for(const p of params){
const find = search.find(s => s.includes(p))
if(find) res.push(find)
}
return res.join('&')
},// 收藏列表的保存区域后缀,用来区分不同参数下的列表
canAutomaticallySelect: () => !location.search.match(/buildName|delivery/) // 满足条件时,可允许自动选中列表第一项
}).register()
})();