Tweetbox

tweetbox

by Mark Benedict Santiago

HTML

<div class="tw-inline-login clearfix">
	<div class="profile-image">
		<div class="image" style="background-image: url('{{twitterUser('profile_image_url')}}')"></div>
	</div>
	<div class="tweetbox">
		<div class="input-area">
			<twinput max=140 contenteditable="true" id="editor" placeholder="What's happening?" ng-model="tweetText" ng-class="{'has-photo': tweetImage}"></twinput>
			<div class="photo-container" ng-class="{'has-photo': tweetImage}">
				<div class="photo" style="background-image:url('{{thumbnailUrl}}')">
					<span class="glyphicon glyphicon-remove remove" ng-click="removeImage()"></span>
				</div>
			</div>
		</div>
		<div class="buttons">
			<span class="pull-right">
				<span class="counter" ng-bind="tweetLength(tweetText, offset)" class="pull-right text-muted"></span>
				<button class="btn btn-primary tweet" type="button" ng-disabled="(!tweetText || tweetText.length < 1) && (!tweetImage || !fixed) || (tweetLength(tweetText, offset) < 0)" ng-click="tweet(fixed)">
					<i class="fa fa-twitter"></i> Tweet
				</button>
			</span>
			<div class="btn btn-default btn-file tw-image pull-left" ng-hide="tweetImage">
				<span class="glyphicon glyphicon-camera"></span> Add Photo
				<input type="file" name="picture" on-selected-change="onFileSelect(file)">
			</div>
		</div>
		<span ng-bind-html="tw_message"></span>
	</div>
</div>

CSS

.tw-inline-login, .tw-inline-form{
	margin: 20px auto;
	width: 100%;
	min-width: 450px;
	max-width: 600px;

	.profile-image{
		float: left;
		width: 10%;
		
		.image{
			border-radius: 6px;
			width: 35px;
			height: 35px;
			background-size: contain;
		}
	}

	.tweetbox{
		float: left;
		width: 90%;

		.input-area{
			border-radius: 4px;
			margin-bottom: 10px;

			twinput{
				// appearance
				background-color: white;
				border: thin solid lightblue;
				border-radius: inherit;
				cursor: text;

				// box
				display: block;
				vertical-align: top;
				padding: 8px 10px;

				// height
				overflow-y: auto;
				min-height: 35px;
				max-height: 240px;

				// width
				min-width: 100px;
				width: 100%;

				// others
				word-wrap: break-word;

				// font
				font-size: 14px;
				line-height: 20px;

				// make it look like a textarea
				-webkit-user-modify: read-write;
				-webkit-line-break: after-white-space;
				direction: ltr;
				unicode-bidi: embed;

				span.red{
					background-color: lightpink;
				}
			}

			twinput.expand{
				min-height: 80px;
			}

			twinput:focus{
				outline: none;
			}

			twinput.has-photo{
				border-bottom-left-radius: 0px;
				border-bottom-right-radius: 0px;
				border-bottom: none;
			}

			twinput:empty:not(:focus):before{
				color: dodgerblue;
				content:attr(placeholder)
			}
			.photo-container{
				display: none;
				border: thin solid lightblue;
				border-bottom-left-radius: inherit;
				border-bottom-right-radius: inherit;
				height: 50px;
				width: 100%;
				padding: 5px;

				.photo{
					display: inline-block;
					float: left;
					border-radius: 2px;
					width: 40px;
					height: 40px;
					background-size: cover;
					background-repeat: no-repeat;
					position: relative;

					.remove{
						color: white;
						background-color: rgb(56, 56, 56);
						font-size: 10pt;
						opacity: 0.5;
						float: right;
						border-bottom-left-radius: 3px;
						padding:...

JavaScript

.directive('twinput', function(Twitter, $sce) {
	return {
		restrict: 'E',
		require: '?ngModel',
		link: function(scope, element, attr, model){
			if(!model) return
			// initialize
			rangy.init();
			var classApplier = rangy.createClassApplier("red");
			var editor = element[0];

			var handleEditorChangeEvent = (function() {
				var timer;
				function debouncer() {
					if (timer) timer = null;
					highlightExcessCharacters();
				}
				return function() {
					if (timer) window.clearTimeout(timer);
					timer = window.setTimeout(debouncer, 10);
				};
			})();

			function highlightExcessCharacters() {
				// Save selection
				var sel = rangy.getSelection();
				var savedSel = sel.saveCharacterRanges(editor);
				// Remove previous highlight
				var range = rangy.createRange();
				range.selectNodeContents(editor);
				var editorCharCount = range.text().length;
				classApplier.undoToRange(range);
				// Create a range selecting the excess characters
				range.selectCharacters(editor, attr.max, editorCharCount);
				// Highlight the excess
				classApplier.applyToRange(range);
				// Restore selection
				sel.restoreCharacterRanges(editor, savedSel);
			}

			function setCaretToEnd(){
				//Set caret to end
				var selection = rangy.getSelection();
				selection.selectAllChildren(element[0]);
				selection.collapseToEnd();
			}

			element.on('keyup keydown cut copy paste', function() {
				// assign element value to model
				scope.$evalAsync(read);
				// highlight excess
				handleEditorChangeEvent();
			});

			model.$render = function() {
				element.html($sce.getTrustedHtml(model.$viewValue));
				handleEditorChangeEvent();
				if(element.text().trim() == scope.hashes) setCaretToEnd();
			};
			function read() {
				var html = element.text();
				model.$setViewValue(html);
			}
			read(); // initialize

			element.on('focus', function(){
				if(scope.tweetText == "" || scope.tweetText == undefined){
					scope.hashes =...