2015年10月21日星期三

DNN7 WebApi CamelCase problem

I used to encounter an error with DotNetNuke version 7 several time which displays all modules with a name of "undefined" and a broken picture.



This happens when people try to configure their DNN Web Api library to JSON format. Because in C# this is usually done by PascalCase and in JavaScript this is but done using camelCase. So when people want to consume the service with some javascript library, AngularJS for example, they configure the data source to JSON format. In DNN, it's usually done like this.

public class RouteMapper : IServiceRouteMapper
{
  public void RegisterRoutes(IMapRoute mapRouteManager)
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    formatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

    mapRouteManager.MapHttpRoute("Invoice", "default", "{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional }, namespaces: new[] { "DnnWebApi.Controllers" });
  }
}

This will however configure the data format to be camelcase globally. And DNN has its own Web Api implementations which use actually PascalCase. So camelCase will make its client scripts break.

for (var i = 0; i < moduleList.length; i++) {
  ul.append('<li><div class="ControlBar_ModuleDiv" data-module=' + moduleList[i].ModuleID + '><div class="ModuleLocator_Menu"></div><img src="' + moduleList[i].ModuleImage + '" alt="" /><span>' + moduleList[i].ModuleName + '</span></div></li>');
}

So my solution is pretty simple and dumb. Switch to camelcase when the controller instantiates, and switch it back when it disposes.

public class AppController: DnnApiController, IDisposable
{
  private IContractResolver _resolver;

  public AppController()
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;

    _resolver = formatter.SerializerSettings.ContractResolver;
    formatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

  }

  //your implementation...

  void IDisposable.Dispose()
  {
    var formatter = System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    formatter.SerializerSettings.ContractResolver = _resolver;            
  }
}

2015年10月17日星期六

Simple sidebar transition

Simple Sidebar Transition

Sidebars are responsive components that display various forms of information to the side of applications or sites. This approach is designed to fit in with the Bootstrap framework. Hence, Bootstrap components and JQuery are needed.

Default Sidebar

Default Sidebar works as same as .col-md-3 in Bootstrap grid system. But when the device width is less than its breakpoint size, it hides itself instead of Collapsing to start.

<div class="container">
  <div class="row">
    <div class="sidebar">
    </div>
    <div class="col-md-10">
    </div>
  </div>
</div>

Fixed to Top

The sidebar hides itself when the device width is less than its breakpoint size. And it can be toogled to show and hide the element via class changes.

Adjustment required

The fixed sidebar will overlay Bootstrap Navbar and create a gap when the screen scrolls down. So the following adjustments are needed.

  • Add .sidebar-fixed and include a .sidebar-container to contain content.
  • Add .navbar-fixed-top to navbar if available.
  • Add offsetting class .col-md-offset-2 to the right side.
  • Add padding to the top of the <body>. By default, the navbar is 50px high.
  • Set up toggle element via data-toogle-target attribute.

Example

<div class="container">
  <div class="row">
    <div class="sidebar sidebar-fixed">
      <button type="button" class="btn" data-toggle-target="#sidebar-fixed-1"><span class="glyphicon glyphicon-align-justify"></span></button>
      <div class="sidebar-container">
    </div>
    <div class="col-md-10 col-md-offset-2">
    </div>
  </div>
</div>

Javascript

Include the following code snippet to your page and replace the toggle element selector with your own. By default, it is .sidebar>.btn It requires JQuery.

+function($){
  function toggleSidebar(e) {
    $($(e.currentTarget).attr('data-toggle-target')).toggleClass('open');
  }

  $(document).on('click.sidebar', '.sidebar>.btn', toggleSidebar);
}(jQuery)

Inverted Fixed to Top

Modify to sidebar to have it collapse regardless deveice width by adding .sidebar-fixed-inverse.

<div class="container">
  <div class="row">
    <div class="sidebar sidebar-fixed-inverse">
      ...
    </div>
  </div>
</div>

Style sidebar

Include the following style to your page or stylesheet file to make it properly display.

.sidebar-fixed, .sidebar-fixed-inverse{
  background-color:#eee;
  position:fixed;
  top:51px;
  bottom:0;
  z-index:2;
  transition: left 0.3s;
  -moz-transition: left 0.3s; /* Firefox 4 */
  -webkit-transition: left 0.3s; /* Safari 和 Chrome */
  -o-transition: left 0.3s; /* Opera */
}
.sidebar>.btn{
  display:none;
  border-radius:0;
  background-color:#eee;
  border-color:#e0e0e0;
}
.sidebar-fixed>.btn, .sidebar-fixed-inverse>.btn{
  position:absolute;
  right:-40px;
  top:0;
 display:block;
}
.sidebar-fixed>.sidebar-container, .sidebar-fixed-inverse>.sidebar-container{
  overflow:auto;
  position:absolute;
  top:0;
  bottom:0;
  margin:0;
  width:100%;
}
.sidebar.open{
  left:0;
}
@media(min-width: 768px){
  .sidebar{
    display:none;
  }
  .sidebar-fixed, .sidebar-fixed-inverse{
    display:block;
    width:25%;
    left:-25%;
  }
}
@media(min-width: 992px){
  .sidebar{
    display:block;
    float:left;
    width:16.66666667%;
  }
  .sidebar-fixed, .sidebar-fixed-inverse{
    width:16.66666667%;
    left:-16.66666667%;
  }
  .sidebar-fixed{
    left:0;
  }
  .sidebar-fixed>.btn{
    display:none;
  }
}

2015年10月4日星期日

UNBLUR BACKGROUND (PART 3)

Fading effect

So the fading effect would be using a loop of timed-out call to simulate. It will be something like this...

var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], 
    pctx = painter.getContext('2d'), ctx = $('#canvas2')[0].getContext('2d'), img= $('img')[0];
for(var i = i; i <= 10; i++){
  (function(i){
    setTimeout(function(){
        pctx.strokeStyle = "rgba(0,0,0,"+(1 - i / 10)+")";
        pctx.beginPath();
        pctx.moveTo(760, 20);
        pctx.lineTo(760, 100);
        pctx.stroke();
        ctx.drawImage(img, 0, 0, img.width, img.height);
        ctx.globalCompositeOperation = "destination-in";
        ctx.drawImage(painter, 0, 0);
        ctx.globalCompositeOperation = "source-over";
 },i * 200);
  })(i);
}

But I cannot make it directly work with mousemove event, because it fires for every pixel mouse move which is very costly and inefficient. And also for every line it draws, it draws also one image and one canvas as well, which is also very expensive.

This is what I'm going to do. Firstly, the event, instead of directly drawing, it just records the current mouse coordinate and the time stamp and trigger one recursive function if it's not started. And then the recursive function, which will constantly run once in a short while till no more event occurs, it contains the logic of drawing, coordinate collection adding and removal and the indicator that decide whether it can be triggered. Finally, I need a collection that stores coordinates so that I can draw all of them in the painter and copy the painter to the main canvas.

Collect the coordinates

The first thing is, I'm going to need an array to store coordinate objects.

var points = [];

Handle the mousemove event

Firstly I instantiate several global variables so I can read in the recursive function. And in the event handler, I record the coordinate and time stamp and trigger the recursive function if it’s not started yet.

var x, y, eventTimestamp, isContinue;
$(document).on('mousemove', function(e){
  x = e.clientX, y = e.clientY, eventTimestamp = Date.now;
  if(!isContinue) draw();
});

The recursive function

This function does basically three things.

  • It adds and removes coordinates of the collection.
  • It checks and decides whether it continues and can be triggered by the event handler.
  • It draws all lines using adjacent coordinates and put it to the main canvas.

This function makes a delayed call to itself if mousemove event keeps firing. And when it executes it adds the new coordinate object to the collection. I can use this for the fading effect. So I put time stamps in coordinate objects and compare them with the newest one to decide the opacity value. By doing this, it draws lines with different fading effects and refreshes the canvas every time it executes. That's how the amination works.

function draw(){
  var currentTimestamp = Date.now, newPoint = {x: x, y: y, t: currentTimestamp};
  
  //check if to continue
  isContinue = currentTimestamp - eventTimestamp < 500;
  //add new coordinate
  if(isContinue){
 points.unshift(newPoint);
  }
  //remove faded out coordinates
  for(var i = 0; i < points.length;){
 currentTimestamp - points[i].t > 1000 ? points.splice(i, points.length) : i++;
  }
  //recursive call
  if(points.length){
 setTimeout(draw, 20);
  }
  
  var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], pctx = painter.getContext('2d');
  //clear painter, ready to draw
  pctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  //draw all lines in the collection
  for (var i = 1; i < points.length; i++) {
 pctx.strokeStyle = "rgba(0,0,0," + Math.max(1-(currentTimestamp-points[i].t)/1000, 0) + ")";
 //umcomment this line to get dynamic line width
 //pctx.lineWidth = 25+75*Math.max(1-Math.sqrt(Math.pow(points[i].x-points[i-1].x,2)+Math.pow(points[i].y-points[i-1].y,2))/50,0);
 pctx.lineWidth = 50;
 pctx.beginPath();
 pctx.moveTo(points[i - 1].x, points[i - 1].y);
 pctx.lineTo(points[i].x, points[i].y);
 pctx.stroke();
  }
  var ctx = $('canvas')[0].getContext('2d'), img= $('img')[0];
  //copy-paste 
  ctx.drawImage(img, 0, 0, img.width, img.height);
  ctx.globalCompositeOperation = "destination-in";
  ctx.drawImage(painter, 0, 0);
  ctx.globalCompositeOperation = "source-over";
}

Handle resize

The background will automatically re-adjust to adopt the new width or height. So the image and canvases are also need to be taken care of.

$(window).on('resize', function(){
var img = $('img')[0];
  var windowHeight = $(window).height(), windowWidth = $(window).width(), windowRate = windowWidth/windowHeight;
  var imgHeight = img.naturalHeight, imgWidth = img.naturalWidth, imgRate = imgWidth/imgHeight;

  if(windowRate>=imgRate){
    $(img).attr('height', (imgHeight/imgWidth*windowWidth)).attr('width', windowWidth);
  }else{
    $(img).attr('width', (imgWidth/imgHeight*windowHeight)).attr('height', windowHeight);
  }
  $('canvas').attr('width', windowWidth).attr('height', windowHeight);
});

So that's the all of it. I put the demo effect on the background. Feel free to try.



UNBLUR BACKGROUND (PART 1)

UNBLUR BACKGROUND (PART 2)

2015年9月30日星期三

UNBLUR BACKGROUND (PART 2)

Start to draw

The basic idea is to draw lines with blurring effect from the last coordinate to the current coordinate whenever mouse moved and make it fade over time to zero. Here I want to draw line with brash effect and blur effect. And then to make it fade, I simply manipulate the opacity a bit. So let's see what comes out.

var ctx = $('canvas')[0].getContext('2d');
ctx.lineCap = "round";
ctx.shadowColor = "#000";
ctx.lineWidth = 30;
ctx.shadowBlur = 30;
for(var i=0; i<=10;i++){
 ctx.strokeStyle = "rgba(0,0,0,"+(1-i/10)+")";
 ctx.beginPath();
 ctx.moveTo(i*70+50, 20);
 ctx.lineTo(i*70+50, 100);
 ctx.stroke();
}

Well, to draw lines with a single color is easy. What about copy some photo parts and fill them to those lines? There are two options, compositing and clipping. Initially I thought clipping was more promising because it was simpler and more straightforward. Soon after that I began to have problems with implementing blurring and fading effects. And most importantly the browser started to lag when those was added. So I decided to try the other approach.

Global Composite Operation

By definition, the canvas 2D rendering context globalCompositeOperation property of the Canvas 2D API sets the type of compositing operation to apply when drawing new shapes. And the type "destination-in" keeps the existing canvas content where both the new shape and existing canvas content overlap. Everything else is made transparent. So I can draw a line and paste the image to achieve the goal. See the effect.

But wait, it can only draw something in the overlap zone. to make trace of mouse move I'm definitely going to draw lots of lines not just in a narrowing area. Ideally, it would be great I can draw whole trace in somewhere else and paste it into canvas. It took me a while to figure out that it could actually copy the drawing from one canvas and paste it to another one. So the idea came out. Draw the whole trace in a temporary canvas and then paste the drawing to the main canvas. And I found out that this temp canvas doesn't have to be inside the body. It just needs to be instantiate properly with exactly same width and height.

var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], 
    pctx = painter.getContext('2d');

pctx.lineCap = "round";
pctx.shadowColor = "#000";
pctx.lineWidth = 30;
pctx.shadowBlur = 30;

for(var i = 0; i <= 10; i++){
 pctx.strokeStyle = "rgba(0,0,0," + (1 - i / 10) + ")";
 pctx.beginPath();
 pctx.moveTo(i * 70 + 50, 20);
 pctx.lineTo(i * 70 + 50, 100);
 pctx.stroke();
}

var ctx = $('canvas')[0].getContext('2d'), img= $('#imgtarget')[0];

ctx.drawImage(img, 0, 0, img.width, img.height);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(painter, 0, 0);
ctx.globalCompositeOperation = "source-over";

So I've got the method to draw. The next step would be to use it to handle the mousemove event. But handling the event was far more complicated than I thought. I will break it down next time.



UNBLUR BACKGROUND (PART 1)

UNBLUR BACKGROUND (PART 3)

2015年9月27日星期日

UNBLUR BACKGROUND (PART 1)

Recently I've found a very interesting animation effect from this website, which can unblur the blurred background and leave a disappearing track on the screen. I want to make one myself but I couldn't find any library can do something like that. So I tried to build one from scratch. So the basic idea is to use a blurred image as background image and use HTML5 <canvas> tag to draw a small dot with the original image on the location whenever mouse move.

Prepare background

Frist thing I will need two same images, one blur and one not. And then set up the blurred image as background and make it not repeat, cover the whole screen and not moving with scroll. Here background-attachment:fixed makes it fixed with regard to the viewport, and background-size:cover makes it always cover the whole screen and keep the image aspect ratio.

body{
  background: url('images/background-blur.jpg') no-repeat 0 0 fixed;
  background-size: cover;
}

Prepare image

And then set up the image to draw. This image will basically need to be kept invisible. But I cannot use display:none here because I'm going to need to read its width and height in the future. Most importantly, the image needs to be adjust its width and height to be identical with the background.

<div style="position:absolute;width:0;height:0;overflow:hidden;">
  <img src="background.jpg" alt="" / >
</div>
<script>
  var img = $('img')[0];
  var windowHeight = $(window).height(), windowWidth = $(window).width(), windowRate = windowWidth/windowHeight;
  var imgHeight = img.naturalHeight, imgWidth = img.naturalWidth, imgRate = imgWidth/imgHeight;

  if(windowRate>=imgRate){
    $(img).attr('height', (imgHeight/imgWidth*windowWidth)).attr('width', windowWidth);
  }else{
    $(img).attr('width', (imgWidth/imgHeight*windowHeight)).attr('height', windowHeight);
  }
</script>

Prepare canvas

I also need create a <canvas> element, make it cover the whole screen and set the position to fixed. Here I got a problem. It is a bit difficult to set height. And I have to use a piece of javascript to make it work properly.

<script>
  $('body').append(
    $('<canvas style="position:fixed;top:0;left:0;z-index:-1;"></canvas>')
   .attr('width', $(window).width()).attr('height', $(window).height())
  );
</script>

I'm going to have to split this article to several parts. Next time I'm going to explain how it works between image and canvas.



UNBLUR BACKGROUND (PART 2)

UNBLUR BACKGROUND (PART 3)

2015年9月23日星期三

CSS: hiding text when it's overflow

Use the following CSS to hide the text. This piece of CSS will hide the exceeded part and replace with some "..." instead.

.nowrap{
  white-space: nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

This will work with both fixed width and percentage width. See the sample below:

The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.

But sometime it doesn't work. I encountered several problems when using it. So I list the problems here just in case somebody got the problem.

  • "text-overflow" will only apply to its PLAIN text child elements.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
  • "text-overflow" will not apply to "table" element.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.

2015年9月21日星期一

Nested Table using AngularJS and Bootstrap

Last time I created a bootstrap like nested table. Now I'm going to show you that how it's going to work with AngularJS. Let's say I got some JSON data from a RESTful service. BTW, AngularJS doesn't really need JQuery. But I simply attached the JQuery and use it whenever I need it.

$scope.data =
[{
  name: 'test name',
  createDate: '2013-05-10T15:04:44.593',
  description: "Don't use data attributes from multiple plugins on the same element.",
  amount: '$20',
  quantity: '2',
  children: [{
    name: 'test name',
    createDate: '2013-05-10T15:04:44.593',
    description: "Don't use data attributes from multiple plugins on the same element.",
    amount: '$20',
    quantity: '2'
  },
  {
    name: 'test name',
    createDate: '2013-05-10T15:04:44.593',
    description: "Don't use data attributes from multiple plugins on the same element.",
    amount: '$20',
    quantity: '2'
  }]
},
{
  name: 'test name',
  createDate: '2013-05-10T15:04:44.593',
  description: "Don't use data attributes from multiple plugins on the same element.",
  amount: '$20',
  quantity: '2',
  children: []
}];

And the angular way of doing this would be basically like this.

<div class="table">
  <div class="header">
    <div class="line">
      <div ng-repeat="(key, value) in data[0]" >{{key}}</div>
    </div>
  </div>
  <div class="body">
    <div class="line" ng-repeat="row in data" >
      <div class="main">
     <div ng-show="row.children && row.children.length" ><button.../div>
        <div ng-repeat="(key, value) in row" >{{value}}</div>
      </div>
      <div class="sub">
        <div class="table">
          ...
        </div>
      </div>
    </div>
  </div>
</div>

There are two major problems about this solution. It needs two definitions for each data in the array and columns to show. For each data, there needs an object to store some status, functions and maybe the data also. For columns, there could also store some definitions such as width, formatter. Fortunately, Angular also developed a lot more advanced solution called Angular UI-Grid that provides some pretty good concepts I can use for my own. So I took a bit of its concepts and implemented them to my template.

The column concept

So in the controller, I created an option object in which I put column definitions. But unlike UI-Grid, I put the data in the scope and passed them separately to the template instead of just one option object. Because the nested table will need to bind with child objects which is different from its parent.

var app = angular.module('demoApp', ['ngRoute']);
app.controller('demoController', ['$scope', '$timeout', function ($scope, $timeout) {
  $scope.options = {
    colDefs: [
      { name: "name", displayName: "Name", width: '15%' },
      { name: "createDate", displayName: "Create Date", width: '15%', cellFilter: "date:'yyyy-MM-dd'" },
      { name: "description", displayName: "Description", width: '35%' },
      { name: "quantity", displayName: "Quantity", width: '10%' },
      { name: "amount", displayName: "Total", width: '10%' }
    ]
  };
  
  $timeout(function(){
    $scope.options.data = [...];
  });
}]);

The table template directives

Then I will need to create four directives for table, row, header and column. Here I'm going to explain a bit about the row directive first. ng-repeat creates a child scope and store the iteration in it. that gives the perfect place to store the row definitions. So in table directive I looped the data array and created a separate object for each element. And then here came a problem. Since I created an array of row objects, ng-repeat will not be able to notice whether there is something change in the data array. So I will need to watch the data manually, and see if there is any data insertion and removal. The header and column directives are relative simple. They just needs to read the column definitions from the option, display the value or name in a proper format.

app.directive('uiTable', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    scope: {
      options: "=uiTable",
      items: "=uiTableModel"
    },
    controller: ["$scope", "$element", "$attrs", function ($scope, $element, $attrs) {
      //template
      var template =  "<div class=\"table\">\n" +
              "  <div class=\"header\">\n" +
              "    <div class=\"line\">\n" +
              "      <div ng-repeat=\"colDef in options.colDefs\" grid-header-render ></div>\n" +
              "    </div>\n" +
              "  </div>\n" +
              "  <div class=\"body\" ng-show=\"rows.length\" >\n" +
              "    <div class=\"line\" ng-class=\"{ 'open': row.isOpened }\" ng-repeat=\"row in rows\" >\n" +
              "      <div class=\"main\" grid-row-render ></div>\n" +
              "      <div class=\"sub\" data-ui-table=\"options\" data-ui-table-model=\"row.entity[options.children]\" ></div>\n" +
              "    </div>\n" +
              "  </div>\n" +
              "   <div ng-hide=\"rows.length\" >No Data</div>\n" +
              "</div>";

      var row = function (entity) {
        this.entity = entity;
      };
      row.prototype.expand = function (e) {
        e.preventDefault();
        this.isOpened = !this.isOpened;
      };

      $scope.rows = [];
      $scope.options.children = $scope.options.children || 'children';
      //watch data
      var deregFunctions = [];
      var dataWatchFunction = function () {
        var newData = $scope.items, newRows = [];
        $.each(newData, function (i, entity) {
          var oldRows = $scope.rows.filter(function (o) { return o.entity == entity; });
          if (oldRows.length) {
            newRows.push(oldRows[0]);
          } else {
            newRows.push(new row(entity));
          }
        });
        $scope.rows = newRows;
      };
      if (!angular.isString($scope.options.data)) {
        deregFunctions.push($scope.$watch(function () { return $scope.items; }, dataWatchFunction));
        deregFunctions.push($scope.$watch(function () { return $scope.items.length; }, dataWatchFunction));
      }

      var $table = $compile(template)($scope);

      if ($scope.options.cssClass) {
        $table.addClass($scope.options.cssClass);
      }

      //Rendering template.
      $element.html('').append($table);
      //destory watch
      $scope.$on('$destroy', function () {
        deregFunctions.forEach(function (deregFn) { deregFn(); });
      });
    }]
  };
}])
.directive('gridRowRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.options.rowTemplate || "<div ng-repeat=\"colDef in options.colDefs\" grid-col-render></div>";
      var $row = $compile(template)($scope);

      if ($scope.options.rowCssClass) {
        $row.addClass($scope.options.rowCssClass);
      }

      $element.append($row);
    }
  };
}])
.directive('gridColRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.colDef.cellTemplate || "<div><span class=\"view\">{{row.entity[colDef.name] CELLFILTER }}</span></div>";
      template = template.replace("CELLFILTER", $scope.colDef.cellFilter ? "| " + $scope.colDef.cellFilter : "");
      var $cell = $compile(template)($scope);

      if ($scope.colDef.cellClass) {
        $cell.addClass($scope.colDef.cellClass);
      }

      if ($scope.colDef.width) {
        $cell.css('width', $scope.colDef.width);
      }

      $element.replaceWith($cell);
    }
  };
}])
.directive('gridHeaderRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.colDef.cellHeaderTemplate || $scope.options.headerTemplate || "<div>{{ colDef.displayName || colDef.name }}</div>";

      var $row = $compile(template)($scope);

      if ($scope.colDef.headerClass) {
        $row.addClass($scope.colDef.cellClass);
      }

      if ($scope.colDef.width) {
        $row.css('width', $scope.colDef.width);
      }
        
      $element.replaceWith($row);
    }
  };
}]);

Sample

2015年9月20日星期日

Visual Studio theme for code prettifier

I put this code in here just in case somebody might want it.

.com       { color: #008000; }
.str, .tag { color: #A31515; }
.kwd, .atv { color: #0000FF; }
.typ       { color: #2B91AF; }
.lit, .atn { color: #FF0000; }
.pun, .pln { color: #000000; }
.dec       { color: #800080; }
.kwd{
  font-style:italic;
}

2015年9月16日星期三

Bootstrap Nested Table

Bootstrap Nested Table

In my recent project, I came across a problem to make bootstrap like table that can fit in for a nested table to display some sub items in each row.The problem for a normal <table> is that it doesn't provide to much flexibility for hierarchy structured data.It will need to create a separate row with a spanned column to store a nested table. So I came up with this <div> solution.

Name
Date
Description
Amount
Quantity
Test Name
2013-05-10T15:04:44.593
Don't use data attributes from multiple plugins on the same element.
$20
2
Name
Date
Description
Amount
Quantity
Test Name
2013-05-10T15:04:44.593
Don't use data attributes from multiple plugins on the same element.
$20
2
Test Name
2013-05-10T15:04:44.593
Don't use data attributes from multiple plugins on the same element.
$20
2
Test Name
2013-05-10T15:04:44.593
Don't use data attributes from multiple plugins on the same element.
$20
2

Table Template

The first thing is to create table like template which has similar table structure. And each line has two containers, "main" container for certain columns and "sub" for nested table.

<div class="table">
  <div class="header">
    <div class="line">
      headers...
    </div>
  </div>
  <div class="body">
    <div class="line">
      <div class="main">
        columns...
      </div>
      <div class="sub">
        <div class="table">
          ...
        </div>
      </div>
    </div>
  </div>
</div>

CSS

The major problem of the <div> table approach is, the column elements cannot auto align and adjust their widths. So a width value must be manually set to each element, which can be a fixed value or precentage value. For fixed value, "overflow:auto" must be set in case of the total width exceed its parent element. For precentage value, it's better to have to total value to be 100%. And also, an indent value needs to be set. It's better but not necessary to be the width of the first column.And then, to make to be consistent with Bootstrap table style and configurable for further table styles. The following styles are needed. I've also add some styles to make it to be fitting to the basic Bootstrap table style.

.header>.line>div:nth-child(1), .body>.line>.main>div:nth-child(1){
  width:5%;
}
.header>.line>div:nth-child(2), .body>.line>.main>div:nth-child(2){
  width:15%;
}
.header>.line>div:nth-child(3), .body>.line>.main>div:nth-child(3){
  width:15%;
}
.header>.line>div:nth-child(4), .body>.line>.main>div:nth-child(4){
  width:45%;
}
.header>.line>div:nth-child(5), .body>.line>.main>div:nth-child(5){
  width:10%;
}
.header>.line>div:nth-child(6), .body>.line>.main>div:nth-child(6){
  width:10%;
}
.sub{
  padding-left:5%;
}
.body>.line a{
  color:#222;
  font-size:12px;
}
.header{
  font-weight:bold;
}
.header>.line{
  border-bottom:solid 2px #AAA;
}
.header>.line>div, .body>.line>.main>div{
  float:left;
  padding:8px;
}
.header>.line:before, .body>.line>.main:before, .header>.line:after, .body>.line>.main:after{
  content:' ';
  display:table;
}
.header>.line:after, .body>.line>.main:after{
  clear:both;
}
.table>.body>.line:not(:first-child)>.main{
  border-top:solid 1px #AAA;
}
.table>.body>.line.open>.main{
  border-bottom:solid 1px #AAA;
}

Javascript

Here it needs a little javascript to expand and unexpand nested tables. In real practice, it will also need to define whether the button shows or hides.

function togglecollapse(e){
  e.preventDefault();
  $(e.currentTarget.parentNode.parentNode.parentNode).toggleClass('open');
}

Next time I will write an article to demonstrate how I implement this template with some MVC approach.

2015年1月2日星期五

HTML Drag and Drop

Here is a basic drag and drop function which works fine on Chrome and Firefox. But without set some data in the dataTransfer object, it doesn't really behaves right.In Firefox it doesn't trigger any of these dragOver, dragEnter or drop events.

drag me
drop here

drag me
drop here

Here is the complete code.

<div draggable="true" style="width:64px;height:64px;border:1px solid #666;background:#acf;margin:0.25em;padding:0.25em;cursor:pointer;float:left;" id="move">drag me</div>
<div style="padding:0.25em;width:15ex;height:15ex;border:1px solid #666;background:#eee;margin:0 0 0 15ex;float:left;" draggable="true" id="target"> drop here</div>
<script>
$(document).ready(function() {
    $('#move')
    
        // Set the element as draggable.
        .attr('draggable', 'true')

        // Handle the start of dragging to initialize.
        .bind('dragstart', function(ev) {
            ev.originalEvent.dataTransfer.setData("target", '#' + ev.target.id);
            return true;
        })

        // Handle the end of dragging.
        .bind('dragend', function(ev) {
            return false;
        });

    $('#target')

        // Highlight on drag entering drop zone.
        .bind('dragenter', function(ev) {
            $(ev.target).addClass('dragover');
            return false;
        })

        // Un-highlight on drag leaving drop zone.
        .bind('dragleave', function(ev) {
            $(ev.target).removeClass('dragover');
            return false;
        })

        // Decide whether the thing dragged in is welcome.
        .bind('dragover', function(ev) {
            return false;
        })

        // Handle the final drop...
        .bind('drop', function(ev) {
            var dt = ev.originalEvent.dataTransfer;
            $(ev.target).append($(dt.getData("target")));
            return false;
        });
});
</script>