2017-11-13 01:23:39 -05:00
'use strict' ;
// ============================================================================
// Menu Module
// ============================================================================
import Module from 'utilities/module' ;
2018-04-01 18:24:08 -04:00
import { createElement } from 'utilities/dom' ;
2017-11-13 01:23:39 -05:00
import { has , deep _copy } from 'utilities/object' ;
2018-03-24 04:19:41 -04:00
import { parse _path } from 'src/settings' ;
const EXCLUSIVE _CONTAINER = '.twilight-main,.twilight-minimal-root>div' ;
2017-11-13 01:23:39 -05:00
function format _term ( term ) {
return term . replace ( /<[^>]*>/g , '' ) . toLocaleLowerCase ( ) ;
}
// TODO: Rewrite literally everything about the menu to use vue-router and further
// separate the concept of navigation from visible pages.
export default class MainMenu extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( 'settings' ) ;
this . inject ( 'i18n' ) ;
this . inject ( 'site' ) ;
this . inject ( 'vue' ) ;
//this.should_enable = true;
this . _settings _tree = null ;
this . _settings _count = 0 ;
this . _menu = null ;
this . _visible = true ;
this . _maximized = false ;
2018-03-22 19:09:28 +01:00
this . exclusive = false ;
2017-11-13 01:23:39 -05:00
this . settings . addUI ( 'profiles' , {
path : 'Data Management @{"sort": 1000, "profile_warning": false} > Profiles @{"profile_warning": false}' ,
component : 'profile-manager'
} ) ;
this . settings . addUI ( 'home' , {
path : 'Home @{"sort": -1000, "profile_warning": false}' ,
component : 'home-page'
} ) ;
this . settings . addUI ( 'feedback' , {
path : 'Home > Feedback' ,
component : 'feedback-page'
} ) ;
this . settings . addUI ( 'changelog' , {
path : 'Home > Changelog' ,
component : 'changelog'
} ) ;
}
async onLoad ( ) {
this . vue . component (
( await import ( /* webpackChunkName: "main-menu" */ './components.js' ) ) . default
) ;
}
get maximized ( ) {
return this . _maximized ;
}
set maximized ( val ) {
val = Boolean ( val ) ;
if ( val === this . _maximized )
return ;
if ( this . enabled )
this . toggleSize ( ) ;
}
get visible ( ) {
return this . _visible ;
}
set visible ( val ) {
val = Boolean ( val ) ;
if ( val === this . _visible )
return ;
if ( this . enabled )
this . toggleVisible ( ) ;
}
async onEnable ( event ) {
2018-03-24 04:19:41 -04:00
await this . site . awaitElement ( EXCLUSIVE _CONTAINER ) ;
2017-11-13 01:23:39 -05:00
this . on ( 'site.menu_button:clicked' , this . toggleVisible ) ;
if ( this . _visible ) {
this . _visible = false ;
this . toggleVisible ( event ) ;
}
}
onDisable ( ) {
if ( this . _visible ) {
this . toggleVisible ( ) ;
this . _visible = true ;
}
this . off ( 'site.menu_button:clicked' , this . toggleVisible ) ;
}
toggleVisible ( event ) {
if ( event && event . button !== 0 )
return ;
const maximized = this . _maximized ,
visible = this . _visible = ! this . _visible ,
2018-03-24 04:19:41 -04:00
main = this . exclusive ? document . querySelector ( EXCLUSIVE _CONTAINER ) : document . querySelector ( maximized ? '.twilight-main' : '.twilight-root>.tw-full-height' ) ;
2017-11-13 01:23:39 -05:00
if ( ! visible ) {
if ( maximized )
main . classList . remove ( 'ffz-has-menu' ) ;
if ( this . _menu ) {
2017-12-01 15:36:18 -05:00
this . _menu . remove ( ) ;
2017-11-13 01:23:39 -05:00
this . _vue . $destroy ( ) ;
this . _menu = this . _vue = null ;
}
return ;
}
if ( ! this . _menu )
this . createMenu ( ) ;
if ( maximized )
main . classList . add ( 'ffz-has-menu' ) ;
main . appendChild ( this . _menu ) ;
}
toggleSize ( event ) {
if ( ! this . _visible || event && event . button !== 0 )
return ;
const maximized = this . _maximized = ! this . _maximized ,
2018-03-24 04:19:41 -04:00
main = this . exclusive ? document . querySelector ( EXCLUSIVE _CONTAINER ) : document . querySelector ( maximized ? '.twilight-main' : '.twilight-root>.tw-full-height' ) ,
2017-11-13 01:23:39 -05:00
old _main = this . _menu . parentElement ;
if ( maximized )
main . classList . add ( 'ffz-has-menu' ) ;
else
old _main . classList . remove ( 'ffz-has-menu' ) ;
2017-12-01 15:36:18 -05:00
this . _menu . remove ( ) ;
2017-11-13 01:23:39 -05:00
main . appendChild ( this . _menu ) ;
this . _vue . $children [ 0 ] . maximized = maximized ;
}
rebuildSettingsTree ( ) {
this . _settings _tree = { } ;
this . _settings _count = 0 ;
for ( const [ key , def ] of this . settings . definitions )
this . _addDefinitionToTree ( key , def ) ;
for ( const [ key , def ] of this . settings . ui _structures )
this . _addDefinitionToTree ( key , def ) ;
}
_addDefinitionToTree ( key , def ) {
if ( ! def . ui || ! this . _settings _tree )
return ;
if ( ! def . ui . path _tokens ) {
if ( def . ui . path )
def . ui . path _tokens = parse _path ( def . ui . path ) ;
else
return ;
}
if ( ! def . ui || ! def . ui . path _tokens || ! this . _settings _tree )
return ;
const tree = this . _settings _tree ,
tokens = def . ui . path _tokens ,
len = tokens . length ;
let prefix = null ,
token ;
// Create and/or update all the necessary structure elements for
// this node in the settings tree.
for ( let i = 0 ; i < len ; i ++ ) {
const raw _token = tokens [ i ] ,
key = prefix ? ` ${ prefix } . ${ raw _token . key } ` : raw _token . key ;
token = tree [ key ] ;
if ( ! token )
token = tree [ key ] = {
full _key : key ,
sort : 0 ,
parent : prefix ,
expanded : prefix === null ,
i18n _key : ` setting. ${ key } ` ,
desc _i18n _key : ` setting. ${ key } .description `
} ;
Object . assign ( token , raw _token ) ;
prefix = key ;
}
// Add this setting to the tree.
token . settings = token . settings || [ ] ;
token . settings . push ( [ key , def ] ) ;
this . _settings _count ++ ;
}
getSettingsTree ( ) {
const started = performance . now ( ) ;
if ( ! this . _settings _tree )
this . rebuildSettingsTree ( ) ;
const tree = this . _settings _tree ,
root = { } ,
copies = { } ,
needs _sort = new Set ,
needs _component = new Set ,
have _locale = this . i18n . locale !== 'en' ;
for ( const key in tree ) {
if ( ! has ( tree , key ) )
continue ;
const token = copies [ key ] = copies [ key ] || Object . assign ( { } , tree [ key ] ) ,
p _key = token . parent ,
parent = p _key ?
( copies [ p _key ] = copies [ p _key ] || Object . assign ( { } , tree [ p _key ] ) ) :
root ;
token . parent = p _key ? parent : null ;
token . page = token . page || parent . page ;
if ( token . page && ! token . component )
needs _component . add ( token ) ;
if ( token . settings ) {
const list = token . contents = token . contents || [ ] ;
for ( const [ setting _key , def ] of token . settings )
if ( def . ui ) { //} && def.ui.title ) {
const i18n _key = ` ${ token . i18n _key } . ${ def . ui . key } `
const tok = Object . assign ( {
i18n _key ,
desc _i18n _key : ` ${ i18n _key } .description ` ,
sort : 0 ,
title : setting _key
} , def . ui , {
full _key : ` setting: ${ setting _key } ` ,
setting : setting _key ,
path _tokens : undefined ,
parent : token
} ) ;
2017-11-15 21:59:47 -05:00
if ( has ( def , 'default' ) && ! has ( tok , 'default' ) ) {
2017-11-13 01:23:39 -05:00
const def _type = typeof def . default ;
if ( def _type === 'object' ) {
// TODO: Better way to deep copy this object.
tok . default = JSON . parse ( JSON . stringify ( def . default ) ) ;
} else
tok . default = def . default ;
}
const terms = [
setting _key ,
this . i18n . t ( tok . i18n _key , tok . title , tok , true )
] ;
if ( have _locale && this . i18n . has ( tok . i18n _key ) )
terms . push ( this . i18n . t ( tok . i18n _key , tok . title , tok ) ) ;
if ( tok . description ) {
terms . push ( this . i18n . t ( tok . desc _i18n _key , tok . description , tok , true ) ) ;
if ( have _locale && this . i18n . has ( tok . desc _i18n _key ) )
terms . push ( this . i18n . t ( tok . desc _i18n _key , tok . description , tok ) ) ;
}
tok . search _terms = terms . map ( format _term ) . join ( '\n' ) ;
list . push ( tok ) ;
}
token . settings = undefined ;
if ( list . length > 1 )
needs _sort . add ( list ) ;
}
if ( ! token . search _terms ) {
const formatted = this . i18n . t ( token . i18n _key , token . title , token , true ) ;
let terms = [ token . key ] ;
if ( formatted && formatted . localeCompare ( token . key , undefined , { sensitivity : 'base' } ) )
terms . push ( formatted ) ;
if ( have _locale && this . i18n . has ( token . i18n _key ) )
terms . push ( this . i18n . t ( token . i18n _key , token . title , token ) ) ;
if ( token . description ) {
terms . push ( this . i18n . t ( token . desc _i18n _key , token . description , token , true ) ) ;
if ( have _locale && this . i18n . has ( token . desc _i18n _key ) )
terms . push ( this . i18n . t ( token . desc _i18n _key , token . description , token ) ) ;
}
terms = terms . map ( format _term ) ;
for ( const lk of [ 'tabs' , 'contents' , 'items' ] )
if ( token [ lk ] )
for ( const tok of token [ lk ] )
if ( tok . search _terms )
terms . push ( tok . search _terms ) ;
terms = token . search _terms = terms . join ( '\n' ) ;
let p = parent ;
while ( p && p . search _terms ) {
p . search _terms += '\n' + terms ;
p = p . parent ;
}
}
const lk = token . tab ? 'tabs' : token . page ? 'contents' : 'items' ,
list = parent [ lk ] = parent [ lk ] || [ ] ;
list . push ( token ) ;
if ( list . length > 1 )
needs _sort . add ( list ) ;
}
for ( const token of needs _component ) {
token . component = token . tabs ? 'tab-container' :
token . contents ? 'menu-container' :
2018-03-24 04:19:41 -04:00
'setting-check-box' ;
2017-11-13 01:23:39 -05:00
}
for ( const list of needs _sort )
list . sort ( ( a , b ) => {
if ( a . sort < b . sort ) return - 1 ;
if ( a . sort > b . sort ) return 1 ;
return a . key . localeCompare ( b . key ) ;
} ) ;
this . log . info ( ` Built Tree in ${ ( performance . now ( ) - started ) . toFixed ( 5 ) } ms with ${ Object . keys ( tree ) . length } structure nodes and ${ this . _settings _count } settings nodes. ` ) ;
const items = root . items || [ ] ;
items . keys = copies ;
return items ;
}
getProfiles ( context ) {
const profiles = [ ] ,
keys = { } ;
context = context || this . settings . main _context ;
for ( const profile of this . settings . _ _profiles )
profiles . push ( keys [ profile . id ] = this . getProfileProxy ( profile , context ) ) ;
return [ profiles , keys ] ;
}
2018-03-24 04:19:41 -04:00
getProfileProxy ( profile , context ) { // eslint-disable-line class-methods-use-this
2017-11-13 01:23:39 -05:00
return {
id : profile . id ,
order : context . manager . _ _profiles . indexOf ( profile ) ,
live : context . _ _profiles . includes ( profile ) ,
title : profile . name ,
i18n _key : profile . i18n _key ,
description : profile . description ,
desc _i18n _key : profile . desc _i18n _key || profile . i18n _key && ` ${ profile . i18n _key } .description ` ,
move : idx => context . manager . moveProfile ( profile . id , idx ) ,
save : ( ) => profile . save ( ) ,
update : data => {
profile . data = data
profile . save ( )
} ,
context : deep _copy ( profile . context ) ,
get : key => profile . get ( key ) ,
set : ( key , val ) => profile . set ( key , val ) ,
delete : key => profile . delete ( key ) ,
has : key => profile . has ( key ) ,
on : ( ... args ) => profile . on ( ... args ) ,
off : ( ... args ) => profile . off ( ... args )
}
}
getContext ( ) {
const t = this ,
Vue = this . vue . Vue ,
settings = this . settings ,
context = settings . main _context ,
[ profiles , profile _keys ] = this . getProfiles ( ) ,
2018-03-24 04:19:41 -04:00
_c = {
profiles ,
profile _keys ,
currentProfile : profile _keys [ 0 ] ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
createProfile : data => {
const profile = settings . createProfile ( data ) ;
return t . getProfileProxy ( profile , context ) ;
} ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
deleteProfile : profile => settings . deleteProfile ( profile ) ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
context : {
_users : 0 ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
profiles : context . _ _profiles . map ( profile => profile . id ) ,
get : key => context . get ( key ) ,
uses : key => context . uses ( key ) ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
on : ( ... args ) => context . on ( ... args ) ,
off : ( ... args ) => context . off ( ... args ) ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
order : id => context . order . indexOf ( id ) ,
context : deep _copy ( context . context ) ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
_update _profiles ( changed ) {
const new _list = [ ] ,
profiles = context . manager . _ _profiles ;
for ( let i = 0 ; i < profiles . length ; i ++ ) {
const profile = profile _keys [ profiles [ i ] . id ] ;
profile . order = i ;
new _list . push ( profile ) ;
}
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
Vue . set ( _c , 'profiles' , new _list ) ;
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
if ( changed && changed . id === _c . currentProfile . id )
_c . currentProfile = profile _keys [ changed . id ] ;
} ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
_profile _created ( profile ) {
Vue . set ( profile _keys , profile . id , t . getProfileProxy ( profile , context ) ) ;
this . _update _profiles ( )
} ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
_profile _changed ( profile ) {
Vue . set ( profile _keys , profile . id , t . getProfileProxy ( profile , context ) ) ;
this . _update _profiles ( profile ) ;
} ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
_profile _deleted ( profile ) {
Vue . delete ( profile _keys , profile . id ) ;
this . _update _profiles ( ) ;
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
if ( _c . currentProfile . id === profile . id )
_c . currentProfile = profile _keys [ 0 ]
} ,
2017-11-13 01:23:39 -05:00
2018-03-24 04:19:41 -04:00
_context _changed ( ) {
this . context = deep _copy ( context . context ) ;
const ids = this . profiles = context . _ _profiles . map ( profile => profile . id ) ;
for ( const id of ids ) {
const profile = profiles [ id ] ;
profile . live = this . profiles . includes ( profile . id ) ;
}
} ,
_add _user ( ) {
this . _users ++ ;
if ( this . _users === 1 ) {
settings . on ( ':profile-created' , this . _profile _created , this ) ;
settings . on ( ':profile-changed' , this . _profile _changed , this ) ;
settings . on ( ':profile-deleted' , this . _profile _deleted , this ) ;
settings . on ( ':profiles-reordered' , this . _update _profiles , this ) ;
context . on ( 'context_changed' , this . _context _changed , this ) ;
context . on ( 'profiles_changed' , this . _context _changed , this ) ;
this . profiles = context . _ _profiles . map ( profile => profile . id ) ;
}
} ,
_remove _user ( ) {
this . _users -- ;
if ( this . _users === 0 ) {
settings . off ( ':profile-created' , this . _profile _created , this ) ;
settings . off ( ':profile-changed' , this . _profile _changed , this ) ;
settings . off ( ':profile-deleted' , this . _profile _deleted , this ) ;
settings . off ( ':profiles-reordered' , this . _update _profiles , this ) ;
context . off ( 'context_changed' , this . _context _changed , this ) ;
context . off ( 'profiles_changed' , this . _context _changed , this ) ;
}
2017-11-13 01:23:39 -05:00
}
}
2018-03-24 04:19:41 -04:00
} ;
2017-11-13 01:23:39 -05:00
return _c ;
}
getData ( ) {
const settings = this . getSettingsTree ( ) ,
context = this . getContext ( ) ;
return {
context ,
nav : settings ,
currentItem : settings . keys [ 'home' ] , // settings[0],
nav _keys : settings . keys ,
maximized : this . _maximized ,
2018-03-22 19:09:28 +01:00
resize : e => ! this . exclusive && this . toggleSize ( e ) ,
close : e => ! this . exclusive && this . toggleVisible ( e ) ,
version : window . FrankerFaceZ . version _info ,
exclusive : this . exclusive
2017-11-13 01:23:39 -05:00
}
}
createMenu ( ) {
if ( this . _menu )
return ;
this . _vue = new this . vue . Vue ( {
2018-04-01 18:24:08 -04:00
el : createElement ( 'div' ) ,
2017-11-13 01:23:39 -05:00
render : h => h ( 'main-menu' , this . getData ( ) )
} ) ;
this . _menu = this . _vue . $el ;
}
2018-03-24 04:19:41 -04:00
}