@ -1602,7 +1602,7 @@ HTML = """<!doctype html>
< button id = " configResetLayout " type = " button " > сбросить раскладку < / button >
< button id = " configRestoreToggle " type = " button " aria - expanded = " false " > структура < / button >
< label class = " config-scale-control " title = " Масштаб элементов и текста выбранного блока: 50– 200 % " > < span id = " configScaleLabel " > масштаб блока < / span > < input id = " configContentScale " type = " range " min = " 50 " max = " 200 " step = " 5 " value = " 100 " aria - label = " Масштаб содержимого выбранного блока, от 50 до 200 процентов " disabled > < output id = " configContentScaleValue " > 100 % < / output > < / label >
< span id = " configLayoutState " class = " config-layout-state " > перетаскивайте за заголовок или свободное место < / span >
< span id = " configLayoutState " class = " config-layout-state " > Ctrl/ Cmd + клик — выбрать несколько · перетаскивайте группу < / span >
< / div >
< div id = " configWorkspace " class = " config-workspace " >
< div id = " configRestorePanel " class = " config-restore-panel " hidden >
@ -1621,7 +1621,7 @@ HTML = """<!doctype html>
< / label >
< button id = " configCreateBlock " type = " button " > создать блок < / button >
< / div >
< span class = " config-structure-hint " > Клик по строке фокусирует блок . Перетаскивайте на canvas , меняйте размер границей , удаляйте через корзину . < / span >
< span class = " config-structure-hint " > Клик фокусирует блок . Ctrl / Cmd + клик выбирает несколько . Перетаскивайте группу на canvas или в корзину . < / span >
< div id = " configRestoreList " class = " config-restore-list " > < / div >
< button id = " configDeletedToggle " class = " config-deleted-toggle " type = " button " hidden > корзина < / button >
< div id = " configDeletedList " class = " config-deleted-list " hidden > < / div >
@ -1827,7 +1827,7 @@ HTML = """<!doctype html>
< button id = " networkResetLayout " type = " button " > сбросить раскладку < / button >
< button id = " networkStructureToggle " type = " button " aria - expanded = " false " > структура < / button >
< label class = " config-scale-control " title = " Масштаб элементов и текста выбранного сетевого блока " > < span > масштаб блока < / span > < input id = " networkContentScale " type = " range " min = " 50 " max = " 200 " step = " 5 " value = " 100 " aria - label = " Масштаб содержимого выбранного сетевого блока " disabled > < output id = " networkContentScaleValue " > 100 % < / output > < / label >
< span id = " networkLayoutState " class = " config-layout-state " > перетаскивайте блоки за свободное место < / span >
< span id = " networkLayoutState " class = " config-layout-state " > Ctrl/ Cmd + клик — выбрать несколько · перетаскивайте группу < / span >
< / div >
< div id = " networkWorkspace " class = " network-workspace " >
< aside id = " networkRestorePanel " class = " config-restore-panel network-restore-panel " hidden >
@ -2092,6 +2092,7 @@ HTML = """<!doctype html>
let configContentScale = 1 ;
let configBlockObserver = null ;
let selectedConfigBlock = null ;
let selectedConfigBlocks = new Set ( ) ;
function readConfigLayout ( ) {
try {
const saved = JSON . parse ( localStorage . getItem ( ' fpv-config-layout-v1 ' ) | | ' {} ' ) ;
@ -2135,23 +2136,34 @@ HTML = """<!doctype html>
saveConfigLayout ( ) ;
}
function selectConfigBlock ( block ) {
if ( ! block | | configBlockState ( block ) ) {
selectedConfigBlock = null ;
configBlocks . forEach ( other = > other . classList . remove ( ' is-selected ' ) ) ;
const control = $ ( ' configContentScale ' ) ;
selectedConfigBlocks = new Set ( block & & ! configBlockState ( block ) ? [ block ] : [ ] ) ;
updateConfigSelection ( block ) ;
}
function updateConfigSelection ( preferred = selectedConfigBlock ) {
selectedConfigBlocks = new Set ( [ . . . selectedConfigBlocks ] . filter ( block = > configBlocks . includes ( block ) & & ! configBlockState ( block ) ) ) ;
selectedConfigBlock = selectedConfigBlocks . has ( preferred ) ? preferred : [ . . . selectedConfigBlocks ] . at ( - 1 ) | | null ;
configBlocks . forEach ( other = > other . classList . toggle ( ' is-selected ' , selectedConfigBlocks . has ( other ) ) ) ;
$ ( ' configRestorePanel ' ) ? . querySelectorAll ( ' .config-restore-item ' ) . forEach ( item = > item . classList . toggle ( ' is-selected ' , selectedConfigBlocks . has ( item . _configBlock ) ) ) ;
const control = $ ( ' configContentScale ' ) ;
if ( ! selectedConfigBlock ) {
if ( control ) control . disabled = true ;
return ;
}
selectedConfigBlock = block ;
configBlocks . forEach ( other = > other . classList . toggle ( ' is-selected ' , other == = block ) ) ;
const entry = configLayout [ block . dataset . section ] | | { } ;
const entry = configLayout [ selectedConfigBlock . dataset . section ] | | { } ;
const value = Math . round ( ( Number ( entry . scale ) | | 100 ) ) ;
configContentScale = Math . max ( .5 , Math . min ( 2 , value / 100 ) ) ;
setValue ( ' configContentScale ' , value ) ;
setText ( ' configContentScaleValue ' , ` $ { value } % ` ) ;
const control = $ ( ' configContentScale ' ) ;
if ( control ) control . disabled = false ;
updateConfigBlockScale ( block ) ;
if ( control ) control . disabled = selectedConfigBlocks . size != = 1 ;
updateConfigBlockScale ( selectedConfigBlock ) ;
if ( selectedConfigBlocks . size > 1 ) setText ( ' configLayoutState ' , ` выбрано $ { selectedConfigBlocks . size } блока · перетаскивайте группу ` ) ;
}
function toggleConfigBlockSelection ( block , additive = false ) {
if ( ! block | | configBlockState ( block ) ) return ;
if ( ! additive ) return selectConfigBlock ( block ) ;
if ( selectedConfigBlocks . has ( block ) ) selectedConfigBlocks . delete ( block ) ;
else selectedConfigBlocks . add ( block ) ;
updateConfigSelection ( block ) ;
}
function configTitle ( block ) {
return block . querySelector ( ' :scope > summary .block-title ' ) ? . textContent | | block . dataset . section | | ' Блок ' ;
@ -2283,29 +2295,39 @@ HTML = """<!doctype html>
if ( fitted ) saveConfigLayout ( ) ;
updateConfigCanvasHeight ( ) ;
}
function setConfigBlockState ( block , state ) {
if ( ! block ) return ;
if ( state & & block . contains ( document . activeElement ) ) document . activeElement . blur ( ) ;
const key = block . dataset . section ;
if ( key & & state ) configLayout [ key ] = { . . . ( configLayout [ key ] | | { } ) , state } ;
else if ( key & & configLayout [ key ] ) {
delete configLayout [ key ] . state ;
if ( ! Object . keys ( configLayout [ key ] ) . length ) delete configLayout [ key ] ;
}
block . dataset . blockState = state | | ' ' ;
block . classList . toggle ( ' config-block-hidden ' , state == = ' hidden ' ) ;
block . classList . toggle ( ' config-block-deleted ' , state == = ' deleted ' ) ;
if ( state ) block . setAttribute ( ' aria-hidden ' , ' true ' ) ;
else block . removeAttribute ( ' aria-hidden ' ) ;
function setConfigBlockStates ( blocks , state ) {
const items = [ . . . new Set ( blocks ) ] . filter ( block = > block & & configBlocks . includes ( block ) ) ;
if ( ! items . length ) return ;
items . forEach ( block = > {
if ( state & & block . contains ( document . activeElement ) ) document . activeElement . blur ( ) ;
const key = block . dataset . section ;
if ( key & & state ) configLayout [ key ] = { . . . ( configLayout [ key ] | | { } ) , state } ;
else if ( key & & configLayout [ key ] ) {
delete configLayout [ key ] . state ;
if ( ! Object . keys ( configLayout [ key ] ) . length ) delete configLayout [ key ] ;
}
block . dataset . blockState = state | | ' ' ;
block . classList . toggle ( ' config-block-hidden ' , state == = ' hidden ' ) ;
block . classList . toggle ( ' config-block-deleted ' , state == = ' deleted ' ) ;
if ( state ) block . setAttribute ( ' aria-hidden ' , ' true ' ) ;
else block . removeAttribute ( ' aria-hidden ' ) ;
} ) ;
saveConfigLayout ( ) ;
applyStoredConfigLayout ( ) ;
renderConfigRestorePanel ( ) ;
selectConfigBlock ( configBlocks . find ( candidate = > ! configBlockState ( candidate ) ) | | null ) ;
}
function focusConfigBlock ( block , item ) {
function setConfigBlockState ( block , state ) { setConfigBlockStates ( [ block ] , state ) ; }
function focusConfigBlock ( block , item , event ) {
if ( ! block | | configBlockState ( block ) ) return ;
if ( event ? . ctrlKey | | event ? . metaKey ) {
toggleConfigBlockSelection ( block , true ) ;
item ? . classList . toggle ( ' is-selected ' , selectedConfigBlocks . has ( block ) ) ;
bringConfigBlockFront ( block ) ;
return ;
}
configBlocks . forEach ( other = > other . classList . remove ( ' is-selected ' ) ) ;
document . querySelectorAll ( ' .config-restore-item.is-selected ' ) . forEach ( node = > node . classList . remove ( ' is-selected ' ) ) ;
$ ( ' configRestorePanel ' ) ? . querySelectorAll ( ' .config-restore-item.is-selected ' ) . forEach ( node = > node . classList . remove ( ' is-selected ' ) ) ;
selectConfigBlock ( block ) ;
item ? . classList . add ( ' is-selected ' ) ;
bringConfigBlockFront ( block ) ;
@ -2314,7 +2336,8 @@ HTML = """<!doctype html>
function renderConfigItem ( list , block , deleted = false ) {
const state = configBlockState ( block ) ;
const item = document . createElement ( ' div ' ) ;
item . className = ` config - restore - item $ { state ? ' is-hidden ' : ' ' } ` ;
item . className = ` config - restore - item $ { state ? ' is-hidden ' : ' ' } $ { selectedConfigBlocks . has ( block ) ? ' is-selected ' : ' ' } ` ;
item . _configBlock = block ;
item . title = ' Нажмите, чтобы показать блок на canvas ' ;
const label = document . createElement ( ' span ' ) ;
label . className = ' config-item-title ' ;
@ -2353,7 +2376,7 @@ HTML = """<!doctype html>
actions . appendChild ( remove ) ;
}
item . append ( label , stateNode , actions ) ;
item . addEventListener ( ' click ' , ( ) = > focusConfigBlock ( block , item ) ) ;
item . addEventListener ( ' click ' , event = > focusConfigBlock ( block , item , event ) ) ;
list . appendChild ( item ) ;
}
function renderConfigRestorePanel ( ) {
@ -2384,7 +2407,8 @@ HTML = """<!doctype html>
panel . hidden = ! restorePanelOpen ;
workspace ? . classList . toggle ( ' has-structure ' , restorePanelOpen ) ;
const visible = configBlocks . length - hidden . length - deleted . length ;
setText ( ' configLayoutState ' , ` $ { visible } блоков · $ { hidden . length } скрыто · $ { deleted . length } в корзине ` ) ;
const selected = selectedConfigBlocks . size > 1 ? ` · выбрано $ { selectedConfigBlocks . size } ` : ' ' ;
setText ( ' configLayoutState ' , ` $ { visible } блоков · $ { hidden . length } скрыто · $ { deleted . length } в корзине $ { selected } ` ) ;
if ( configCanvas ) applyStoredConfigLayout ( ) ;
}
function removeConfigBlockForever ( block ) {
@ -2484,7 +2508,7 @@ HTML = """<!doctype html>
trash . hidden = false ;
trash . classList . remove ( ' is-over ' ) ;
}
function finishConfigDrag ( block , event , dragged ) {
function finishConfigDrag ( block , event , dragged , group = [ block ] ) {
const trash = $ ( ' configTrash ' ) ;
const droppedOnTrash = trashDrop ( event ) ;
if ( trash ) {
@ -2492,14 +2516,16 @@ HTML = """<!doctype html>
trash . hidden = true ;
}
if ( ! dragged ) return ;
captureConfigLayout( block ) ;
if ( droppedOnTrash ) setConfigBlockState ( block , ' deleted ' ) ;
group. forEach ( item = > captureConfigLayout ( item ) ) ;
if ( droppedOnTrash ) setConfigBlockState s( group , ' deleted ' ) ;
else {
saveConfigLayout ( ) ;
setText ( ' configLayoutState ' , ` $ { configBlocks . length - configBlocks . filter ( item = > configBlockState ( item ) ) . length } блоков · раскладка сохранена ` ) ;
}
block . _suppressSummaryClick = true ;
window . setTimeout ( ( ) = > { block . _suppressSummaryClick = false ; } , 0 ) ;
group . forEach ( item = > {
item . _suppressSummaryClick = true ;
window . setTimeout ( ( ) = > { item . _suppressSummaryClick = false ; } , 0 ) ;
} ) ;
}
function makeConfigBlockInteractive ( block ) {
const summary = block . firstElementChild ;
@ -2522,9 +2548,16 @@ HTML = """<!doctype html>
block . open = true ;
} ) ;
block . addEventListener ( ' pointerdown ' , event = > {
selectConfigBlock ( block ) ;
bringConfigBlockFront ( block ) ;
const additive = event . ctrlKey | | event . metaKey ;
const edge = configResizeEdge ( event , block ) ;
if ( additive & & ! edge & & ! configDragBlocked ( block , event ) ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
toggleConfigBlockSelection ( block , true ) ;
return ;
}
if ( edge | | ! selectedConfigBlocks . has ( block ) ) selectConfigBlock ( block ) ;
bringConfigBlockFront ( block ) ;
if ( ! edge | | configDragBlocked ( block , event ) ) return ;
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
@ -2603,8 +2636,10 @@ HTML = """<!doctype html>
if ( event . button != = 0 | | ! configCanvas | | configDragBlocked ( block , event ) ) return ;
const startX = event . clientX ;
const startY = event . clientY ;
const startLeft = parseFloat ( block . style . left ) | | block . offsetLeft | | 0 ;
const startTop = parseFloat ( block . style . top ) | | block . offsetTop | | 0 ;
const group = selectedConfigBlocks . has ( block )
? [ . . . selectedConfigBlocks ] . filter ( item = > ! configBlockState ( item ) )
: [ block ] ;
const origins = group . map ( item = > ( { item , left : parseFloat ( item . style . left ) | | item . offsetLeft | | 0 , top : parseFloat ( item . style . top ) | | item . offsetTop | | 0 } ) ) ;
let dragged = false ;
showConfigTrash ( ) ;
block . setPointerCapture ? . ( event . pointerId ) ;
@ -2613,12 +2648,18 @@ HTML = """<!doctype html>
if ( ! dragged ) {
dragged = true ;
event . preventDefault ( ) ;
block . classList . add ( ' is-dragging ' ) ;
group. forEach ( item = > item . classList . add ( ' is-dragging ' ) ) ;
}
event . preventDefault ( ) ;
const maxLeft = Math . max ( 0 , configCanvasWidth ( ) - block . offsetWidth ) ;
block . style . left = ` $ { Math . max ( 0 , Math . min ( maxLeft , Math . round ( startLeft + moveEvent . clientX - startX ) ) ) } px ` ;
block . style . top = ` $ { Math . max ( 0 , Math . round ( startTop + moveEvent . clientY - startY ) ) } px ` ;
const rawDx = moveEvent . clientX - startX ;
const minDx = - Math . min ( . . . origins . map ( origin = > origin . left ) ) ;
const maxDx = Math . min ( . . . origins . map ( origin = > configCanvasWidth ( ) - origin . item . offsetWidth - origin . left ) ) ;
const dx = Math . max ( minDx , Math . min ( maxDx , rawDx ) ) ;
const dy = moveEvent . clientY - startY ;
origins . forEach ( origin = > {
origin . item . style . left = ` $ { Math . max ( 0 , Math . round ( origin . left + dx ) ) } px ` ;
origin . item . style . top = ` $ { Math . max ( 0 , Math . round ( origin . top + dy ) ) } px ` ;
} ) ;
setTrashHover ( moveEvent ) ;
updateConfigCanvasHeight ( ) ;
} ;
@ -2627,8 +2668,8 @@ HTML = """<!doctype html>
block . removeEventListener ( ' pointermove ' , move ) ;
block . removeEventListener ( ' pointerup ' , end ) ;
block . removeEventListener ( ' pointercancel ' , end ) ;
block . classList . remove ( ' is-dragging ' ) ;
finishConfigDrag ( block , endEvent , dragged );
group. forEach ( item = > item . classList . remove ( ' is-dragging ' ) ) ;
finishConfigDrag ( block , endEvent , dragged , group );
} ;
block . addEventListener ( ' pointermove ' , move ) ;
block . addEventListener ( ' pointerup ' , end , { once : true } ) ;
@ -2734,6 +2775,7 @@ HTML = """<!doctype html>
let networkLayout = { } ;
let networkZIndex = 1 ;
let networkSelectedBlock = null ;
let selectedNetworkBlocks = new Set ( ) ;
let networkStructureOpen = false ;
let networkContentScale = 1 ;
function readNetworkLayout ( ) {
@ -2769,22 +2811,33 @@ HTML = """<!doctype html>
block . style . setProperty ( ' --block-scale ' , Math . max ( .5 , Math . min ( 2 , Math . sqrt ( area ) * block . _networkUserScale ) ) . toFixed ( 3 ) ) ;
}
function selectNetworkBlock ( block ) {
if ( ! block | | networkState ( block ) ) {
networkSelectedBlock = null ;
networkBlocks . forEach ( item = > item . classList . remove ( ' is-selected ' ) ) ;
const control = $ ( ' networkContentScale ' ) ;
selectedNetworkBlocks = new Set ( block & & ! networkState ( block ) ? [ block ] : [ ] ) ;
updateNetworkSelection ( block ) ;
}
function updateNetworkSelection ( preferred = networkSelectedBlock ) {
selectedNetworkBlocks = new Set ( [ . . . selectedNetworkBlocks ] . filter ( block = > networkBlocks . includes ( block ) & & ! networkState ( block ) ) ) ;
networkSelectedBlock = selectedNetworkBlocks . has ( preferred ) ? preferred : [ . . . selectedNetworkBlocks ] . at ( - 1 ) | | null ;
networkBlocks . forEach ( item = > item . classList . toggle ( ' is-selected ' , selectedNetworkBlocks . has ( item ) ) ) ;
$ ( ' networkRestorePanel ' ) ? . querySelectorAll ( ' .config-restore-item ' ) . forEach ( item = > item . classList . toggle ( ' is-selected ' , selectedNetworkBlocks . has ( item . _networkBlock ) ) ) ;
const control = $ ( ' networkContentScale ' ) ;
if ( ! networkSelectedBlock ) {
if ( control ) control . disabled = true ;
return ;
}
networkSelectedBlock = block ;
networkBlocks . forEach ( item = > item . classList . toggle ( ' is-selected ' , item == = block ) ) ;
const value = Math . max ( 50 , Math . min ( 200 , Math . round ( Number ( networkLayout [ block . dataset . section ] ? . scale ) | | 100 ) ) ) ;
const value = Math . max ( 50 , Math . min ( 200 , Math . round ( Number ( networkLayout [ networkSelectedBlock . dataset . section ] ? . scale ) | | 100 ) ) ) ;
networkContentScale = value / 100 ;
setValue ( ' networkContentScale ' , value ) ;
setText ( ' networkContentScaleValue ' , ` $ { value } % ` ) ;
const control = $ ( ' networkContentScale ' ) ;
if ( control ) control . disabled = false ;
updateNetworkBlockScale ( block ) ;
if ( control ) control . disabled = selectedNetworkBlocks . size != = 1 ;
updateNetworkBlockScale ( networkSelectedBlock ) ;
if ( selectedNetworkBlocks . size > 1 ) setText ( ' networkLayoutState ' , ` выбрано $ { selectedNetworkBlocks . size } блока · перетаскивайте группу ` ) ;
}
function toggleNetworkBlockSelection ( block , additive = false ) {
if ( ! block | | networkState ( block ) ) return ;
if ( ! additive ) return selectNetworkBlock ( block ) ;
if ( selectedNetworkBlocks . has ( block ) ) selectedNetworkBlocks . delete ( block ) ;
else selectedNetworkBlocks . add ( block ) ;
updateNetworkSelection ( block ) ;
}
function applyNetworkContentScale ( value ) {
networkContentScale = Math . max ( .5 , Math . min ( 2 , ( Number ( value ) | | 100 ) / 100 ) ) ;
@ -2874,7 +2927,8 @@ HTML = """<!doctype html>
networkBlocks . forEach ( block = > {
const state = networkState ( block ) ;
const item = document . createElement ( ' div ' ) ;
item . className = ` config - restore - item $ { state ? ' is-hidden ' : ' ' } ` ;
item . className = ` config - restore - item $ { state ? ' is-hidden ' : ' ' } $ { selectedNetworkBlocks . has ( block ) ? ' is-selected ' : ' ' } ` ;
item . _networkBlock = block ;
item . title = ' Нажмите, чтобы выбрать блок ' ;
const label = document . createElement ( ' span ' ) ;
label . className = ' config-item-title ' ;
@ -2912,9 +2966,10 @@ HTML = """<!doctype html>
actions . appendChild ( remove ) ;
}
item . append ( label , stateNode , actions ) ;
item . addEventListener ( ' click ' , ( ) = > {
if ( ! state ) selectNetworkBlock ( block ) ;
item . classList . add ( ' is-selected ' ) ;
item . addEventListener ( ' click ' , event = > {
if ( state ) return ;
if ( event . ctrlKey | | event . metaKey ) toggleNetworkBlockSelection ( block , true ) ;
else selectNetworkBlock ( block ) ;
bringNetworkBlockFront ( block ) ;
} ) ;
list . appendChild ( item ) ;
@ -2925,26 +2980,31 @@ HTML = """<!doctype html>
toggle . setAttribute ( ' aria-expanded ' , String ( networkStructureOpen ) ) ;
panel . hidden = ! networkStructureOpen ;
workspace ? . classList . toggle ( ' has-structure ' , networkStructureOpen ) ;
setText ( ' networkLayoutState ' , ` $ { networkBlocks . length - hidden - deleted } блоков · $ { hidden } скрыто · $ { deleted } в корзине ` ) ;
}
function setNetworkBlockState ( block , state ) {
if ( ! block ) return ;
const key = block . dataset . section ;
if ( key & & state ) networkLayout [ key ] = { . . . ( networkLayout [ key ] | | { } ) , state } ;
else if ( key & & networkLayout [ key ] ) {
delete networkLayout [ key ] . state ;
if ( ! Object . keys ( networkLayout [ key ] ) . length ) delete networkLayout [ key ] ;
}
block . dataset . blockState = state | | ' ' ;
block . classList . toggle ( ' is-hidden ' , state == = ' hidden ' ) ;
block . classList . toggle ( ' is-deleted ' , state == = ' deleted ' ) ;
if ( state ) block . setAttribute ( ' aria-hidden ' , ' true ' ) ;
else block . removeAttribute ( ' aria-hidden ' ) ;
const selected = selectedNetworkBlocks . size > 1 ? ` · выбрано $ { selectedNetworkBlocks . size } ` : ' ' ;
setText ( ' networkLayoutState ' , ` $ { networkBlocks . length - hidden - deleted } блоков · $ { hidden } скрыто · $ { deleted } в корзине $ { selected } ` ) ;
}
function setNetworkBlockStates ( blocks , state ) {
const items = [ . . . new Set ( blocks ) ] . filter ( block = > block & & networkBlocks . includes ( block ) ) ;
if ( ! items . length ) return ;
items . forEach ( block = > {
const key = block . dataset . section ;
if ( key & & state ) networkLayout [ key ] = { . . . ( networkLayout [ key ] | | { } ) , state } ;
else if ( key & & networkLayout [ key ] ) {
delete networkLayout [ key ] . state ;
if ( ! Object . keys ( networkLayout [ key ] ) . length ) delete networkLayout [ key ] ;
}
block . dataset . blockState = state | | ' ' ;
block . classList . toggle ( ' is-hidden ' , state == = ' hidden ' ) ;
block . classList . toggle ( ' is-deleted ' , state == = ' deleted ' ) ;
if ( state ) block . setAttribute ( ' aria-hidden ' , ' true ' ) ;
else block . removeAttribute ( ' aria-hidden ' ) ;
} ) ;
saveNetworkLayout ( ) ;
applyNetworkLayout ( ) ;
renderNetworkStructure ( ) ;
selectNetworkBlock ( networkBlocks . find ( candidate = > ! networkState ( candidate ) ) | | null ) ;
}
function setNetworkBlockState ( block , state ) { setNetworkBlockStates ( [ block ] , state ) ; }
function createNetworkBlock ( templateKey , title , existingKey = ' ' ) {
const template = networkBlocks . find ( block = > block . dataset . custom != = ' true ' & & block . dataset . section == = templateKey ) ;
if ( ! template | | ! networkCanvas ) return null ;
@ -3018,33 +3078,43 @@ HTML = """<!doctype html>
const trash = $ ( ' networkTrash ' ) ;
if ( trash ) { trash . hidden = false ; trash . classList . remove ( ' is-over ' ) ; }
}
function finishNetworkDrag ( block , event , dragged ) {
function finishNetworkDrag ( block , event , dragged , group = [ block ] ) {
const trash = $ ( ' networkTrash ' ) ;
const droppedOnTrash = networkTrashDrop ( event ) ;
if ( trash ) { trash . classList . remove ( ' is-over ' ) ; trash . hidden = true ; }
if ( ! dragged ) return ;
captureNetworkLayout( block ) ;
if ( droppedOnTrash ) setNetworkBlockState ( block , ' deleted ' ) ;
group. forEach ( item = > captureNetworkLayout ( item ) ) ;
if ( droppedOnTrash ) setNetworkBlockState s( group , ' deleted ' ) ;
else { saveNetworkLayout ( ) ; renderNetworkStructure ( ) ; }
block . _networkSuppressClick = true ;
window . setTimeout ( ( ) = > { block . _networkSuppressClick = false ; } , 0 ) ;
group . forEach ( item = > {
item . _networkSuppressClick = true ;
window . setTimeout ( ( ) = > { item . _networkSuppressClick = false ; } , 0 ) ;
} ) ;
}
function startNetworkDrag ( block , event ) {
if ( event . button != = 0 | | ! networkCanvas | | networkDragBlocked ( block , event ) ) return ;
const startX = event . clientX ;
const startY = event . clientY ;
const startLeft = parseFloat ( block . style . left ) | | block . offsetLeft | | 0 ;
const startTop = parseFloat ( block . style . top ) | | block . offsetTop | | 0 ;
const group = selectedNetworkBlocks . has ( block )
? [ . . . selectedNetworkBlocks ] . filter ( item = > ! networkState ( item ) )
: [ block ] ;
const origins = group . map ( item = > ( { item , left : parseFloat ( item . style . left ) | | item . offsetLeft | | 0 , top : parseFloat ( item . style . top ) | | item . offsetTop | | 0 } ) ) ;
let dragged = false ;
showNetworkTrash ( ) ;
block . setPointerCapture ? . ( event . pointerId ) ;
const move = moveEvent = > {
if ( ! dragged & & Math . hypot ( moveEvent . clientX - startX , moveEvent . clientY - startY ) < 4 ) return ;
if ( ! dragged ) { dragged = true ; moveEvent . preventDefault ( ) ; block . classList . add ( ' is-dragging ' ) ; }
if ( ! dragged ) { dragged = true ; moveEvent . preventDefault ( ) ; group. forEach ( item = > item . classList . add ( ' is-dragging ' ) ) ; }
moveEvent . preventDefault ( ) ;
const maxLeft = Math . max ( 0 , networkCanvasWidth ( ) - block . offsetWidth ) ;
block . style . left = ` $ { Math . max ( 0 , Math . min ( maxLeft , Math . round ( startLeft + moveEvent . clientX - startX ) ) ) } px ` ;
block . style . top = ` $ { Math . max ( 0 , Math . round ( startTop + moveEvent . clientY - startY ) ) } px ` ;
const rawDx = moveEvent . clientX - startX ;
const minDx = - Math . min ( . . . origins . map ( origin = > origin . left ) ) ;
const maxDx = Math . min ( . . . origins . map ( origin = > networkCanvasWidth ( ) - origin . item . offsetWidth - origin . left ) ) ;
const dx = Math . max ( minDx , Math . min ( maxDx , rawDx ) ) ;
const dy = moveEvent . clientY - startY ;
origins . forEach ( origin = > {
origin . item . style . left = ` $ { Math . max ( 0 , Math . round ( origin . left + dx ) ) } px ` ;
origin . item . style . top = ` $ { Math . max ( 0 , Math . round ( origin . top + dy ) ) } px ` ;
} ) ;
const trash = $ ( ' networkTrash ' ) ;
if ( trash ) trash . classList . toggle ( ' is-over ' , networkTrashDrop ( moveEvent ) ) ;
updateNetworkCanvasHeight ( ) ;
@ -3054,8 +3124,8 @@ HTML = """<!doctype html>
block . removeEventListener ( ' pointermove ' , move ) ;
block . removeEventListener ( ' pointerup ' , end ) ;
block . removeEventListener ( ' pointercancel ' , end ) ;
block . classList . remove ( ' is-dragging ' ) ;
finishNetworkDrag ( block , endEvent , dragged );
group. forEach ( item = > item . classList . remove ( ' is-dragging ' ) ) ;
finishNetworkDrag ( block , endEvent , dragged , group );
} ;
block . addEventListener ( ' pointermove ' , move ) ;
block . addEventListener ( ' pointerup ' , end , { once : true } ) ;
@ -3065,9 +3135,16 @@ HTML = """<!doctype html>
if ( ! block | | block . _networkInteractive ) return ;
block . _networkInteractive = true ;
block . addEventListener ( ' pointerdown ' , event = > {
selectNetworkBlock ( block ) ;
bringNetworkBlockFront ( block ) ;
const additive = event . ctrlKey | | event . metaKey ;
const edge = networkResizeEdge ( event , block ) ;
if ( additive & & ! edge & & ! networkDragBlocked ( block , event ) ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
toggleNetworkBlockSelection ( block , true ) ;
return ;
}
if ( edge | | ! selectedNetworkBlocks . has ( block ) ) selectNetworkBlock ( block ) ;
bringNetworkBlockFront ( block ) ;
if ( ! edge | | networkDragBlocked ( block , event ) ) return ;
event . preventDefault ( ) ;
event . stopPropagation ( ) ;