forked from Botanical/BotanJS
523 lines
13 KiB
JavaScript
523 lines
13 KiB
JavaScript
(function(){
|
|
var ns = __namespace( "Astro.Blog.AstroEdit" );
|
|
|
|
/** @type {typeof Dandelion} */
|
|
var Dand = __import( "Dandelion" );
|
|
/** @type {function(...?): Dandelion.IDOMElement} */
|
|
var IDOMElement = __import( "Dandelion.IDOMElement" );
|
|
/** @type {Dandelion.IDOMObject} */
|
|
var IDOMObject = __import( "Dandelion.IDOMObject" );
|
|
/** @type {System.Cycle} */
|
|
var Cycle = __import( "System.Cycle" );
|
|
/** @type {System.Debug} */
|
|
var debug = __import( "System.Debug" );
|
|
/** @type {System.Net.ClassLoader} */
|
|
var Loader = __import( "System.Net.ClassLoader" );
|
|
/** @type {System.utils} */
|
|
var utils = __import( "System.utils" );
|
|
/** @type {typeof System.utils.DataKey} */
|
|
var DataKey = __import( "System.utils.DataKey" );
|
|
/** @type {typeof System.utils.IKey} */
|
|
var IKey = __import( "System.utils.IKey" );
|
|
/** @type {typeof Components.MessageBox} */
|
|
var MessageBox = __import( "Components.MessageBox" );
|
|
/** @type {Astro.Blog.Config} */
|
|
var Config = __import( "Astro.Blog.Config" );
|
|
|
|
var moduleNs = "Astro.Blog.AstroEdit.SmartInput.CandidateAction.";
|
|
var service_uri = Config.get( "ServiceUri" );
|
|
var code;
|
|
|
|
var Destructor = function( target )
|
|
{
|
|
this.target = target;
|
|
this.target.Disposed = false;
|
|
this.destructSequence = [];
|
|
};
|
|
|
|
Destructor.prototype.Register = function( destruct )
|
|
{
|
|
this.destructSequence.push( destruct );
|
|
};
|
|
|
|
Destructor.prototype.Destruct = function()
|
|
{
|
|
for( var i in this.destructSequence )
|
|
{
|
|
this.destructSequence[i]();
|
|
}
|
|
|
|
this.target.Disposed = true;
|
|
this.destructSequence = null;
|
|
};
|
|
|
|
var KeyHandler = function( sender, handler )
|
|
{
|
|
return function( e )
|
|
{
|
|
e = e || window.event;
|
|
if ( e.keyCode ) code = e.keyCode;
|
|
else if ( e.which ) code = e.which;
|
|
|
|
handler( sender, e );
|
|
};
|
|
};
|
|
|
|
// {{{ Candidates Class
|
|
var Candidates = function( title, desc, defs )
|
|
{
|
|
this.title = title;
|
|
this.desc = desc;
|
|
this.__cands = [];
|
|
/** @type Array<Astro.Blog.AstroEdit.SmartInput.Definition> */
|
|
this.__defs = defs || {};
|
|
this.Empty = !defs;
|
|
};
|
|
|
|
Candidates.prototype.Get = function()
|
|
{
|
|
if( this.__cands.length ) return this.__cands;
|
|
|
|
for( var i in this.__defs )
|
|
{
|
|
var c = this.__defs[i];
|
|
this.__cands.push( Dand.wrapc( "cn", [ i, Dand.wrapc( "desc", c.desc ) ], new DataKey( "key", i ) ) );
|
|
}
|
|
|
|
return this.__cands;
|
|
};
|
|
|
|
Candidates.prototype.Reset = function()
|
|
{
|
|
var Cands = this.Get();
|
|
for( var i in Cands )
|
|
{
|
|
var c = IDOMElement( Cands[i] );
|
|
c.setAttribute( new DataKey( "selected", 0 ) );
|
|
}
|
|
};
|
|
|
|
Candidates.prototype.Filtered = function()
|
|
{
|
|
var Cands = this.Get();
|
|
var selected = [];
|
|
for( var i in Cands )
|
|
{
|
|
var c = IDOMElement( Cands[i] );
|
|
c.setAttribute( new DataKey( "selected", 0 ) );
|
|
|
|
if( c.style.display != "none" )
|
|
{
|
|
selected.push( c );
|
|
}
|
|
}
|
|
|
|
return selected;
|
|
};
|
|
|
|
Candidates.prototype.Selected = function()
|
|
{
|
|
var Cands = this.Get();
|
|
for( var i in Cands )
|
|
{
|
|
var c = IDOMElement( Cands[i] );
|
|
if( c.getDAttribute( "selected" ) == "1" )
|
|
{
|
|
return c;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
// }}}
|
|
|
|
/** @param {Astro.Blog.AstroEdit.Visualizer} visualizer */
|
|
var SmartInput = function( visualizer )
|
|
{
|
|
this.Present = false;
|
|
var destructor = new Destructor( this );
|
|
|
|
var _self = this;
|
|
var insert = function() { return Dand.textNode( "" ); };
|
|
|
|
var ModLevels = [];
|
|
var BindingBox = null;
|
|
|
|
ModLevels.Cands = function() { return ModLevels[0][0]; };
|
|
ModLevels.Action = function() { return ModLevels[0][1]; };
|
|
ModLevels.Retreat = function() { return ModLevels[0][2]; };
|
|
|
|
var __title = null;
|
|
var title = function( reload )
|
|
{
|
|
if( !reload && __title ) return __title;
|
|
|
|
var l = ModLevels.length - 1;
|
|
var t = ModLevels[ l ][0].title;
|
|
|
|
for( var i = 0; i < l; i ++ )
|
|
{
|
|
var Mod = ModLevels[i];
|
|
t = t + " > " + Mod[0].title;
|
|
}
|
|
|
|
if( __title )
|
|
{
|
|
__title.textContent = t;
|
|
}
|
|
else
|
|
{
|
|
__title = Dand.wrape( t );
|
|
}
|
|
|
|
return __title;
|
|
};
|
|
var __stage = null;
|
|
var stage = function( incoming )
|
|
{
|
|
if( __stage )
|
|
{
|
|
if( incoming )
|
|
{
|
|
IDOMElement( __stage[1] ).clear();
|
|
|
|
var ThisCands = ModLevels.Cands().Get();
|
|
if( ThisCands.length )
|
|
{
|
|
IDOMElement( __stage[1] ).loot( Dand.wrape( ThisCands ) );
|
|
}
|
|
}
|
|
|
|
return __stage;
|
|
}
|
|
var Cands = ModLevels.Cands();
|
|
|
|
var Command = Dand.wrap(
|
|
"input", null, "v_snippet_input_single"
|
|
, null, IKey.quickDef( "type", "text", "placeholder", Cands.desc, "value", "`" )
|
|
);
|
|
|
|
var CandList = Dand.wrap( "div", null, "compx smartbar-candidates", Cands.Get() );
|
|
|
|
Command.selectionStart = Command.selectionEnd = 1;
|
|
|
|
return __stage = [ Command, CandList ];
|
|
};
|
|
|
|
var CandidateCycle = -1;
|
|
var KeywordTyped = 0;
|
|
var shiftTabbed = false;
|
|
|
|
var HandleInput = function( sender, e )
|
|
{
|
|
// Don't handle if holding shift or ctrl key
|
|
if( e.shiftKey || e.ctrlKey )
|
|
{
|
|
// Except the Shift + Tab, we need to cycle this
|
|
shiftTabbed = e.shiftKey && e.keyCode == 9;
|
|
|
|
if( !shiftTabbed ) return;
|
|
}
|
|
|
|
switch( e.keyCode )
|
|
{
|
|
case 192: // `
|
|
// If we are not are the first level, do nothing
|
|
if( 1 < ModLevels.length ) break;
|
|
|
|
// Closing the quote, that means this is a block-quoted text
|
|
e.preventDefault();
|
|
insert = undefined;
|
|
|
|
// Hitting ` twice escapes the ` character itself
|
|
var v = sender.value.substr( 1 );
|
|
if( v == "" )
|
|
{
|
|
insert = function() { return Dand.textNode( "`" ); };
|
|
BindingBox.close( true );
|
|
break;
|
|
}
|
|
|
|
BindingBox.close();
|
|
// Insert the code snippet with inline flag
|
|
visualizer.insertSnippet( "code", { "inline": "on", "lang": "plain", "value": v } );
|
|
break;
|
|
|
|
case 13: // Enter
|
|
// Not closing the quote, either a direct text or the first matched action
|
|
e.preventDefault();
|
|
|
|
// No candidates, directly pass the input text to the processor
|
|
if( ModLevels.Cands().Empty && 1 < ModLevels.length )
|
|
{
|
|
ModLevels.Action()( sender.value.substr( 1 ) );
|
|
BindingBox.close();
|
|
break;
|
|
}
|
|
|
|
var selected = ModLevels.Cands().Selected();
|
|
// Check if matched an action first
|
|
if( selected )
|
|
{
|
|
insert = undefined;
|
|
var close = ModLevels.Action()( selected.getDAttribute( "key" ) );
|
|
if( close ) BindingBox.close();
|
|
}
|
|
else
|
|
{
|
|
// Insert this text directly
|
|
var v = Dand.textNode( sender.value.substr( 1 ) );
|
|
insert = function() { return v; };
|
|
BindingBox.close( true );
|
|
}
|
|
break;
|
|
|
|
case 27: // Esc
|
|
BindingBox.close();
|
|
break;
|
|
|
|
case 9: // Tab
|
|
// Hitting tab will cycle around the candidates
|
|
e.preventDefault();
|
|
|
|
var c = ModLevels.Cands().Filtered();
|
|
var l = c.length;
|
|
if( !l ) break;
|
|
|
|
CandidateCycle += e.shiftKey ? -1 : 1;
|
|
|
|
if( CandidateCycle == l )
|
|
{
|
|
CandidateCycle = 0;
|
|
}
|
|
else if( CandidateCycle == -1 )
|
|
{
|
|
CandidateCycle = c.length - 1;
|
|
}
|
|
|
|
var ThisCandidate = c[ CandidateCycle ];
|
|
ThisCandidate.setAttribute( new DataKey( "selected", 1 ) );
|
|
|
|
var CyclingKeyword = ThisCandidate.getDAttribute( "key" );
|
|
|
|
sender.value = "`" + CyclingKeyword;
|
|
|
|
// Check if only 1 matched
|
|
if( c.length == 1 && KeywordTyped == sender.value.length )
|
|
{
|
|
insert = undefined;
|
|
ModLevels.Action()( ThisCandidate.getDAttribute( "key" ) );
|
|
break;
|
|
}
|
|
|
|
sender.setSelectionRange( KeywordTyped, sender.value.length );
|
|
break;
|
|
|
|
default:
|
|
ModLevels.Cands().Filtered();
|
|
CandidateCycle = -1;
|
|
}
|
|
};
|
|
|
|
var TestEmpty = function( sender, e )
|
|
{
|
|
if( ModLevels.Retreat()( sender, e ) )
|
|
{
|
|
_self.RetreatLevel( sender );
|
|
if( !ModLevels.length ) return;
|
|
}
|
|
|
|
// Search exact matched Candidates
|
|
switch( e.keyCode )
|
|
{
|
|
case 9: // Tab, do nothing
|
|
break;
|
|
case 16: // Shift, check if shiftTabbed
|
|
if( shiftTabbed )
|
|
{
|
|
shiftTabbed = false;
|
|
break;
|
|
}
|
|
default:
|
|
if( CandidateCycle == -1 )
|
|
{
|
|
KeywordTyped = sender.value.length;
|
|
}
|
|
|
|
var c = ModLevels.Cands().Get();
|
|
var keyword = sender.value.substr( 1 );
|
|
for( var i in c )
|
|
{
|
|
var Cand = IDOMElement( c[i] );
|
|
var t = Cand.getDAttribute( "key" );
|
|
Cand.style.display = t.match( new RegExp( keyword, "i" ) ) ? "" : "none";
|
|
|
|
// Highlight the exact match
|
|
if( t.match( new RegExp( "^" + keyword + "$", "i" ) ) )
|
|
{
|
|
Cand.setAttribute( new DataKey( "selected", 1 ) );
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var ClosePanel = function( confirmed )
|
|
{
|
|
visualizer.restoreSelection();
|
|
|
|
if( confirmed && insert != undefined )
|
|
visualizer.insertAtCaret( insert() );
|
|
|
|
// Posponing this prevents the BackQuoteBinding firing
|
|
Cycle.next( function() {
|
|
_self.Present = false;
|
|
destructor.Destruct();
|
|
} );
|
|
};
|
|
|
|
// Advance the input level by CandidateAction
|
|
this.advanceLevel = function( Candidates, Action, Retreat )
|
|
{
|
|
if( ModLevels.length )
|
|
{
|
|
ModLevels[0].selected = ModLevels.Cands().Selected().getDAttribute( "key" );
|
|
}
|
|
|
|
ModLevels.unshift([ Candidates, Action, Retreat ]);
|
|
|
|
if( 1 < ModLevels.length )
|
|
{
|
|
CandidateCycle = -1;
|
|
stage( Candidates );
|
|
title( true );
|
|
stage()[0].value = "`";
|
|
stage()[0].setAttribute( "placeholder", ModLevels.Cands().desc );
|
|
BindingBox.show();
|
|
}
|
|
};
|
|
|
|
this.RetreatLevel = function( sender )
|
|
{
|
|
if( ModLevels.length == 1 )
|
|
{
|
|
BindingBox.close();
|
|
destructor.Destruct();
|
|
}
|
|
|
|
ModLevels.shift();
|
|
|
|
if( ModLevels.length )
|
|
{
|
|
var Cands = ModLevels.Cands();
|
|
var input = stage( Cands )[0];
|
|
title( true );
|
|
input.value
|
|
= "`" + Cands.Selected().getDAttribute( "key" );
|
|
|
|
input.setAttribute( "placeholder", Cands.desc );
|
|
|
|
input.selectionStart = 1;
|
|
input.selectionEnd = input.value.length;
|
|
BindingBox.show();
|
|
}
|
|
};
|
|
|
|
this.Show = function()
|
|
{
|
|
if( _self.Present ) return;
|
|
visualizer.saveSelection();
|
|
|
|
_self.Present = true;
|
|
|
|
var MsgBox = new MessageBox(
|
|
title()
|
|
, Dand.wrape( stage() )
|
|
, "Back", false
|
|
, ClosePanel
|
|
);
|
|
|
|
MsgBox.show();
|
|
|
|
var Command = stage()[0];
|
|
BindingBox = MsgBox;
|
|
|
|
Command.focus();
|
|
|
|
var DCommand = IDOMElement( Command );
|
|
var KDown = KeyHandler( Command, HandleInput );
|
|
var KUp = KeyHandler( Command, TestEmpty );
|
|
|
|
DCommand.addEventListener( "KeyDown", KDown );
|
|
DCommand.addEventListener( "KeyUp", KUp );
|
|
|
|
destructor.Register( function() {
|
|
DCommand.removeEventListener( "KeyDown", KDown );
|
|
DCommand.removeEventListener( "KeyUp", KUp );
|
|
} );
|
|
};
|
|
};
|
|
|
|
var MasterInput = function( visualizer )
|
|
{
|
|
var Cands = {
|
|
"Article Reference": { module: "ArticleReference", desc: "Links to other article" }
|
|
, "Article Content": { module: "ArticleContent", desc: "Put contents of an article" }
|
|
, "facts": { module: "Facts", desc: "Facts, a fact bubble popup when mouseover" }
|
|
, "footnote": { module: "Footnote", desc: "Footnote, a footnote displayed at the end of article" }
|
|
, "h1": { module: "Heading", options: 1, desc: "Heading, size 1" }
|
|
, "h2": { module: "Heading", options: 2, desc: "Heading, size 2" }
|
|
, "h3": { module: "Heading", options: 3, desc: "Heading, size 3" }
|
|
, "h4": { module: "Heading", options: 4, desc: "Heading, size 4" }
|
|
, "h5": { module: "Heading", options: 5, desc: "Heading, size 5" }
|
|
};
|
|
|
|
var LoadModule = function( mod )
|
|
{
|
|
var ldr = new Loader( service_uri, "o" );
|
|
var ModItem = Cands[ mod ];
|
|
ldr.load( moduleNs + ModItem.module, function( e ) { ModuleLoaded( mod, e ); } );
|
|
};
|
|
|
|
var InputBox = null;
|
|
var ModuleLoaded = function( sender, e )
|
|
{
|
|
/** @type {Astro.Blog.AstroEdit.SmartInput.ICandidateAction} */
|
|
var module = new ( __import( e ) )( visualizer, sender );
|
|
|
|
var ModItem = Cands[ sender ];
|
|
|
|
module.GetCandidates( function( x ) {
|
|
InputBox.advanceLevel(
|
|
new Candidates( ModItem.module, ModItem.desc, x )
|
|
, module.Process.bind( module )
|
|
, module.Retreat.bind( module )
|
|
);
|
|
} );
|
|
};
|
|
|
|
var BackQuoteBinding = function ( sender, e )
|
|
{
|
|
if( !InputBox || InputBox.Disposed )
|
|
{
|
|
InputBox = new SmartInput( visualizer );
|
|
InputBox.advanceLevel(
|
|
// First level Candidates
|
|
new Candidates( "Quick Access", "Keyword", Cands )
|
|
, LoadModule
|
|
, function( sender, e ) { return sender.value == ""; }
|
|
);
|
|
}
|
|
|
|
if( !InputBox.Present && code == 192 )
|
|
{
|
|
e.preventDefault();
|
|
InputBox.Show();
|
|
}
|
|
};
|
|
|
|
IDOMObject( document ).addEventListener( "KeyDown", KeyHandler( document, BackQuoteBinding ), false );
|
|
};
|
|
|
|
ns[ NS_EXPORT ]( EX_CLASS, "SmartInput", MasterInput );
|
|
})();
|