import { Chord } from "../music-theory/Chord" ; import { Transport } from "tone" ; import $ from "jquery" ; /**
 * Holds information for sequence of chords in the progression
 */ export class ChordProgression { chordsList : Array < Chord > ; curIndex : number ; curChord : null | Chord ; curChordNameBtn : null | HTMLElement ; chordNameBtns : Array < HTMLElement > ; onActivate : Function ; onPlay : Function ; _playButton : HTMLElement ; onSwitch : Function ; onStop : Function ; constructor ( ) { this . chordsList = [ ] ; this . curIndex = - 1 ; this . curChord = null ; this . curChordNameBtn = null ; this . chordNameBtns = [ ] ; this . _playButton = document . getElementById ( "playBtn" ) ! ; this . onActivate = function ( ) { } ; this . onPlay = function ( ) { } ; this . onSwitch = function ( ) { } ; this . onStop = function ( ) { } ; this . _appendChord ( new Chord ( 48 ) , null ) ; this . _appendAddBtn ( ) ; this . _addEventListeners ( ) ; $ ( '#resetBtn' ) . tooltip ( ) ; $ ( '#playBtn' ) . tooltip ( ) ; } private private _addEventListeners ( ) { const resetBtn = document . getElementById ( "resetBtn" ) ! ; resetBtn . addEventListener ( "pointerup" , ( ) => { if ( ! resetBtn . classList . contains ( "disabled" ) ) { this . _reset ( ) ; } } ) ; this . _playButton . addEventListener ( "pointerup" , ( ) => { this . _toggleProgression ( ) ; } ) ; } private private static _removeAddBtn ( ) { const addButton = document . getElementById ( "addBtn" ) ; if ( addButton == null ) { throw new Error ( "addBtn does not exist. Cannot delete" ) ; } addButton . remove ( ) ; } private private _appendAddBtn ( ) { const btnContainer = document . createElement ( "div" ) ; btnContainer . classList . add ( "btn-chord" ) ; btnContainer . id = "addBtn" ; const btn = document . createElement ( "div" ) ; btn . classList . add ( "button-chord-name" , "add" , "shadow" ) ; const icon = document . createElement ( "i" ) ; icon . classList . add ( "material-icons" ) ; icon . innerText = "add" ; btn . appendChild ( icon ) ; btn . addEventListener ( "pointerup" , ( ) => { const chord = new Chord ( 48 ) ; this . _appendChord ( chord , btnContainer ) ; } ) ; btn . addEventListener ( "pointerover" , ( ) => { $ ( btn ) . tooltip ( 'hide' ) . attr ( "data-placement" , "bottom" ) . attr ( 'data-original-title' , "Add a Chord" ) . tooltip ( 'show' ) ; } ) ; btnContainer . appendChild ( btn ) ; const progressionContainer = document . getElementById ( "progressionChordsContainer" ) ! ; progressionContainer . appendChild ( btnContainer ) ; } param chord param addBtn private private _appendChord ( chord , addBtn ) { if ( this . curChordNameBtn !== null ) { this . curChordNameBtn . parentElement ! . classList . remove ( "active" ) ; } this . chordsList . push ( chord ) ; this . curChord = chord ; this . curIndex = this . chordsList . length - 1 ; const btnContainer = document . createElement ( "div" ) ; btnContainer . classList . add ( "btn-chord" , "active" ) ; this . _appendChordNameBtn ( btnContainer ) ; this . _appendDeleteBtn ( btnContainer ) ; this . _appendCopyBtn ( btnContainer ) ; const progressionContainer = document . getElementById ( "progressionChordsContainer" ) ! ; progressionContainer . insertBefore ( btnContainer , addBtn ) ; console . log ( `Append a new chord ${ this . curChord } at ${ this . curIndex }` ) ; this . _toggleFirstChordDelete ( ) ; } param chord param index private _insertChord ( chord , index ) { if ( this . curChordNameBtn !== null ) { this . curChordNameBtn . parentElement ! . classList . remove ( "active" ) ; } this . chordsList . splice ( index , 0 , chord ) ; this . curChord = chord ; this . curIndex = index ; const btnContainer = document . createElement ( "div" ) ; btnContainer . classList . add ( "btn-chord" , "active" ) ; this . _insertChordNameBtn ( btnContainer , index , chord ) ; this . _appendDeleteBtn ( btnContainer ) ; this . _appendCopyBtn ( btnContainer ) ; const prevButton = this . chordNameBtns [ index - 1 ] ; const progressionContainer = document . getElementById ( "progressionChordsContainer" ) ! ; progressionContainer . insertBefore ( btnContainer , prevButton . parentElement ! . nextSibling ) ; console . log ( `Insert a new chord ${ this . curChord } at ${ this . curIndex }` ) ; this . _toggleFirstChordDelete ( ) ; } param container private private _appendDeleteBtn ( container ) { const deleteBtn = document . createElement ( "div" ) ; deleteBtn . classList . add ( "btn-chord-delete" , "shadow-sm" ) ; const icon = document . createElement ( "i" ) ; icon . classList . add ( "material-icons-outlined" ) ; icon . innerText = "clear" ; deleteBtn . appendChild ( icon ) ; deleteBtn . addEventListener ( "pointerup" , ( ) => { this . _delete ( deleteBtn ) ; } ) ; container . appendChild ( deleteBtn ) ; } param container private private _appendCopyBtn ( container ) { const copyBtn = document . createElement ( "div" ) ; $ ( copyBtn ) . tooltip ( 'hide' ) . attr ( "data-placement" , "bottom" ) . attr ( 'data-original-title' , "Copy at End" ) . tooltip ( 'show' ) ; copyBtn . classList . add ( "btn-chord-copy" , "shadow-sm" ) ; const icon = document . createElement ( "i" ) ; icon . classList . add ( "material-icons-outlined" ) ; icon . innerText = "file_copy" ; copyBtn . appendChild ( icon ) ; copyBtn . addEventListener ( "pointerup" , ( ) => { if ( ! copyBtn . classList . contains ( "disabled" ) ) { const chordNameBtn = copyBtn . parentElement ?. querySelector ( ".button-chord-name" ) ; if ( chordNameBtn == null ) { throw new Error ( "Cannot find the chord name button" ) ; } const index = this . chordNameBtns . indexOf ( chordNameBtn as HTMLElement ) ; const chord = $ . extend ( true , { } , this . chordsList [ index ] ) ; this . _insertChord ( chord , this . chordsList . length ) ; } } ) ; container . appendChild ( copyBtn ) ; } param container private private _appendChordNameBtn ( container ) { const btn = document . createElement ( "div" ) ; btn . classList . add ( "button-chord-name" , "shadow" ) ; const text = document . createElement ( "div" ) ; this . curChord = new Chord ( ) ; text . innerHTML = this . curChord . getChordName ( ) ; btn . appendChild ( text ) ; btn . addEventListener ( "pointerup" , ( ) => { this . activate ( btn ) ; } ) ; container . appendChild ( btn ) ; this . chordNameBtns . push ( btn ) ; this . curChordNameBtn = btn ; this . activate ( btn ) ; } param container param index param chord private private _insertChordNameBtn ( container , index , chord = new Chord ( 48 ) ) { const btn = document . createElement ( "div" ) ; btn . classList . add ( "button-chord-name" , "shadow" ) ; const text = document . createElement ( "div" ) ; this . curChord = chord ; text . innerHTML = this . curChord . getChordName ( ) ; btn . appendChild ( text ) ; btn . addEventListener ( "pointerup" , ( ) => { this . activate ( btn ) ; } ) ; container . appendChild ( btn ) ; this . chordNameBtns . splice ( index , 0 , btn ) ; this . curChordNameBtn = btn ; this . activate ( btn ) ; } /**
     * Switch to and activate the next chord after the current one
     */ public switch ( ) { let btnIndex = 0 ; if ( this . curIndex >= this . chordsList . length - 1 ) { btnIndex = 0 ; } else { btnIndex = this . curIndex + 1 ; } const newBtn = this . chordNameBtns [ btnIndex ] ; this . activate ( newBtn ) ; this . onSwitch ( ) ; } param chordNameBtn public public activate ( chordNameBtn ) { for ( let chordButton of this . chordNameBtns ) { chordButton . classList . remove ( 'animated' ) ; } if ( this . curChordNameBtn !== null ) { this . curChordNameBtn . parentElement ! . classList . remove ( "active" ) ; } this . curIndex = this . chordNameBtns . indexOf ( chordNameBtn ) ; this . curChord = this . chordsList [ this . curIndex ] ; chordNameBtn . parentElement ! . classList . add ( "active" ) ; this . curChordNameBtn = chordNameBtn ; console . log ( `Activate chord ${ this . curChord } at ${ this . curIndex }` ) ; chordNameBtn . classList . add ( 'animated' ) ; this . onActivate ( this . curChord ) ; } param index private return private _disableEdit ( index ) { const button = this . chordNameBtns [ index ] ; if ( button == null ) { throw new Error ( "Button does not exist. Cannot disable deleting" ) ; } const container = button . parentElement ! ; const deleteBtns = container . querySelectorAll ( ".btn-chord-delete" ) ; deleteBtns . forEach ( e => { e . remove ( ) ; } ) ; const copyButtons = container . querySelectorAll ( ".btn-chord-copy" ) ; copyButtons . forEach ( e => { e . remove ( ) ; } ) ; return true ; } private private _disableEditAll ( ) { for ( let i = 0 ; i < this . chordNameBtns . length ; i ++ ) { this . _disableEdit ( i ) ; } } private private _enableEditAll ( ) { for ( let chordNameBtn of this . chordNameBtns ) { const container = chordNameBtn . parentElement ; if ( container == null ) { throw new Error ( "The chord name button does not have a container. Cannot enable deleting" ) ; } this . _appendDeleteBtn ( container ) ; this . _appendCopyBtn ( container ) ; this . _toggleFirstChordDelete ( ) ; } } private private _toggleFirstChordDelete ( ) { if ( this . chordsList . length <= 1 ) { const button = this . chordNameBtns [ 0 ] ; const container = button . parentElement ! ; const deleteBtn = container . querySelector ( ".btn-chord-delete" ) ; if ( deleteBtn == null ) { throw new Error ( "Delete button does not exist" ) ; } deleteBtn . remove ( ) ; console . log ( "The first delete button is deleted" ) ; } else { const button = this . chordNameBtns [ 0 ] ; if ( button . parentElement ?. querySelector ( ".btn-chord-delete" ) ) { return ; } const container = button . parentElement ! ; this . _appendDeleteBtn ( container ) ; console . log ( "The first delete button is added" ) ; } } ; param deleteBtn private private _delete ( deleteBtn ) { const chordNameBtn = deleteBtn . parentElement ?. querySelector ( ".button-chord-name" ) as HTMLElement ; const index = this . chordNameBtns . indexOf ( chordNameBtn ) ; this . chordsList . splice ( index , 1 ) ; this . chordNameBtns . splice ( index , 1 ) ; if ( this . curIndex > index ) { this . curIndex -= 1 ; } else if ( this . curIndex == 0 && this . chordsList . length >= 1 ) { this . curIndex = 0 ; this . curChord = this . chordsList [ 0 ] ; const newChordNameBtn = this . chordNameBtns [ 0 ] ; this . activate ( newChordNameBtn ) ; } else if ( this . curIndex == index ) { this . curIndex -= 1 ; this . curChord = this . chordsList [ this . curIndex ] ; if ( this . curIndex >= 0 ) { const newChordNameBtn = this . chordNameBtns [ this . curIndex ] ; this . activate ( newChordNameBtn ) ; } } console . log ( `Delete chord at index ${ index }. Change the chord to ${ this . curChord } at ${ this . curIndex }` ) ; deleteBtn . parentElement ! . remove ( ) ; this . _toggleFirstChordDelete ( ) ; } param chord public setChord ( chord ) { this . curChord = chord ; this . chordsList [ this . curIndex ] = chord ; this . _setChordName ( ) ; } private private _setChordName ( ) { this . curChordNameBtn ! . innerHTML = "" ; const text = document . createElement ( "div" ) ; if ( this . curChord == null ) { throw new Error ( "curChord is null" ) ; } text . innerHTML = this . curChord . getChordName ( ) ; this . curChordNameBtn ! . appendChild ( text ) ; } param family param type public setChordType ( family , type ) { if ( this . curChord == null ) { throw new Error ( "curChord is null" ) ; } this . curChord . type = type ; this . curChord . family = family ; this . curChord . inversionNum = 0 ; this . _setChordName ( ) ; } param inversion public setInversion ( inversion ) { if ( inversion < 0 ) { throw new Error ( "Invalid inversion, inversion < 0" ) ; } if ( this . curChord == null ) { throw new Error ( "curChord is null" ) ; } this . curChord . inversionNum = inversion ; this . chordsList [ this . curIndex ] = this . curChord ; this . _setChordName ( ) ; } private private _reset ( ) { const ans = confirm ( "Are you sure to reset the chord progression?\nAfter reset, all progress will be lost." ) ; if ( ans == false ) { return false ; } this . chordsList = [ ] ; this . curChord = null ; this . curIndex = - 1 ; this . curChordNameBtn = null ; this . chordNameBtns = [ ] ; const container = document . getElementById ( "progressionChordsContainer" ) ! ; container . innerHTML = "" ; this . _appendChord ( new Chord ( 48 ) , null ) ; this . _appendAddBtn ( ) ; return true ; } private private _getNotesList ( ) { const notesList = [ ] ; for ( let chord of this . chordsList ) { const notes = chord . getNotes ( ) ; notesList . push ( notes ) ; } return notesList ; } private private _toggleProgression ( ) { if ( Transport . state === 'started' ) { this . _playButton . innerHTML = "<i class=\"fas fa-play\"></i>" ; this . _playButton . classList . remove ( "active" ) ; this . _enableBtnsAnimation ( ) ; $ ( "#playBtn" ) . tooltip ( 'hide' ) . attr ( 'data-original-title' , "Play" ) . tooltip ( 'show' ) ; Transport . stop ( ) ; this . _appendAddBtn ( ) ; this . _enableEditAll ( ) ; ChordProgression . _enableReset ( ) ; this . onStop ( ) ; } else { this . _playButton . innerHTML = "<i class=\"fas fa-pause\"></i>" ; this . _playButton . classList . add ( "active" ) ; this . _disableBtnsAnimation ( ) ; $ ( "#playBtn" ) . tooltip ( 'hide' ) . attr ( 'data-original-title' , "Stop" ) . tooltip ( 'show' ) ; const notesList = this . _getNotesList ( ) ; ChordProgression . _removeAddBtn ( ) ; this . _disableEditAll ( ) ; ChordProgression . _disableReset ( ) ; this . onPlay ( notesList ) ; } } /**
     * Disable button's jumping animation
     */ private _disableBtnsAnimation ( ) { for ( let btn of this . chordNameBtns ) { btn . classList . add ( "disable" ) ; } } /**
     * Enable button's jumping animation
     */ private _enableBtnsAnimation ( ) { for ( let btn of this . chordNameBtns ) { btn . classList . remove ( "disable" ) ; } } private private static _disableReset ( ) { const resetBtn = document . getElementById ( "resetBtn" ) ! ; resetBtn . classList . add ( "disabled" ) ; } private private static _enableReset ( ) { const resetBtn = document . getElementById ( "resetBtn" ) ! ; resetBtn . classList . remove ( "disabled" ) ; } }