├── .bowerrc ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE.md ├── README.md ├── bower.json ├── demo ├── demo.css ├── demo.html ├── demo.js └── giant.json ├── dist ├── json-formatter.css ├── json-formatter.js ├── json-formatter.min.css └── json-formatter.min.js ├── gulpfile.js ├── index.html ├── karma.conf.js ├── package.json ├── screenshot.png ├── src ├── json-formatter.html ├── json-formatter.js ├── json-formatter.less └── recursion-helper.js └── test ├── .jshintrc └── json-formatter.spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | *.log -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "immed": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "trailing": true, 9 | "maxlen": 200, 10 | "boss": true, 11 | "eqnull": true, 12 | "expr": true, 13 | "globalstrict": true, 14 | "laxbreak": true, 15 | "loopfunc": true, 16 | "sub": true, 17 | "undef": true, 18 | "indent": 2, 19 | "globals": { 20 | "angular": true, 21 | "window": true, 22 | "module": true 23 | } 24 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | 6 | before_script: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | - npm install -g gulp bower 10 | - bower install 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2014 Mohsen Azimi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Formatter 2 | [![Build Status](https://travis-ci.org/mohsen1/json-formatter.svg?branch=master)](https://travis-ci.org/mohsen1/json-formatter) 3 | [![Code Climate](https://codeclimate.com/github/mohsen1/json-formatter/badges/gpa.svg)](https://codeclimate.com/github/mohsen1/json-formatter) 4 | 5 | JSON Formatter is an AngularJS directive for rendering JSON objects in HTML with a **collapsible** navigation. 6 | 7 | [![Screebshot](./screenshot.png)](http://azimi.me/json-formatter/demo/demo.html) 8 | 9 | #### [Now also available in pure JavaScript with zero dependency!](https://github.com/mohsen1/json-formatter-js) 10 | 11 | ## Usage 12 | 13 | * Install via Bower or npm 14 | 15 | ```bash 16 | bower install json-formatter --save 17 | ``` 18 | ...or 19 | 20 | ```bash 21 | npm install jsonformatter --save 22 | ``` 23 | * Add `jsonFormatter` to your app dependencies 24 | 25 | ```js 26 | angular.module('MyApp', ['jsonFormatter']) 27 | ``` 28 | * Use `` directive 29 | 30 | ```html 31 | 32 | ``` 33 | * `open` attribute accepts a number which indicates how many levels rendered JSON should be opened 34 | 35 | #### Configuration 36 | 37 | You can use `JSONFormatterConfig` provider to configure JOSN Formatter. 38 | 39 | Available configurations 40 | 41 | ##### Hover Preview 42 | * `hoverPreviewEnabled`: enable preview on hover 43 | * `hoverPreviewArrayCount`: number of array items to show in preview Any array larger than this number will be shown as `Array[XXX]` where `XXX` is length of the array. 44 | * `hoverPreviewFieldCount`: number of object properties to show for object preview. Any object with more properties that thin number will be truncated. 45 | 46 | Example using configuration 47 | 48 | ```js 49 | app.config(function (JSONFormatterConfigProvider) { 50 | 51 | // Enable the hover preview feature 52 | JSONFormatterConfigProvider.hoverPreviewEnabled = true; 53 | }); 54 | ``` 55 | 56 | ## Demo 57 | See [Examples here](http://azimi.me/json-formatter/demo/demo.html) 58 | 59 | 60 | ## Known Bugs 61 | ##### `hashKey` 62 | 63 | If you are iterating in an array of objects using `ng-repeat`, make sure you are using `track by $index` to avoid adding extra `$$hashKey` to your objects. 64 | 65 | ## Browser Support 66 | All modern browsers are supported. Lowest supported version of Internet Explorer is **IE9**. 67 | 68 | ## License 69 | 70 | Apache 2.0 71 | 72 | See [LICENSE](./LICENSE) 73 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-formatter", 3 | "version": "0.6.0", 4 | "authors": "Mohsen Azimi ", 5 | "description": "JSON Formatter is an AngularJS directive for rendering JSON objects in HTML with a **collapsible** navigation.", 6 | "main": [ 7 | "dist/json-formatter.js", 8 | "dist/json-formatter.css" 9 | ], 10 | "license": "Apache 2.0", 11 | "keywords": [ 12 | "json", 13 | "formatter", 14 | "collapsible", 15 | "json to HTML", 16 | "JSON" 17 | ], 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ], 25 | "dependencies": { 26 | "angular": "^1.4.4" 27 | }, 28 | "devDependencies": { 29 | "angular": "^1.4.4", 30 | "angular-mocks": "^1.4.4", 31 | "angular-sanitize": "^1.4.4", 32 | "bootstrap": "~3.3.5", 33 | "jquery": "~2.1.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 730px; 3 | } 4 | 5 | h1, h2, h3 { 6 | font-family: sans-serif; 7 | } 8 | 9 | h2 { 10 | margin: .5em 0 .2em; 11 | padding: 0; 12 | } 13 | 14 | textarea { 15 | height: 6em; 16 | width: 100%; 17 | display: block; 18 | -webkit-appearance: none; 19 | padding: .3em; 20 | font-family: monospace; 21 | } 22 | 23 | .group { 24 | padding: 1em; 25 | } 26 | .result { 27 | padding: 1em; 28 | border: 1px solid #eee; 29 | margin-bottom: 2em; 30 | overflow: auto; 31 | } 32 | 33 | 34 | .dark { 35 | background: #333; 36 | } 37 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularJS directive demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 19 |

<json-formatter>

20 |

21 | 23 | 25 |

26 |
27 | 28 |
29 |

About

30 |

31 | JSON Formatter is an AngularJS directive for rendering JSON objects in HTML with a collapsible navigation. 32 |

33 |

How to use

34 |

Install via Bower

35 |

bower install json-formatter --save

36 |

Add jsonFormatter to your app dependencies

37 |

38 |

angular.module('myApp', ['jsonFormatter'])
39 |

40 |

Use <json-formatter> directive

41 |

42 |

<json-formatter open="1" json="{my: 'json'}"></json-formatter>
43 |

44 |

open attribute accepts a number that indicated how many levels JSON should be open

45 | 46 |

Configuration

47 |

You can set following configurations via JSONFormatterConfig provider.

48 |

Changing these configurations will impact all demos

49 |
    50 |
  • 51 | 55 |
  • 56 |
  • 57 | 61 |
  • 62 |
  • 63 | 67 |
  • 68 |
69 | 70 |

Demo

71 |
72 |
73 | 74 |
75 |
76 | 77 |

Live JSON

78 |
79 | 80 |
81 | 82 |
83 |
84 | 85 |

Other Examples

86 |
87 |
88 | 89 |

Null

90 |
91 | 92 |
93 | 94 |

Undefined

95 |
96 | 97 |
98 | 99 |

Number

100 |
101 | 102 |
103 | 104 |

String

105 |
106 | 107 |
108 | 109 |

Date String

110 |
111 | 112 |
113 | 114 |

URL String

115 |
116 | 117 |
118 | 119 |

Function

120 |
121 | 122 |
123 | 124 |

Empty object

125 |
126 | 127 |
128 | 129 |

Empty array

130 |
131 | 132 |
133 | 134 |

Object with one key

135 |
136 | 137 |
138 | 139 |

Nested Object

140 |
141 | 142 |
143 | 144 |

Array

145 |
146 | 147 |
148 | 149 |

Complex Object

150 |
151 | 152 |
153 | 154 |

Object with long key

155 |
156 | 160 |
161 | 162 |

Object with empty key

163 |
164 | 165 |
166 | 167 |

Dates

168 |
169 | 174 |
175 | 176 |

Long String content

177 |
178 | 181 |
182 | 183 |

HTML content

184 |
185 | 186 |
187 | 188 |

Opened 1 level

189 |
190 | 191 |
192 | 193 |

Opened 2 level

194 |
195 | 196 |
197 | 198 |

Opened 3 level

199 |
200 | 201 |
202 | 203 |

Opened 4 level

204 |
205 | 206 |
207 | 208 |

Custom constructors

209 |
210 | 211 |
212 | 213 |

ng-repeat

214 |

If you are iterating in an array of objects, make sure you are using track by $index to avoid adding extra $$hashKey to your objects.

215 |
With track by $index
216 |
217 | 218 |
219 |
Without track by $index
220 |
221 | 222 |
223 | 224 |

Dark theme

225 |

To use dark theme add json-formatter-dark class this directive. Note that json-formatter-dark class make text and other elements lighter so they look good on a dark background.

226 |
227 | 228 |
229 | 230 |

Altering JSON object

231 | 235 |
236 | 237 |
238 | 239 |

Giant JSON

240 |

(for performance testing)

241 | 244 |
245 | 246 |
247 |
248 |
249 |
250 | 253 | 254 |
255 | 256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('demo', ['ngSanitize', 'jsonFormatter']); 2 | 3 | app.controller('MainCtrl', function ($scope, $http, JSONFormatterConfig) { 4 | 5 | $scope.hoverPreviewEnabled = JSONFormatterConfig.hoverPreviewEnabled; 6 | $scope.hoverPreviewArrayCount = JSONFormatterConfig.hoverPreviewArrayCount; 7 | $scope.hoverPreviewFieldCount = JSONFormatterConfig.hoverPreviewFieldCount; 8 | 9 | $scope.$watch('hoverPreviewEnabled', function(newValue){ 10 | JSONFormatterConfig.hoverPreviewEnabled = newValue; 11 | }); 12 | $scope.$watch('hoverPreviewArrayCount', function(newValue){ 13 | JSONFormatterConfig.hoverPreviewArrayCount = newValue; 14 | }); 15 | $scope.$watch('hoverPreviewFieldCount', function(newValue){ 16 | JSONFormatterConfig.hoverPreviewFieldCount = newValue; 17 | }); 18 | 19 | $scope.undef = undefined; 20 | $scope.textarea = '{}'; 21 | $scope.complex = { 22 | numbers: [ 23 | 1, 24 | 2, 25 | 3 26 | ], 27 | boolean: true, 28 | 'null': null, 29 | number: 123, 30 | anObject: { 31 | a: 'b', 32 | c: 'd', 33 | e: 'f\"' 34 | }, 35 | string: 'Hello World', 36 | url: 'https://github.com/mohsen1/json-formatter', 37 | date: 'Sun Aug 03 2014 20:46:55 GMT-0700 (PDT)', 38 | func: function add(a,b){return a + b; } 39 | }; 40 | 41 | $scope.randArray1 = [null, null, null].map(function(r) { 42 | return {value: Math.random()}; 43 | }); 44 | 45 | $scope.randArray2 = [null, null, null].map(function(r) { 46 | return {value: Math.random()}; 47 | }); 48 | 49 | $scope.deep = {a:{b:{c:{d:{}}}}}; 50 | 51 | $scope.fn = function fn(arg1, /*arg*/arg2) { 52 | return arg1 + arg2; 53 | }; 54 | 55 | $scope.alternate1 = {o: 1, d: 'Alternate 1', b: []}; 56 | $scope.alternate2 = [1, 'Alternate 2', {b: {}}]; 57 | 58 | $scope.fetchGiantJSON = function() { 59 | $scope.giant = 'Fetching...'; 60 | $http.get('giant.json').then(function (json) { 61 | $scope.giant = json; 62 | }); 63 | } 64 | 65 | function Person(name){ this.name = name; } 66 | $scope.person = new Person('Mohsen'); 67 | 68 | $scope.$watch('textarea', function (str){ 69 | var result = {}; 70 | 71 | try { 72 | $scope.textareaJson = JSON.parse(str); 73 | } catch (e) {} 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /dist/json-formatter.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * jsonformatter 3 | * 4 | * Version: 0.6.0 - 2016-08-27T12:58:03.339Z 5 | * License: Apache-2.0 6 | */ 7 | 8 | 9 | .json-formatter-row { 10 | font-family: monospace; 11 | } 12 | .json-formatter-row, 13 | .json-formatter-row a, 14 | .json-formatter-row a:hover { 15 | color: black; 16 | text-decoration: none; 17 | } 18 | .json-formatter-row .json-formatter-row { 19 | margin-left: 1em; 20 | } 21 | .json-formatter-row .children.empty { 22 | opacity: 0.5; 23 | margin-left: 1em; 24 | } 25 | .json-formatter-row .children.empty.object:after { 26 | content: "No properties"; 27 | } 28 | .json-formatter-row .children.empty.array:after { 29 | content: "[]"; 30 | } 31 | .json-formatter-row .string { 32 | color: green; 33 | white-space: pre; 34 | word-wrap: break-word; 35 | } 36 | .json-formatter-row .number { 37 | color: blue; 38 | } 39 | .json-formatter-row .boolean { 40 | color: red; 41 | } 42 | .json-formatter-row .null { 43 | color: #855A00; 44 | } 45 | .json-formatter-row .undefined { 46 | color: #ca0b69; 47 | } 48 | .json-formatter-row .function { 49 | color: #FF20ED; 50 | } 51 | .json-formatter-row .date { 52 | background-color: rgba(0, 0, 0, 0.05); 53 | } 54 | .json-formatter-row .url { 55 | text-decoration: underline; 56 | color: blue; 57 | cursor: pointer; 58 | } 59 | .json-formatter-row .bracket { 60 | color: blue; 61 | } 62 | .json-formatter-row .key { 63 | color: #00008B; 64 | cursor: pointer; 65 | } 66 | .json-formatter-row .constructor-name { 67 | cursor: pointer; 68 | } 69 | .json-formatter-row .toggler { 70 | font-size: 0.8em; 71 | line-height: 1.2em; 72 | vertical-align: middle; 73 | opacity: 0.6; 74 | cursor: pointer; 75 | } 76 | .json-formatter-row .toggler:after { 77 | display: inline-block; 78 | transition: transform 100ms ease-in; 79 | content: "►"; 80 | } 81 | .json-formatter-row .toggler.open:after { 82 | transform: rotate(90deg); 83 | } 84 | .json-formatter-row > a > .thumbnail-text { 85 | opacity: 0; 86 | transition: opacity 0.15s ease-in; 87 | font-style: italic; 88 | } 89 | .json-formatter-row:hover > a > .thumbnail-text { 90 | opacity: 0.6; 91 | } 92 | .json-formatter-dark.json-formatter-row { 93 | font-family: monospace; 94 | } 95 | .json-formatter-dark.json-formatter-row, 96 | .json-formatter-dark.json-formatter-row a, 97 | .json-formatter-dark.json-formatter-row a:hover { 98 | color: white; 99 | text-decoration: none; 100 | } 101 | .json-formatter-dark.json-formatter-row .json-formatter-row { 102 | margin-left: 1em; 103 | } 104 | .json-formatter-dark.json-formatter-row .children.empty { 105 | opacity: 0.5; 106 | margin-left: 1em; 107 | } 108 | .json-formatter-dark.json-formatter-row .children.empty.object:after { 109 | content: "No properties"; 110 | } 111 | .json-formatter-dark.json-formatter-row .children.empty.array:after { 112 | content: "[]"; 113 | } 114 | .json-formatter-dark.json-formatter-row .string { 115 | color: #31F031; 116 | white-space: pre; 117 | word-wrap: break-word; 118 | } 119 | .json-formatter-dark.json-formatter-row .number { 120 | color: #66C2FF; 121 | } 122 | .json-formatter-dark.json-formatter-row .boolean { 123 | color: #EC4242; 124 | } 125 | .json-formatter-dark.json-formatter-row .null { 126 | color: #EEC97D; 127 | } 128 | .json-formatter-dark.json-formatter-row .undefined { 129 | color: #ef8fbe; 130 | } 131 | .json-formatter-dark.json-formatter-row .function { 132 | color: #FD48CB; 133 | } 134 | .json-formatter-dark.json-formatter-row .date { 135 | background-color: rgba(255, 255, 255, 0.05); 136 | } 137 | .json-formatter-dark.json-formatter-row .url { 138 | text-decoration: underline; 139 | color: #027BFF; 140 | cursor: pointer; 141 | } 142 | .json-formatter-dark.json-formatter-row .bracket { 143 | color: #9494FF; 144 | } 145 | .json-formatter-dark.json-formatter-row .key { 146 | color: #23A0DB; 147 | cursor: pointer; 148 | } 149 | .json-formatter-dark.json-formatter-row .constructor-name { 150 | cursor: pointer; 151 | } 152 | .json-formatter-dark.json-formatter-row .toggler { 153 | font-size: 0.8em; 154 | line-height: 1.2em; 155 | vertical-align: middle; 156 | opacity: 0.6; 157 | cursor: pointer; 158 | } 159 | .json-formatter-dark.json-formatter-row .toggler:after { 160 | display: inline-block; 161 | transition: transform 100ms ease-in; 162 | content: "►"; 163 | } 164 | .json-formatter-dark.json-formatter-row .toggler.open:after { 165 | transform: rotate(90deg); 166 | } 167 | .json-formatter-dark.json-formatter-row > a > .thumbnail-text { 168 | opacity: 0; 169 | transition: opacity 0.15s ease-in; 170 | font-style: italic; 171 | } 172 | .json-formatter-dark.json-formatter-row:hover > a > .thumbnail-text { 173 | opacity: 0.6; 174 | } 175 | -------------------------------------------------------------------------------- /dist/json-formatter.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jsonformatter 3 | * 4 | * Version: 0.6.0 - 2016-08-27T12:58:03.306Z 5 | * License: Apache-2.0 6 | */ 7 | 8 | 9 | 'use strict'; 10 | 11 | angular.module('jsonFormatter', ['RecursionHelper']) 12 | 13 | .provider('JSONFormatterConfig', function JSONFormatterConfigProvider() { 14 | 15 | // Default values for hover preview config 16 | var hoverPreviewEnabled = false; 17 | var hoverPreviewArrayCount = 100; 18 | var hoverPreviewFieldCount = 5; 19 | 20 | return { 21 | get hoverPreviewEnabled() { 22 | return hoverPreviewEnabled; 23 | }, 24 | set hoverPreviewEnabled(value) { 25 | hoverPreviewEnabled = !!value; 26 | }, 27 | 28 | get hoverPreviewArrayCount() { 29 | return hoverPreviewArrayCount; 30 | }, 31 | set hoverPreviewArrayCount(value) { 32 | hoverPreviewArrayCount = parseInt(value, 10); 33 | }, 34 | 35 | get hoverPreviewFieldCount() { 36 | return hoverPreviewFieldCount; 37 | }, 38 | set hoverPreviewFieldCount(value) { 39 | hoverPreviewFieldCount = parseInt(value, 10); 40 | }, 41 | 42 | $get: function () { 43 | return { 44 | hoverPreviewEnabled: hoverPreviewEnabled, 45 | hoverPreviewArrayCount: hoverPreviewArrayCount, 46 | hoverPreviewFieldCount: hoverPreviewFieldCount 47 | }; 48 | } 49 | }; 50 | }) 51 | 52 | .directive('jsonFormatter', ['RecursionHelper', 'JSONFormatterConfig', function jsonFormatterDirective(RecursionHelper, JSONFormatterConfig) { 53 | function escapeString(str) { 54 | return str.replace('"', '\"'); 55 | } 56 | 57 | // From http://stackoverflow.com/a/332429 58 | function getObjectName(object) { 59 | if (object === undefined) { 60 | return ''; 61 | } 62 | if (object === null) { 63 | return 'Object'; 64 | } 65 | if (typeof object === 'object' && !object.constructor) { 66 | return 'Object'; 67 | } 68 | 69 | //ES6 default gives name to constructor 70 | if (object.__proto__ !== undefined && object.__proto__.constructor !== undefined && object.__proto__.constructor.name !== undefined) { 71 | return object.__proto__.constructor.name; 72 | } 73 | 74 | var funcNameRegex = /function (.{1,})\(/; 75 | var results = (funcNameRegex).exec((object).constructor.toString()); 76 | if (results && results.length > 1) { 77 | return results[1]; 78 | } else { 79 | return ''; 80 | } 81 | } 82 | 83 | function getType(object) { 84 | if (object === null) { return 'null'; } 85 | return typeof object; 86 | } 87 | 88 | function getValuePreview (object, value) { 89 | var type = getType(object); 90 | 91 | if (type === 'null' || type === 'undefined') { return type; } 92 | 93 | if (type === 'string') { 94 | value = '"' + escapeString(value) + '"'; 95 | } 96 | if (type === 'function'){ 97 | 98 | // Remove content of the function 99 | return object.toString() 100 | .replace(/[\r\n]/g, '') 101 | .replace(/\{.*\}/, '') + '{…}'; 102 | 103 | } 104 | return value; 105 | } 106 | 107 | function getPreview(object) { 108 | var value = ''; 109 | if (angular.isObject(object)) { 110 | value = getObjectName(object); 111 | if (angular.isArray(object)) 112 | value += '[' + object.length + ']'; 113 | } else { 114 | value = getValuePreview(object, object); 115 | } 116 | return value; 117 | } 118 | 119 | function link(scope) { 120 | scope.isArray = function () { 121 | return angular.isArray(scope.json); 122 | }; 123 | 124 | scope.isObject = function() { 125 | return angular.isObject(scope.json); 126 | }; 127 | 128 | scope.getKeys = function (){ 129 | if (scope.isObject()) { 130 | return Object.keys(scope.json).map(function(key) { 131 | if (key === '') { return '""'; } 132 | return key; 133 | }); 134 | } 135 | }; 136 | scope.type = getType(scope.json); 137 | scope.hasKey = typeof scope.key !== 'undefined'; 138 | scope.getConstructorName = function(){ 139 | return getObjectName(scope.json); 140 | }; 141 | 142 | if (scope.type === 'string'){ 143 | 144 | // Add custom type for date 145 | if((new Date(scope.json)).toString() !== 'Invalid Date') { 146 | scope.isDate = true; 147 | } 148 | 149 | // Add custom type for URLs 150 | if (scope.json.indexOf('http') === 0) { 151 | scope.isUrl = true; 152 | } 153 | } 154 | 155 | scope.isEmptyObject = function () { 156 | return scope.getKeys() && !scope.getKeys().length && 157 | scope.isOpen && !scope.isArray(); 158 | }; 159 | 160 | 161 | // If 'open' attribute is present 162 | scope.isOpen = !!scope.open; 163 | scope.toggleOpen = function () { 164 | scope.isOpen = !scope.isOpen; 165 | }; 166 | scope.childrenOpen = function () { 167 | if (scope.open > 1){ 168 | return scope.open - 1; 169 | } 170 | return 0; 171 | }; 172 | 173 | scope.openLink = function (isUrl) { 174 | if(isUrl) { 175 | window.location.href = scope.json; 176 | } 177 | }; 178 | 179 | scope.parseValue = function (value){ 180 | return getValuePreview(scope.json, value); 181 | }; 182 | 183 | scope.showThumbnail = function () { 184 | return !!JSONFormatterConfig.hoverPreviewEnabled && scope.isObject() && !scope.isOpen; 185 | }; 186 | 187 | scope.getThumbnail = function () { 188 | if (scope.isArray()) { 189 | 190 | // if array length is greater then 100 it shows "Array[101]" 191 | if (scope.json.length > JSONFormatterConfig.hoverPreviewArrayCount) { 192 | return 'Array[' + scope.json.length + ']'; 193 | } else { 194 | return '[' + scope.json.map(getPreview).join(', ') + ']'; 195 | } 196 | } else { 197 | 198 | var keys = scope.getKeys(); 199 | 200 | // the first five keys (like Chrome Developer Tool) 201 | var narrowKeys = keys.slice(0, JSONFormatterConfig.hoverPreviewFieldCount); 202 | 203 | // json value schematic information 204 | var kvs = narrowKeys 205 | .map(function (key) { return key + ':' + getPreview(scope.json[key]); }); 206 | 207 | // if keys count greater then 5 then show ellipsis 208 | var ellipsis = keys.length >= 5 ? '…' : ''; 209 | 210 | return '{' + kvs.join(', ') + ellipsis + '}'; 211 | } 212 | }; 213 | } 214 | 215 | return { 216 | templateUrl: 'json-formatter.html', 217 | restrict: 'E', 218 | replace: true, 219 | scope: { 220 | json: '=', 221 | key: '=', 222 | open: '=' 223 | }, 224 | compile: function(element) { 225 | 226 | // Use the compile function from the RecursionHelper, 227 | // And return the linking function(s) which it returns 228 | return RecursionHelper.compile(element, link); 229 | } 230 | }; 231 | }]); 232 | 233 | // Export to CommonJS style imports. Exporting this string makes this valid: 234 | // angular.module('myApp', [require('jsonformatter')]); 235 | if (typeof module === 'object') { 236 | module.exports = 'jsonFormatter'; 237 | } 238 | 'use strict'; 239 | 240 | // from http://stackoverflow.com/a/18609594 241 | angular.module('RecursionHelper', []).factory('RecursionHelper', ['$compile', function($compile){ 242 | return { 243 | /** 244 | * Manually compiles the element, fixing the recursion loop. 245 | * @param element 246 | * @param [link] A post-link function, or an object with function(s) 247 | * registered via pre and post properties. 248 | * @returns An object containing the linking functions. 249 | */ 250 | compile: function(element, link){ 251 | // Normalize the link parameter 252 | if(angular.isFunction(link)){ 253 | link = { post: link }; 254 | } 255 | 256 | // Break the recursion loop by removing the contents 257 | var contents = element.contents().remove(); 258 | var compiledContents; 259 | return { 260 | pre: (link && link.pre) ? link.pre : null, 261 | /** 262 | * Compiles and re-adds the contents 263 | */ 264 | post: function(scope, element){ 265 | // Compile the contents 266 | if(!compiledContents){ 267 | compiledContents = $compile(contents); 268 | } 269 | // Re-add the compiled contents to the element 270 | compiledContents(scope, function(clone){ 271 | element.append(clone); 272 | }); 273 | 274 | // Call the post-linking function, if any 275 | if(link && link.post){ 276 | link.post.apply(null, arguments); 277 | } 278 | } 279 | }; 280 | } 281 | }; 282 | }]); 283 | 284 | angular.module("jsonFormatter").run(["$templateCache", function($templateCache) {$templateCache.put("json-formatter.html","
0\" class=\"json-formatter-row\"> {{key}}: {{getConstructorName(json)}} [{{json.length}}] {{parseValue(json)}} {{getThumbnail()}}
");}]); -------------------------------------------------------------------------------- /dist/json-formatter.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * jsonformatter 3 | * 4 | * Version: 0.6.0 - 2016-08-27T12:58:03.339Z 5 | * License: Apache-2.0 6 | */.json-formatter-dark.json-formatter-row,.json-formatter-row{font-family:monospace}.json-formatter-dark.json-formatter-row .toggler.open:after,.json-formatter-row .toggler.open:after{transform:rotate(90deg)}.json-formatter-row,.json-formatter-row a,.json-formatter-row a:hover{color:#000;text-decoration:none}.json-formatter-row .json-formatter-row{margin-left:1em}.json-formatter-row .children.empty{opacity:.5;margin-left:1em}.json-formatter-row .children.empty.object:after{content:"No properties"}.json-formatter-row .children.empty.array:after{content:"[]"}.json-formatter-row .string{color:green;white-space:pre;word-wrap:break-word}.json-formatter-row .number{color:#00f}.json-formatter-row .boolean{color:red}.json-formatter-row .null{color:#855A00}.json-formatter-row .undefined{color:#ca0b69}.json-formatter-row .function{color:#FF20ED}.json-formatter-row .date{background-color:rgba(0,0,0,.05)}.json-formatter-row .url{text-decoration:underline;color:#00f;cursor:pointer}.json-formatter-row .bracket{color:#00f}.json-formatter-row .key{color:#00008B;cursor:pointer}.json-formatter-row .constructor-name{cursor:pointer}.json-formatter-row .toggler{font-size:.8em;line-height:1.2em;vertical-align:middle;opacity:.6;cursor:pointer}.json-formatter-row .toggler:after{display:inline-block;transition:transform .1s ease-in;content:"►"}.json-formatter-row>a>.thumbnail-text{opacity:0;transition:opacity .15s ease-in;font-style:italic}.json-formatter-row:hover>a>.thumbnail-text{opacity:.6}.json-formatter-dark.json-formatter-row,.json-formatter-dark.json-formatter-row a,.json-formatter-dark.json-formatter-row a:hover{color:#fff;text-decoration:none}.json-formatter-dark.json-formatter-row .json-formatter-row{margin-left:1em}.json-formatter-dark.json-formatter-row .children.empty{opacity:.5;margin-left:1em}.json-formatter-dark.json-formatter-row .children.empty.object:after{content:"No properties"}.json-formatter-dark.json-formatter-row .children.empty.array:after{content:"[]"}.json-formatter-dark.json-formatter-row .string{color:#31F031;white-space:pre;word-wrap:break-word}.json-formatter-dark.json-formatter-row .number{color:#66C2FF}.json-formatter-dark.json-formatter-row .boolean{color:#EC4242}.json-formatter-dark.json-formatter-row .null{color:#EEC97D}.json-formatter-dark.json-formatter-row .undefined{color:#ef8fbe}.json-formatter-dark.json-formatter-row .function{color:#FD48CB}.json-formatter-dark.json-formatter-row .date{background-color:rgba(255,255,255,.05)}.json-formatter-dark.json-formatter-row .url{text-decoration:underline;color:#027BFF;cursor:pointer}.json-formatter-dark.json-formatter-row .bracket{color:#9494FF}.json-formatter-dark.json-formatter-row .key{color:#23A0DB;cursor:pointer}.json-formatter-dark.json-formatter-row .constructor-name{cursor:pointer}.json-formatter-dark.json-formatter-row .toggler{font-size:.8em;line-height:1.2em;vertical-align:middle;opacity:.6;cursor:pointer}.json-formatter-dark.json-formatter-row .toggler:after{display:inline-block;transition:transform .1s ease-in;content:"►"}.json-formatter-dark.json-formatter-row>a>.thumbnail-text{opacity:0;transition:opacity .15s ease-in;font-style:italic}.json-formatter-dark.json-formatter-row:hover>a>.thumbnail-text{opacity:.6} -------------------------------------------------------------------------------- /dist/json-formatter.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jsonformatter 3 | * 4 | * Version: 0.6.0 - 2016-08-27T12:58:03.306Z 5 | * License: Apache-2.0 6 | */ 7 | "use strict";angular.module("jsonFormatter",["RecursionHelper"]).provider("JSONFormatterConfig",function(){var n=!1,e=100,t=5;return{get hoverPreviewEnabled(){return n},set hoverPreviewEnabled(e){n=!!e},get hoverPreviewArrayCount(){return e},set hoverPreviewArrayCount(n){e=parseInt(n,10)},get hoverPreviewFieldCount(){return t},set hoverPreviewFieldCount(n){t=parseInt(n,10)},$get:function(){return{hoverPreviewEnabled:n,hoverPreviewArrayCount:e,hoverPreviewFieldCount:t}}}}).directive("jsonFormatter",["RecursionHelper","JSONFormatterConfig",function(n,e){function t(n){return n.replace('"','"')}function r(n){if(void 0===n)return"";if(null===n)return"Object";if("object"==typeof n&&!n.constructor)return"Object";if(void 0!==n.__proto__&&void 0!==n.__proto__.constructor&&void 0!==n.__proto__.constructor.name)return n.__proto__.constructor.name;var e=/function (.{1,})\(/,t=e.exec(n.constructor.toString());return t&&t.length>1?t[1]:""}function o(n){return null===n?"null":typeof n}function s(n,e){var r=o(n);return"null"===r||"undefined"===r?r:("string"===r&&(e='"'+t(e)+'"'),"function"===r?n.toString().replace(/[\r\n]/g,"").replace(/\{.*\}/,"")+"{…}":e)}function i(n){var e="";return angular.isObject(n)?(e=r(n),angular.isArray(n)&&(e+="["+n.length+"]")):e=s(n,n),e}function a(n){n.isArray=function(){return angular.isArray(n.json)},n.isObject=function(){return angular.isObject(n.json)},n.getKeys=function(){if(n.isObject())return Object.keys(n.json).map(function(n){return""===n?'""':n})},n.type=o(n.json),n.hasKey="undefined"!=typeof n.key,n.getConstructorName=function(){return r(n.json)},"string"===n.type&&("Invalid Date"!==new Date(n.json).toString()&&(n.isDate=!0),0===n.json.indexOf("http")&&(n.isUrl=!0)),n.isEmptyObject=function(){return n.getKeys()&&!n.getKeys().length&&n.isOpen&&!n.isArray()},n.isOpen=!!n.open,n.toggleOpen=function(){n.isOpen=!n.isOpen},n.childrenOpen=function(){return n.open>1?n.open-1:0},n.openLink=function(e){e&&(window.location.href=n.json)},n.parseValue=function(e){return s(n.json,e)},n.showThumbnail=function(){return!!e.hoverPreviewEnabled&&n.isObject()&&!n.isOpen},n.getThumbnail=function(){if(n.isArray())return n.json.length>e.hoverPreviewArrayCount?"Array["+n.json.length+"]":"["+n.json.map(i).join(", ")+"]";var t=n.getKeys(),r=t.slice(0,e.hoverPreviewFieldCount),o=r.map(function(e){return e+":"+i(n.json[e])}),s=t.length>=5?"…":"";return"{"+o.join(", ")+s+"}"}}return{templateUrl:"json-formatter.html",restrict:"E",replace:!0,scope:{json:"=",key:"=",open:"="},compile:function(e){return n.compile(e,a)}}}]),"object"==typeof module&&(module.exports="jsonFormatter"),angular.module("RecursionHelper",[]).factory("RecursionHelper",["$compile",function(n){return{compile:function(e,t){angular.isFunction(t)&&(t={post:t});var r,o=e.contents().remove();return{pre:t&&t.pre?t.pre:null,post:function(e,s){r||(r=n(o)),r(e,function(n){s.append(n)}),t&&t.post&&t.post.apply(null,arguments)}}}}}]),angular.module("jsonFormatter").run(["$templateCache",function(n){n.put("json-formatter.html",'')}]); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var connect = require('gulp-connect'); 3 | var gulp = require('gulp'); 4 | var KarmaServer = require('karma').Server; 5 | var concat = require('gulp-concat'); 6 | var jshint = require('gulp-jshint'); 7 | var header = require('gulp-header'); 8 | var rename = require('gulp-rename'); 9 | var es = require('event-stream'); 10 | var del = require('del'); 11 | var uglify = require('gulp-uglify'); 12 | var minifyHtml = require('gulp-minify-html'); 13 | var minifyCSS = require('gulp-minify-css'); 14 | var templateCache = require('gulp-angular-templatecache'); 15 | var gutil = require('gulp-util'); 16 | var plumber = require('gulp-plumber'); 17 | var open = require('gulp-open'); 18 | var less = require('gulp-less'); 19 | var order = require("gulp-order"); 20 | var runSequence = require('run-sequence'); 21 | 22 | 23 | var config = { 24 | pkg : JSON.parse(fs.readFileSync('./package.json')), 25 | banner: 26 | '/*!\n' + 27 | ' * <%= pkg.name %>\n' + 28 | ' * <%= pkg.homepage %>\n' + 29 | ' * Version: <%= pkg.version %> - <%= timestamp %>\n' + 30 | ' * License: <%= pkg.license %>\n' + 31 | ' */\n\n\n' 32 | }; 33 | 34 | gulp.task('connect', function() { 35 | connect.server({ 36 | root: '.', 37 | livereload: true 38 | }); 39 | }); 40 | 41 | gulp.task('html', function () { 42 | gulp.src(['./demo/*.html', '.src/*.html']) 43 | .pipe(connect.reload()); 44 | }); 45 | 46 | gulp.task('watch', function () { 47 | gulp.watch(['./demo/**/*.html'], ['html']); 48 | gulp.watch(['./**/*.less'], ['styles']); 49 | gulp.watch(['./**/*.js', './**/*.html'], ['scripts']); 50 | }); 51 | 52 | gulp.task('clean', function(cb) { 53 | del(['dist'], cb); 54 | }); 55 | 56 | gulp.task('scripts', function() { 57 | 58 | function buildTemplates() { 59 | return gulp.src('src/**/*.html') 60 | .pipe(minifyHtml({ 61 | empty: true, 62 | spare: true, 63 | quotes: true 64 | })) 65 | .pipe(templateCache({module: 'jsonFormatter'})); 66 | }; 67 | 68 | function buildDistJS(){ 69 | return gulp.src([ 70 | 'src/json-formatter.js', 71 | 'src/recursion-helper.js' 72 | ]) 73 | .pipe(concat('json-formatter.js')) 74 | .pipe(plumber({ 75 | errorHandler: handleError 76 | })) 77 | .pipe(jshint()) 78 | .pipe(jshint.reporter('jshint-stylish')) 79 | .pipe(jshint.reporter('fail')); 80 | }; 81 | 82 | es.merge(buildDistJS(), buildTemplates()) 83 | .pipe(plumber({ 84 | errorHandler: handleError 85 | })) 86 | .pipe(order([ 87 | 'json-formatter.js', 88 | 'json-formatter.html' 89 | ])) 90 | .pipe(concat('json-formatter.js')) 91 | .pipe(header(config.banner, { 92 | timestamp: (new Date()).toISOString(), pkg: config.pkg 93 | })) 94 | .pipe(gulp.dest('dist')) 95 | .pipe(rename({suffix: '.min'})) 96 | .pipe(uglify({ 97 | preserveComments: 'some', 98 | reservedNames: 'Person' 99 | })) 100 | .pipe(gulp.dest('./dist')) 101 | .pipe(connect.reload()); 102 | }); 103 | 104 | 105 | gulp.task('styles', function() { 106 | 107 | return gulp.src('src/json-formatter.less') 108 | .pipe(less()) 109 | .pipe(header(config.banner, { 110 | timestamp: (new Date()).toISOString(), pkg: config.pkg 111 | })) 112 | .pipe(gulp.dest('dist')) 113 | .pipe(minifyCSS()) 114 | .pipe(rename({suffix: '.min'})) 115 | .pipe(gulp.dest('dist')) 116 | .pipe(connect.reload()); 117 | }); 118 | 119 | gulp.task('open', function(){ 120 | return open('http://localhost:8080/demo/demo.html'); 121 | }); 122 | 123 | gulp.task('jshint-test', function(){ 124 | return gulp.src('./test/**/*.js').pipe(jshint()); 125 | }) 126 | 127 | gulp.task('karma', function (done) { 128 | new KarmaServer({ 129 | configFile: __dirname + '/karma.conf.js', 130 | singleRun: true 131 | }, done).start(); 132 | }); 133 | 134 | gulp.task('karma-serve', function(done){ 135 | new KarmaServer({ 136 | configFile: __dirname + '/karma.conf.js' 137 | }, done).start(); 138 | }); 139 | 140 | function handleError(err) { 141 | console.log(err.toString()); 142 | this.emit('end'); 143 | }; 144 | 145 | gulp.task('build', function(cb) { 146 | runSequence('clean', 'scripts', 'styles', cb); 147 | }); 148 | gulp.task('serve', ['build', 'connect', 'watch', 'open']); 149 | gulp.task('default', ['build', 'test']); 150 | gulp.task('test', ['build', 'jshint-test', 'karma']); 151 | gulp.task('serve-test', ['build', 'watch', 'jshint-test', 'karma-serve']); 152 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | 4 | // Base path, that will be used to resolve files and exclude 5 | basePath: '', 6 | 7 | // Frameworks to use 8 | frameworks: ['jasmine'], 9 | 10 | // List of files / patterns to load in the browser 11 | files: [ 12 | 'bower_components/jquery/dist/jquery.js', 13 | 'bower_components/angular/angular.js', 14 | 'bower_components/angular-sanitize/angular-sanitize.js', 15 | 'bower_components/angular-mocks/angular-mocks.js', 16 | 17 | 'dist/json-formatter.js', 18 | 'dist/json-formatter.css', 19 | 'test/**/*.spec.js' 20 | ], 21 | 22 | // List of files to exclude 23 | exclude: [], 24 | 25 | // Web server port 26 | port: 9876, 27 | 28 | // Level of logging 29 | // possible values: config.LOG_DISABLE || config.LOG_ERROR 30 | // || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 31 | logLevel: config.LOG_INFO, 32 | 33 | // Enable / disable watching file and executing tests whenever any file changes 34 | autoWatch: true, 35 | 36 | // Start these browsers, currently available: 37 | // - Chrome 38 | // - ChromeCanary 39 | // - Firefox 40 | // - Opera 41 | // - Safari (only Mac) 42 | // - PhantomJS 43 | // - IE (only Windows) 44 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 45 | 46 | // Continuous Integration mode 47 | // if true, it capture browsers, run tests and exit 48 | singleRun: false, 49 | 50 | reporters: ['mocha'] 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonformatter", 3 | "version": "0.7.0", 4 | "description": "Angular Publishable Directive Boilerplate", 5 | "main": "dist/json-formatter.js", 6 | "repository": "git@github.com:mohsen1/json-formatter.git", 7 | "scripts": { 8 | "test": "./node_modules/gulp/bin/gulp.js test" 9 | }, 10 | "keywords": [ 11 | "angularjs", 12 | "angular", 13 | "directive" 14 | ], 15 | "author": { 16 | "name": "Mohsen Azimi" 17 | }, 18 | "license": "Apache-2.0", 19 | "devDependencies": { 20 | "chai": "^3.2.0", 21 | "del": "^1.2.0", 22 | "event-stream": "^3.3.1", 23 | "gulp": "^3.9.0", 24 | "gulp-angular-templatecache": "^1.6.0", 25 | "gulp-concat": "^2.5.2", 26 | "gulp-connect": "^2.2.0", 27 | "gulp-header": "^1.2.2", 28 | "gulp-jshint": "^1.11.0", 29 | "gulp-karma": "0.0.4", 30 | "gulp-less": "^3.0.3", 31 | "gulp-minify-css": "^1.1.6", 32 | "gulp-minify-html": "^1.0.3", 33 | "gulp-open": "^1.0.0", 34 | "gulp-order": "^1.1.1", 35 | "gulp-plumber": "^1.0.1", 36 | "gulp-rename": "^1.2.2", 37 | "gulp-uglify": "^1.2.0", 38 | "gulp-util": "^3.0.5", 39 | "jasmine-core": "^2.3.4", 40 | "jshint-stylish": "^2.0.0", 41 | "karma": "^0.13.9", 42 | "karma-chrome-launcher": "^0.2.0", 43 | "karma-firefox-launcher": "^0.1.6", 44 | "karma-jasmine": "^0.3.5", 45 | "karma-mocha-reporter": "^1.0.2", 46 | "mversion": "^1.10.0", 47 | "run-sequence": "^1.1.0" 48 | }, 49 | "readme": "# Bower Publishable Angular Directive Boilerplate\n\nThis is a simple AngularJS directive boilerplate to help you start your own AngularJS directive and publish it in Bower and NPM.\nThis readme file itself is a boilerplate.\n\n#### Using the boilerplate\nClone the project and install dependencies, then use Gulp to start the prject.\n```shell\ngit clone git@github.com:mohsen1/angular-directive-boilerplate.git my-directive\ncd my-directive\nnpm install\nbower install\ngulp serve\n```\n#### Install via NPM or bower\n\n```shell\nnpm install angular-directive-boilerplate\n```\n```shell\nbower install angular-directive-boilerplate\n```\n\n### License\nMIT\n", 50 | "readmeFilename": "README.md", 51 | "_id": "angular-directive-boilerplate@0.0.6", 52 | "_shasum": "a6be9010ce530141463d331a83ccaf2858e27b92", 53 | "_from": "angular-directive-boilerplate@0.0.6" 54 | } 55 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsen1/json-formatter/34f9e3d052003d3955a19b52b85b424e937ebcf4/screenshot.png -------------------------------------------------------------------------------- /src/json-formatter.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /src/json-formatter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('jsonFormatter', ['RecursionHelper']) 4 | 5 | .provider('JSONFormatterConfig', function JSONFormatterConfigProvider() { 6 | 7 | // Default values for hover preview config 8 | var hoverPreviewEnabled = false; 9 | var hoverPreviewArrayCount = 100; 10 | var hoverPreviewFieldCount = 5; 11 | 12 | return { 13 | get hoverPreviewEnabled() { 14 | return hoverPreviewEnabled; 15 | }, 16 | set hoverPreviewEnabled(value) { 17 | hoverPreviewEnabled = !!value; 18 | }, 19 | 20 | get hoverPreviewArrayCount() { 21 | return hoverPreviewArrayCount; 22 | }, 23 | set hoverPreviewArrayCount(value) { 24 | hoverPreviewArrayCount = parseInt(value, 10); 25 | }, 26 | 27 | get hoverPreviewFieldCount() { 28 | return hoverPreviewFieldCount; 29 | }, 30 | set hoverPreviewFieldCount(value) { 31 | hoverPreviewFieldCount = parseInt(value, 10); 32 | }, 33 | 34 | $get: function () { 35 | return { 36 | hoverPreviewEnabled: hoverPreviewEnabled, 37 | hoverPreviewArrayCount: hoverPreviewArrayCount, 38 | hoverPreviewFieldCount: hoverPreviewFieldCount 39 | }; 40 | } 41 | }; 42 | }) 43 | 44 | .directive('jsonFormatter', ['RecursionHelper', 'JSONFormatterConfig', function jsonFormatterDirective(RecursionHelper, JSONFormatterConfig) { 45 | function escapeString(str) { 46 | return str.replace('"', '\"'); 47 | } 48 | 49 | // From http://stackoverflow.com/a/332429 50 | function getObjectName(object) { 51 | if (object === undefined) { 52 | return ''; 53 | } 54 | if (object === null) { 55 | return 'Object'; 56 | } 57 | if (typeof object === 'object' && !object.constructor) { 58 | return 'Object'; 59 | } 60 | 61 | //ES6 default gives name to constructor 62 | if (object.__proto__ !== undefined && object.__proto__.constructor !== undefined && object.__proto__.constructor.name !== undefined) { 63 | return object.__proto__.constructor.name; 64 | } 65 | 66 | var funcNameRegex = /function (.{1,})\(/; 67 | var results = (funcNameRegex).exec((object).constructor.toString()); 68 | if (results && results.length > 1) { 69 | return results[1]; 70 | } else { 71 | return ''; 72 | } 73 | } 74 | 75 | function getType(object) { 76 | if (object === null) { return 'null'; } 77 | return typeof object; 78 | } 79 | 80 | function getValuePreview (object, value) { 81 | var type = getType(object); 82 | 83 | if (type === 'null' || type === 'undefined') { return type; } 84 | 85 | if (type === 'string') { 86 | value = '"' + escapeString(value) + '"'; 87 | } 88 | if (type === 'function'){ 89 | 90 | // Remove content of the function 91 | return object.toString() 92 | .replace(/[\r\n]/g, '') 93 | .replace(/\{.*\}/, '') + '{…}'; 94 | 95 | } 96 | return value; 97 | } 98 | 99 | function getPreview(object) { 100 | var value = ''; 101 | if (angular.isObject(object)) { 102 | value = getObjectName(object); 103 | if (angular.isArray(object)) 104 | value += '[' + object.length + ']'; 105 | } else { 106 | value = getValuePreview(object, object); 107 | } 108 | return value; 109 | } 110 | 111 | function link(scope) { 112 | scope.isArray = function () { 113 | return angular.isArray(scope.json); 114 | }; 115 | 116 | scope.isObject = function() { 117 | return angular.isObject(scope.json); 118 | }; 119 | 120 | scope.getKeys = function (){ 121 | if (scope.isObject()) { 122 | return Object.keys(scope.json).map(function(key) { 123 | if (key === '') { return '""'; } 124 | return key; 125 | }); 126 | } 127 | }; 128 | scope.type = getType(scope.json); 129 | scope.hasKey = typeof scope.key !== 'undefined'; 130 | scope.getConstructorName = function(){ 131 | return getObjectName(scope.json); 132 | }; 133 | 134 | if (scope.type === 'string'){ 135 | 136 | // Add custom type for date 137 | if((new Date(scope.json)).toString() !== 'Invalid Date') { 138 | scope.isDate = true; 139 | } 140 | 141 | // Add custom type for URLs 142 | if (scope.json.indexOf('http') === 0) { 143 | scope.isUrl = true; 144 | } 145 | } 146 | 147 | scope.isEmptyObject = function () { 148 | return scope.getKeys() && !scope.getKeys().length && 149 | scope.isOpen && !scope.isArray(); 150 | }; 151 | 152 | 153 | // If 'open' attribute is present 154 | scope.isOpen = !!scope.open; 155 | scope.toggleOpen = function () { 156 | scope.isOpen = !scope.isOpen; 157 | }; 158 | scope.childrenOpen = function () { 159 | if (scope.open > 1){ 160 | return scope.open - 1; 161 | } 162 | return 0; 163 | }; 164 | 165 | scope.openLink = function (isUrl) { 166 | if(isUrl) { 167 | window.location.href = scope.json; 168 | } 169 | }; 170 | 171 | scope.parseValue = function (value){ 172 | return getValuePreview(scope.json, value); 173 | }; 174 | 175 | scope.showThumbnail = function () { 176 | return !!JSONFormatterConfig.hoverPreviewEnabled && scope.isObject() && !scope.isOpen; 177 | }; 178 | 179 | scope.getThumbnail = function () { 180 | if (scope.isArray()) { 181 | 182 | // if array length is greater then 100 it shows "Array[101]" 183 | if (scope.json.length > JSONFormatterConfig.hoverPreviewArrayCount) { 184 | return 'Array[' + scope.json.length + ']'; 185 | } else { 186 | return '[' + scope.json.map(getPreview).join(', ') + ']'; 187 | } 188 | } else { 189 | 190 | var keys = scope.getKeys(); 191 | 192 | // the first five keys (like Chrome Developer Tool) 193 | var narrowKeys = keys.slice(0, JSONFormatterConfig.hoverPreviewFieldCount); 194 | 195 | // json value schematic information 196 | var kvs = narrowKeys 197 | .map(function (key) { return key + ':' + getPreview(scope.json[key]); }); 198 | 199 | // if keys count greater then 5 then show ellipsis 200 | var ellipsis = keys.length >= 5 ? '…' : ''; 201 | 202 | return '{' + kvs.join(', ') + ellipsis + '}'; 203 | } 204 | }; 205 | } 206 | 207 | return { 208 | templateUrl: 'json-formatter.html', 209 | restrict: 'E', 210 | replace: true, 211 | scope: { 212 | json: '=', 213 | key: '=', 214 | open: '=' 215 | }, 216 | compile: function(element) { 217 | 218 | // Use the compile function from the RecursionHelper, 219 | // And return the linking function(s) which it returns 220 | return RecursionHelper.compile(element, link); 221 | } 222 | }; 223 | }]); 224 | 225 | // Export to CommonJS style imports. Exporting this string makes this valid: 226 | // angular.module('myApp', [require('jsonformatter')]); 227 | if (typeof module === 'object') { 228 | module.exports = 'jsonFormatter'; 229 | } -------------------------------------------------------------------------------- /src/json-formatter.less: -------------------------------------------------------------------------------- 1 | .theme( 2 | @default-color: black, 3 | @string-color: green, 4 | @number-color: blue, 5 | @boolean-color: red, 6 | @null-color: #855A00, 7 | @undefined-color: rgb(202, 11, 105), 8 | @function-color: #FF20ED, 9 | @rotate-time: 100ms, 10 | @toggler-opacity: 0.6, 11 | @toggler-color: #45376F, 12 | @bracket-color: blue, 13 | @key-color: #00008B, 14 | @url-color: blue ){ 15 | 16 | font-family: monospace; 17 | &, a, a:hover { 18 | color: @default-color; 19 | text-decoration: none; 20 | } 21 | 22 | .json-formatter-row { 23 | margin-left: 1em; 24 | } 25 | 26 | .children { 27 | &.empty { 28 | opacity: 0.5; 29 | margin-left: 1em; 30 | 31 | &.object:after { content: "No properties"; } 32 | &.array:after { content: "[]"; } 33 | } 34 | } 35 | 36 | .string { 37 | color: @string-color; 38 | white-space: pre; 39 | word-wrap: break-word; 40 | } 41 | .number { color: @number-color; } 42 | .boolean { color: @boolean-color; } 43 | .null { color: @null-color; } 44 | .undefined { color: @undefined-color; } 45 | .function { color: @function-color; } 46 | .date { background-color: fade(@default-color, 5%); } 47 | .url { 48 | text-decoration: underline; 49 | color: @url-color; 50 | cursor: pointer; 51 | } 52 | 53 | .bracket { color: @bracket-color; } 54 | .key { 55 | color: @key-color; 56 | cursor: pointer; 57 | } 58 | .constructor-name { 59 | cursor: pointer; 60 | } 61 | 62 | .toggler { 63 | font-size: 0.8em; 64 | line-height: 1.2em; 65 | vertical-align: middle; 66 | opacity: 0.6; 67 | cursor: pointer; 68 | 69 | &:after { 70 | display: inline-block; 71 | transition: transform @rotate-time ease-in; 72 | content: "►"; 73 | } 74 | 75 | &.open:after{ 76 | transform: rotate(90deg); 77 | } 78 | } 79 | 80 | > a >.thumbnail-text { 81 | opacity: 0; 82 | transition: opacity .15s ease-in; 83 | font-style: italic; 84 | } 85 | 86 | &:hover > a > .thumbnail-text { 87 | opacity: 0.6; 88 | } 89 | } 90 | 91 | 92 | .json-formatter-row { 93 | .theme(); 94 | } 95 | 96 | .json-formatter-dark.json-formatter-row { 97 | .theme( 98 | @default-color: white, 99 | @string-color: #31F031, 100 | @number-color: #66C2FF, 101 | @boolean-color: #EC4242, 102 | @null-color: #EEC97D, 103 | @undefined-color: rgb(239, 143, 190), 104 | @function-color: #FD48CB, 105 | @rotate-time: 100ms, 106 | @toggler-opacity: 0.6, 107 | @toggler-color: #45376F, 108 | @bracket-color: #9494FF, 109 | @key-color: #23A0DB, 110 | @url-color: #027BFF); 111 | } 112 | -------------------------------------------------------------------------------- /src/recursion-helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // from http://stackoverflow.com/a/18609594 4 | angular.module('RecursionHelper', []).factory('RecursionHelper', ['$compile', function($compile){ 5 | return { 6 | /** 7 | * Manually compiles the element, fixing the recursion loop. 8 | * @param element 9 | * @param [link] A post-link function, or an object with function(s) 10 | * registered via pre and post properties. 11 | * @returns An object containing the linking functions. 12 | */ 13 | compile: function(element, link){ 14 | // Normalize the link parameter 15 | if(angular.isFunction(link)){ 16 | link = { post: link }; 17 | } 18 | 19 | // Break the recursion loop by removing the contents 20 | var contents = element.contents().remove(); 21 | var compiledContents; 22 | return { 23 | pre: (link && link.pre) ? link.pre : null, 24 | /** 25 | * Compiles and re-adds the contents 26 | */ 27 | post: function(scope, element){ 28 | // Compile the contents 29 | if(!compiledContents){ 30 | compiledContents = $compile(contents); 31 | } 32 | // Re-add the compiled contents to the element 33 | compiledContents(scope, function(clone){ 34 | element.append(clone); 35 | }); 36 | 37 | // Call the post-linking function, if any 38 | if(link && link.post){ 39 | link.post.apply(null, arguments); 40 | } 41 | } 42 | }; 43 | } 44 | }; 45 | }]); 46 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "immed": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "trailing": true, 9 | "maxlen": 200, 10 | "boss": true, 11 | "eqnull": true, 12 | "expr": true, 13 | "globalstrict": true, 14 | "laxbreak": true, 15 | "loopfunc": true, 16 | "sub": true, 17 | "undef": true, 18 | "indent": 2, 19 | "globals": { 20 | "angular": true, 21 | "afterEach": false, 22 | "beforeEach": false, 23 | "confirm": false, 24 | "context": false, 25 | "describe": false, 26 | "expect": false, 27 | "it": false, 28 | "jasmine": false, 29 | "JSHINT": false, 30 | "mostRecentAjaxRequest": false, 31 | "qq": false, 32 | "runs": false, 33 | "spyOn": false, 34 | "spyOnEvent": false, 35 | "waitsFor": false, 36 | "xdescribe": false, 37 | "module": false, 38 | "inject": false 39 | } 40 | } -------------------------------------------------------------------------------- /test/json-formatter.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('json-formatter', function () { 4 | var scope, $compile, $rootScope, element, fakeModule, JSONFormatterConfig; 5 | 6 | function createDirective(key, open) { 7 | open = open === undefined ? 0 : open; 8 | var elm; 9 | var template = ''; 11 | 12 | elm = angular.element(template); 13 | angular.element(document.body).prepend(elm); 14 | scope._null = null; 15 | scope._undefined = undefined; 16 | scope.number = 42; 17 | scope._function = function add(a, b) { 18 | return a + b; 19 | }; 20 | scope.promiseFunction = function getAdd(service, a) { 21 | return service.get(a).then(function (b) { 22 | return a + b; 23 | }); 24 | }; 25 | scope.string = 'Hello world!'; 26 | scope.date = (new Date(0)).toString(); // begging of Unix time 27 | scope.url = 'https://example.com'; 28 | scope.emptyObject = {}; 29 | scope.emptyObjectWithoutPrototype = Object.create(null); 30 | scope.objectWithEmptyKey = {'': 1}; 31 | scope.emptyArray = []; 32 | scope.array = ['one', 'two', 'three']; 33 | scope.simpleObject = {me: 1}; 34 | scope.longerObject = { 35 | numbers: [ 36 | 1, 37 | 2, 38 | 3 39 | ], 40 | boolean: true, 41 | 'null': null, 42 | number: 123, 43 | anObject: { 44 | a: 'b', 45 | c: 'd', 46 | e: 'f\"' 47 | }, 48 | string: 'Hello World', 49 | url: 'https://github.com/mohsen1/json-formatter', 50 | date: 'Sun Aug 03 2014 20:46:55 GMT-0700 (PDT)', 51 | func: function add(a,b){return a + b; } 52 | }; 53 | scope.mixArray = [1, '2', {number: 3}]; 54 | 55 | $compile(elm)(scope); 56 | scope.$digest(); 57 | 58 | return elm; 59 | } 60 | 61 | beforeEach(function () { 62 | fakeModule = angular 63 | .module('test.jsonFormatter', ['jsonFormatter', 'ngSanitize']) 64 | .config(['JSONFormatterConfigProvider', function (_JSONFormatterConfig_) { 65 | JSONFormatterConfig = _JSONFormatterConfig_; 66 | }]); 67 | module('test.jsonFormatter', 'jsonFormatter', 'ngSanitize'); 68 | }); 69 | beforeEach(inject(function(_$rootScope_, _$compile_) { 70 | $rootScope = _$rootScope_; 71 | scope = $rootScope.$new(); 72 | $compile = _$compile_; 73 | })); 74 | 75 | afterEach(function () { 76 | if (element) { 77 | element.remove(); 78 | element = null; 79 | } 80 | }); 81 | 82 | describe('when created with', function () { 83 | describe('null', function(){ 84 | it('should render "null"', function () { 85 | element = createDirective('_null'); 86 | expect(element.text()).toContain('null'); 87 | }); 88 | }); 89 | 90 | describe('undefined', function(){ 91 | it('should render "undefined"', function () { 92 | element = createDirective('_undefined'); 93 | expect(element.text()).toContain('undefined'); 94 | }); 95 | }); 96 | 97 | describe('function', function(){ 98 | it('should render the function', function () { 99 | element = createDirective('_function'); 100 | expect(element.text()).toContain('function'); 101 | expect(element.text()).toContain('add'); 102 | expect(element.text()).toContain('(a, b)'); 103 | expect(element.text().trim().match(/function\s[^\(]*\([^\)]*\)\s*(.*)/)[1]).toBe('{…}'); 104 | }); 105 | }); 106 | 107 | describe('promiseFunction', function(){ 108 | it('should render the function', function () { 109 | element = createDirective('promiseFunction'); 110 | expect(element.text()).toContain('function'); 111 | expect(element.text()).toContain('getAdd'); 112 | expect(element.text()).toContain('(service, a)'); 113 | expect(element.text().trim().match(/function\s[^\(]*\([^\)]*\)\s*(.*)/)[1]).toBe('{…}'); 114 | }); 115 | }); 116 | 117 | describe('string', function(){ 118 | it('should render "Hello world!"', function () { 119 | element = createDirective('string'); 120 | expect(element.text()).toContain('"Hello world!"'); 121 | }); 122 | }); 123 | 124 | describe('date string', function(){ 125 | beforeEach(function(){ 126 | element = createDirective('date'); 127 | }); 128 | it('should render "' + (new Date(0)).toString() + '"', function () { 129 | expect(element.text()).toContain('"' + (new Date(0)).toString() + '"'); 130 | }); 131 | it('should add "date" class to string', function() { 132 | expect(element.find('span.date').length).toBe(1); 133 | }); 134 | }); 135 | 136 | describe('url string', function(){ 137 | beforeEach(function(){ 138 | element = createDirective('url'); 139 | }); 140 | it('should render "https://example.com"', function () { 141 | expect(element.text()).toContain('"https://example.com"'); 142 | }); 143 | it('should add "url" class to string', function() { 144 | expect(element.find('span.url').length).toBe(1); 145 | }); 146 | }); 147 | 148 | describe('empty object', function(){ 149 | testEmptyObject('emptyObject'); 150 | }); 151 | 152 | describe('empty object without prototype: Object.create(null)', function(){ 153 | testEmptyObject('emptyObjectWithoutPrototype'); 154 | }); 155 | 156 | // DRY for testing empty objects 157 | function testEmptyObject(key) { 158 | describe('with open="0"', function() { 159 | beforeEach(function(){ 160 | element = createDirective(key); 161 | }); 162 | it('should render "Object"', function() { 163 | expect(element.text()).toContain('Object'); 164 | }); 165 | }); 166 | describe('with open="1"', function() { 167 | beforeEach(function(){ 168 | element = createDirective(key, 1); 169 | }); 170 | it('should render "Object"', function() { 171 | expect(element.text()).toContain('Object'); 172 | }); 173 | it('should render have toggler opened', function() { 174 | expect(element.find('.toggler').hasClass('open')).toBe(true); 175 | }); 176 | }); 177 | } 178 | 179 | describe('object with empty key', function(){ 180 | beforeEach(function(){ 181 | element = createDirective('objectWithEmptyKey', 1); 182 | }); 183 | 184 | it('should render "" for key', function(){ 185 | debugger 186 | expect(element.find('.key').text()).toContain('""'); 187 | }); 188 | }); 189 | 190 | describe('empty array', function(){ 191 | beforeEach(function(){ 192 | element = createDirective('emptyArray'); 193 | }); 194 | it('should render "Array"', function(){ 195 | expect(element.text()).toContain('Array'); 196 | }); 197 | it('should have brackets and length: [0]', function(){ 198 | expect(element.text()).toContain('[0]'); 199 | }); 200 | }); 201 | 202 | describe('array', function(){ 203 | beforeEach(function(){ 204 | element = createDirective('array'); 205 | }); 206 | it('should render "Array"', function(){ 207 | expect(element.text()).toContain('Array'); 208 | }); 209 | it('should have brackets and length: [3]', function(){ 210 | expect(element.text()).toContain('[3]'); 211 | }); 212 | }); 213 | 214 | describe('object', function(){ 215 | beforeEach(function(){ 216 | element = createDirective('simpleObject'); 217 | }); 218 | it('should render "Object"', function(){ 219 | expect(element.text()).toContain('Object'); 220 | }); 221 | it('should open when clicking on "Object"', function(){ 222 | element.find('.constructor-name').click(); 223 | expect(element.find('.toggler').hasClass('open')).toBe(true); 224 | }); 225 | }); 226 | 227 | describe('hover preview', function() { 228 | 229 | it('default is disabled', function () { 230 | element = createDirective('mixArray'); 231 | expect(element.find('.thumbnail-text').length).toBe(0); 232 | }); 233 | 234 | describe('set enable', function () { 235 | beforeEach(function () { 236 | JSONFormatterConfig.hoverPreviewEnabled = true; 237 | }); 238 | 239 | it('should render "simple object"', function () { 240 | element = createDirective('simpleObject', 0); 241 | expect(element.find('.thumbnail-text').text().trim()).toBe('{me:1}'); 242 | }); 243 | 244 | it('should render "longer object"', function () { 245 | element = createDirective('longerObject', 0); 246 | expect(element.find('.thumbnail-text').text().trim()).toBe('{numbers:Array[3], boolean:true, null:null, number:123, anObject:Object…}'); 247 | }); 248 | 249 | it('should render "array"', function () { 250 | element = createDirective('array', 0); 251 | expect(element.find('.thumbnail-text').text().trim()).toBe('["one", "two", "three"]'); 252 | }); 253 | 254 | it('should render "mixArray"', function () { 255 | element = createDirective('mixArray', 0); 256 | expect(element.find('.thumbnail-text').text().trim()).toBe('[1, "2", Object]'); 257 | }); 258 | }); 259 | 260 | }); 261 | }); 262 | 263 | }); --------------------------------------------------------------------------------