penguin/AstroJS

Javascript framework for my blog

botanjs/src/Astro/Blog/AstroEdit/SmartInput/_this.js

raw ยท 13052 bytes

(function(){
	var ns = __namespace( "Astro.Blog.AstroEdit" );

	/** @type {typeof Dandelion} */
	var Dand                                    = __import( "Dandelion" );
	/** @type {function(...?): Dandelion.IDOMElement} */
	var IDOMElement                             = __import( "Dandelion.IDOMElement" );
	/** @type {typeof 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();
			}
		};

		new IDOMObject( document ).addEventListener( "KeyDown", KeyHandler( document, BackQuoteBinding ), false );
	};

	ns[ NS_EXPORT ]( EX_CLASS, "SmartInput", MasterInput );
})();