penguin/AstroJS

Javascript framework for my blog

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

raw ยท 13366 bytes

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

	/** @type {Dandelion} */
	var Dand                                    = __import( "Dandelion" );
	/** @type {Dandelion.IDOMElement} */
	var IDOMElement                             = __import( "Dandelion.IDOMElement" );
	/** @type {System.Debug} */
	var debug                                   = __import( "System.Debug" );
	/** @type {System.utils} */
	var utils                                   = __import( "System.utils" );
	/** @type {System.utils.Perf} */
	var Perf                                    = __import( "System.utils.Perf" );
	/** @type {System.utils.DataKey} */
	var DataKey                                 = __import( "System.utils.DataKey" );
	/** @type {System.utils.EventKey} */
	var EventKey                                = __import( "System.utils.EventKey" );
	/** @type {System.utils.IKey} */
	var IKey                                    = __import( "System.utils.IKey" );
	/** @type {System.Net.ClassLoader} */
	var Loader                                  = __import( "System.Net.ClassLoader" );
	/** @type {Components.MessageBox} */
	var MessageBox                              = __import( "Components.MessageBox" );
	/** @type {Components.Mouse.ContextMenu} */
	var ContextMenu                             = __import( "Components.Mouse.ContextMenu" );
	/** @type {Astro.Blog.Config} */
	var Config                                  = __import( "Astro.Blog.Config" );

	var snippetList = IKey.quickDef(
		  "Code"             , "background: white; color: cornflowerblue;"
		  , "Image"          , "background: #ff0084;"
		  , "Sound"          , "background: YellowGreen;"
		  , "Video"          , "background: Crimson;"
		  , "Spoiler"        , "background: cornflowerblue;"
		  , "Link"           , "background: blue;"
		  , "AcquireLib"     , "background: black;"
		  , "Html"           , "background: coral;"
		  , "SiteFile"       , "background: royalblue;"
		  , "Heading"        , ""
		  , "Footnote"       , ""
		  , "RespH"          , ""
		  , "Meta"           , ""
		  , "ArticleLink"    , ""
		  , "ArticleContent" , ""
	);

	var snippetNs = "Astro.Blog.AstroEdit.Visualizer.Snippet.";
	//// Document Visualizer visualize snippets, used in AstroEdit and comments
	var Visualizer = function ( e_document, snippetControls, service_uri )
	{
		var Article = ns[ NS_INVOKE ]( "Article" );
		var loadedModule = {};

		//// Constants
		var article;
		// "[^]" does not work in IE
		var stackMatch = /\[([a-z][0-9a-z]*?)([\s\S]*?)\]([\s\S]*?)\[\/\1\]/ig;
		var typeMatch = /([a-z][0-9a-z]*)\=\"([^"]+)\"/ig;

		var snippetTokenQueue = {};
		
		if ( e_document instanceof Article )
		{
			article = e_document;
			// Allow Html snippet
		}
		else if ( e_document instanceof CeDocument )
		{
			article = e_document;
		}
		else return;

		////// Variables
		var contentDiv = Dand.wrap()
		var selRange;
		var snippetExists = false;
		var lastOffset = 0;
		var raw;

		/////  private methods
		var snippetWrap = function( snippetType, element, editable, _with )
		{
			var snippet = Dand.wrap(
				_with || "div"
				, Perf.uuid
				, "comp v_boundary"
				, element
				, [
					new IKey( "contentEditable", editable ? "true" : "false" )
					, new DataKey( "type", snippetType )
				]
			);

			return snippet;
		};

		var appendLinebreak = function ()
		{
			this.parentNode.insertBefore(Dand.wrap("br"), this.nextSibling);
		}

		var insertLinebreak = function ()
		{
			this.parentNode.insertBefore(Dand.wrap("br"), this);
		};

		var snippetDelete = function ()
		{
			this.parentNode.removeChild(this);
		};

		var snippetBBCode = function()
		{
			var snippet = IDOMElement( this );
			var type = snippet.getDAttribute( "type" ).toLowerCase();
			/** @type {Astro.Blog.AstroEdit.Visualizer.Snippet.Model} */
			var module = loadedModule[ type ];
			var title = "An error occurred";
			var message = "No such module";

			if( module )
			{
				title = "BBCode for Snippet: " + type;
				message = module.compile( this.firstChild );
			}

			new MessageBox( title, Dand.wrapne( "pre", message ) ).show();
		};

		var createSnippetMenu = function ( snippetProp, snippet, editHandler, snippetActions )
		{
			var contextItems = [
				editHandler && new EventKey( "Edit", editHandler.bind( snippetProp ) )
				, new EventKey( "Insert a linebreak", insertLinebreak.bind( snippet ) )
				, new EventKey( "Append a linebreak", appendLinebreak.bind( snippet ) )
				, new EventKey( "Delete", snippetDelete.bind( snippet ) )
				, new EventKey( "View BBCode", snippetBBCode.bind( snippet ) )
			];

			e_document.updateContent();
			
			if ( snippetActions instanceof Array )
			{
				contextItems = snippetActions.concat( contextItems );
			}
			
			return new ContextMenu( snippet, contextItems, "RMB" );
		};

		var _savSelection = function ()
		{
			contentDiv.focus();
			if ( window.getSelection )
			{
				var sel = window.getSelection();
				if ( sel.getRangeAt && sel.rangeCount )
				{
					selRange = sel.getRangeAt(0);
				}
			}
			else if ( document.selection && document.selection.createRange )
			{
				selRange = document.selection.createRange();
			}
			else
			{
				selRange = null;
			}
		};

		var _resSelection = function ()
		{
			contentDiv.focus();
			if ( selRange )
			{
				if ( window.getSelection )
				{
					var sel = window.getSelection();
					sel.removeAllRanges();
					sel.addRange( selRange );
				}
				else if ( document.selection && selRange.select )
				{
					selRange.select();
				}
			}
		};
		
		var lastLine = Dand.wrap( "br", "v_linebreak" );
		
		var ensureLastLinebreak = function ()
		{
			if( !Dand.id( "v_linebreak" ) )
			{
				insertElement( lastLine, true );
			}
			else
			{
				insertElement( contentDiv.removeChild( lastLine ), true );
			}
		};

		var insertAtCaret = function( element, newLine )
		{
			var sel, range;
			if( newLine == undefined ) newLine = true;
			
			sel = window.getSelection();
			if ( sel.getRangeAt && sel.rangeCount )
			{
				range = sel.getRangeAt(0);
				range.deleteContents();
				range.insertNode( element );
				range.setStartAfter( element );
				range.setEndAfter( element );
				range.collapse( false );
				sel.removeAllRanges();
				sel.addRange( range );
			}
			
			e_document.updateContent();

			if( newLine ) ensureLastLinebreak();
		};

		var insertElement = function( element, override )
		{
			if ( override )
			{
				contentDiv.appendChild( element );
				return;
			}
			
			_resSelection();
			insertAtCaret( element );
		};

		var replaceToken = function( element )
		{
			contentDiv.insertBefore( element, this.token );
			contentDiv.removeChild( this.token );
		};

		var parseKeys = function ( match, name, value )
		{
			this[ name ] = value;
		};

		var snippetQueue = function( type, alias )
		{
			var q = snippetTokenQueue[ type ] || [];

			if( alias )
			{
				if( !( alias instanceof Array ) ) alias = [ alias ];

				for( var i in alias )
				{
					q = snippetTokenQueue[ alias[i] ] || [];
					if( q.length ) return q;
				}
			}

			return ( snippetTokenQueue[ type ] = q );
		};
		
		var parseSnippet = function( match, type, properties, _value, offset )
		{
			var temp, i, j;
			// Texts goes first
			if ( lastOffset != offset )
			{
				// innerText does not work in firefox:(
				temp = Dand.wrape( raw.substr( lastOffset, offset - lastOffset ) );
				// innerHTML will escape html entities, now replace linebreaks to br
				temp.innerHTML = temp.innerHTML.replace( /[\r\n]/g, "<br>" );
				IDOMElement( contentDiv ).loot( temp );
			}

			lastOffset = offset + match.length;

			// Snippet Args
			temp = { value: _value };
			// to ekeys
			properties.replace( typeMatch, parseKeys.bind( temp ) );

			// Now deal with snippet
			/** @type {Astro.Blog.AstroEdit.Visualizer.Snippet.Model} */
			var _module = loadedModule[ type ];
			if ( _module )
			{
				// Visualize snippet
				new ( _module )( insertElement, snippetWrap, createSnippetMenu, temp );
			}
			else
			{
				// no such module, or module is not loaded yet
				debug.Info( "[Visualizer] Queueing: " + type );
				var token = snippetWrap( "token", type, false );
				token.setAttribute( "class", token.getAttribute( "class" ) + " flsf" );
				snippetQueue( type ).push([ token, temp ]);

				insertElement( token, true );
			}
		};

		var linebreakNodes = ["P", "DIV"];
		var linebreakNode = function( name )
		{
			if( -1 < linebreakNodes.indexOf( name.toUpperCase() )  ) return "\n";
			return "";
		};

		var ensureGoodness = function ()
		{
			_savSelection();
			ensureLastLinebreak();
		};
		
		var stepContent = function ( element, level )
		{
			var j , snippetType
			, elements = element.childNodes
			, output = "";

			for (var i = 0, l = elements.length; i < l; i ++)
			{
				j = elements[i];
				
				// do nothing on lastline
				if (j == lastLine) continue;
				
				if (j.nodeType == 1)
				{
					snippetType = IDOMElement( j ).getDAttribute( "type" );
					if( snippetType && loadedModule[ snippetType = snippetType.toLowerCase() ] )
					{
						output += loadedModule[ snippetType ].compile( j.firstChild );
					}
					else if ( j.firstChild )
					{
						// Text
						output += linebreakNode( j.nodeName ) + stepContent(j, ++level);
					}
					else
					{
						// Handle special <div><br></div> => one line break
						if(l == 1 && j.nodeName.toUpperCase() == "BR") continue;

						// Line breaks
						output += "\n";
					}
				}
				// Text
				else if (j.nodeType == 3)
				{
					output += j.nodeValue;
				}
			}
			
			return output;
		};

		///// Initializations
		var snippetReady = function ( e )
		{
			// Initialize modules
			var mod_name = e.substr( e.lastIndexOf(".") + 1, e.length ).toLowerCase()
			/** @type {Astro.Blog.AstroEdit.Visualizer.Snippet.Model} */
			var _module = __import( e );
			var alias = _module.alias;

			// Implement module if valid and available
			if ( _module && _module.compile )
			{
				loadedModule[ mod_name ] = _module;
				debug.Info( "[Visualizer] Module loaded: " + mod_name );
				// Set alias
				if ( alias )
				{
					alias = ( alias instanceof Array ) ? alias : [ alias ];

					for ( var k in alias )
					{
						loadedModule[ alias[k] ] = _module;
						debug.Info( "[Visualizer] Alias: " + mod_name + " => " + alias );
					}
				}

				// Append module"s control
				temp = Dand.wrapne(
					"span"
					, mod_name
					, new IKey(
						"style"
						, utils.objSearch(
							snippetList
							/** @param {System.utils.IKey} a */
							, function( a ) { return a.keyName.toLowerCase() == mod_name }
							, "keyValue"
						)
					)
				);

				snippetControls.appendChild( temp );
				snippetControls.appendChild( Dand.textNode( "\t" ) );
				temp.onclick = new ( _module )( insertElement, snippetWrap, createSnippetMenu );

				var queue = snippetQueue( mod_name, _module.alias );
				for( var i in queue )
				{
					var token = queue[i];
					new ( _module )( replaceToken.bind({ token: token[0] }), snippetWrap, createSnippetMenu, token[1] );
					delete queue[i];
				}
			}
			else
			{
				if ( _module )
				{
					delete loadedModule[ mod_name ];
					debug.Info( "[Visualizer] Module discarded: invalid module \"" + mod_name + "\"" );
				}
				else
				{
					debug.Info( "[Visualizer] Module \"" + mod_name + "\" does not exists" );
				}
			}
		};

		this.setContentDiv = function( element )
		{
			contentDiv = element;
			// listeners that return values cannot use addEventListener
			contentDiv.onkeypress = function(e)
			{
				if (e.which == 13)
				{
					insertAtCaret( Dand.wrap( "br" ) );
					_savSelection();
					return false;
				}
				return true;
			};
			
			temp = IDOMElement( contentDiv );
			temp.addEventListener( "Input", ensureGoodness );
			temp.addEventListener( "KeyUp", ensureGoodness );
			temp.addEventListener( "Click", ensureGoodness );

			contentDiv.focus();
			ensureGoodness();

			try {
				document.execCommand( "insertBrOnReturn", false, "true" );
			} catch(e) {
				debug.Info( "Not firefox" );
			}
		};

		this.saveRaw = function ()
		{
			return stepContent( contentDiv, 0 );
		};

		this.visualizeData = function ( data )
		{
			if ( !data ) return;
			
			lastOffset = 0;
			// clear content
			contentDiv.innerHTML = "";

			( raw = data ).replace( stackMatch, parseSnippet );
			if ( lastOffset != raw.length )
			{
				// innerText does not work in firefox:(
				temp = Dand.wrape( raw.substr( lastOffset, raw.length - lastOffset ) );
				// innerHTML will escape html entities, now replace linebreaks to br
				temp.innerHTML = temp.innerHTML.replace( /[\r\n]/g, "<br>" );
				IDOMElement( contentDiv ).loot( temp );
			}
			temp = null;
			raw = null;
		};

		this.saveSelection = _savSelection;
		this.restoreSelection = _resSelection;
		this.insertAtCaret = insertAtCaret;

		this.insertSnippet = function( type, options )
		{
			var _module = loadedModule[ type ];

			new ( _module )(
				// Disables the override
				function( e ) { insertElement( e ); }
				, snippetWrap, createSnippetMenu, options
			);
		};

		article.invoke(this);

		var modules = [];
		for( var i in snippetList )
		{
			modules[ modules.length ] = snippetNs + snippetList[i].keyName;
		}

		var ldr = new Loader( service_uri, "o" );
		ldr.load( modules, snippetReady );
	};

	ns[ NS_EXPORT ]( EX_CLASS, "Visualizer", Visualizer );
})();