diff --git a/shells/dev/target/NativeTypes.vue b/shells/dev/target/NativeTypes.vue new file mode 100644 index 000000000..498fb2944 --- /dev/null +++ b/shells/dev/target/NativeTypes.vue @@ -0,0 +1,55 @@ + + + diff --git a/shells/dev/target/index.js b/shells/dev/target/index.js index 7d6197cec..3e5ef0e8f 100644 --- a/shells/dev/target/index.js +++ b/shells/dev/target/index.js @@ -3,6 +3,7 @@ import store from './store' import Target from './Target.vue' import Other from './Other.vue' import Counter from './Counter.vue' +import NativeTypes from './NativeTypes.vue' import Events from './Events.vue' import MyClass from './MyClass.js' @@ -21,7 +22,8 @@ new Vue({ h(Counter), h(Target, {props:{msg: 'hi', ins: new MyClass()}}), h(Other), - h(Events) + h(Events), + h(NativeTypes) ]) }, data: { diff --git a/shells/dev/target/store.js b/shells/dev/target/store.js index 90c222b35..1c55a3a6f 100644 --- a/shells/dev/target/store.js +++ b/shells/dev/target/store.js @@ -5,13 +5,19 @@ Vue.use(Vuex) export default new Vuex.Store({ state: { - count: 0 + count: 0, + date: new Date() }, mutations: { INCREMENT: state => state.count++, - DECREMENT: state => state.count-- + DECREMENT: state => state.count--, + UPDATE_DATE: state => { + state.date = new Date() + }, + TEST_COMPONENT: state => {} }, getters: { - isPositive: state => state.count >= 0 + isPositive: state => state.count >= 0, + hours: state => state.date.getHours() } }) diff --git a/src/backend/index.js b/src/backend/index.js index 04f318bf5..0d72db75c 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -21,7 +21,7 @@ const hook = window.__VUE_DEVTOOLS_GLOBAL_HOOK__ const rootInstances = [] const propModes = ['default', 'sync', 'once'] -const instanceMap = window.__VUE_DEVTOOLS_INSTANCE_MAP__ = new Map() +export const instanceMap = window.__VUE_DEVTOOLS_INSTANCE_MAP__ = new Map() const consoleBoundInstances = Array(5) let currentInspectedId let bridge @@ -320,18 +320,46 @@ function getInstanceDetails (id) { return { id: id, name: getInstanceName(instance), - state: processProps(instance).concat( - processState(instance), - processComputed(instance), - processRouteContext(instance), - processVuexGetters(instance), - processFirebaseBindings(instance), - processObservables(instance) - ) + state: getInstanceState(instance) } } } +function getInstanceState (instance) { + return processProps(instance).concat( + processState(instance), + processComputed(instance), + processRouteContext(instance), + processVuexGetters(instance), + processFirebaseBindings(instance), + processObservables(instance) + ) +} + +export function getCustomInstanceDetails (instance) { + const state = getInstanceState(instance) + return { + _custom: { + type: 'component', + id: instance.__VUE_DEVTOOLS_UID__, + display: getInstanceName(instance), + state: reduceStateList(state) + } + } +} + +export function reduceStateList (list) { + if (!list.length) { + return undefined + } + return list.reduce((map, item) => { + const key = item.type || 'data' + const obj = map[key] = map[key] || {} + obj[item.key] = item.value + return map + }, {}) +} + /** * Get the appropriate display name for an instance. * diff --git a/src/devtools/components/DataField.vue b/src/devtools/components/DataField.vue index 2fa1d7037..4c3e15ab4 100644 --- a/src/devtools/components/DataField.vue +++ b/src/devtools/components/DataField.vue @@ -8,14 +8,14 @@ :class="{ rotated: expanded }" v-show="isExpandableType"> - {{ field.key }} - :
+ {{ field.key }} + :
{{ key }} {{ val }}
- {{ formattedValue }} + {{ formattedValue }}
({ key, - value: value[key] + value: value[key], + noDisplay: isCustom }))) } return value }, limitedSubFields () { return this.formattedSubFields.slice(0, this.limit) + }, + valueClass () { + const cssClass = [this.valueType] + if (this.valueType === 'custom') { + const value = this.field.value + value._custom.type && cssClass.push(`type-${value._custom.type}`) + value._custom.class && cssClass.push(value._custom.class) + } + return cssClass } }, methods: { @@ -175,6 +198,8 @@ export default { transform rotate(90deg) .key color #881391 + &.special + color $blueishGrey .colon margin-right .5em position relative @@ -186,6 +211,16 @@ export default { color #999 &.literal color #0033cc + &.custom + &.type-component + color $green + &::before, + &::after + color $darkerGrey + &::before + content '<' + &::after + content '>' .type color $background-color diff --git a/src/devtools/variables.styl b/src/devtools/variables.styl index d9b54f50c..12188e613 100644 --- a/src/devtools/variables.styl +++ b/src/devtools/variables.styl @@ -1,6 +1,8 @@ // Colors $blue = #44A1FF $grey = #DDDDDD +$darkerGrey = #CCC +$blueishGrey = #486887 $green = #42B983 $darkerGreen = #3BA776 $slate = #242424 diff --git a/src/devtools/views/components/ComponentInstance.vue b/src/devtools/views/components/ComponentInstance.vue index 6ccb9967b..e41feac06 100644 --- a/src/devtools/views/components/ComponentInstance.vue +++ b/src/devtools/views/components/ComponentInstance.vue @@ -186,7 +186,7 @@ export default { transform rotate(90deg) .angle-bracket - color #ccc + color $darkerGrey .instance-name color $component-color diff --git a/src/util.js b/src/util.js index ae1158acf..77eed2e85 100644 --- a/src/util.js +++ b/src/util.js @@ -1,5 +1,7 @@ import CircularJSON from 'circular-json-es6' +import { instanceMap, getCustomInstanceDetails } from 'src/backend' + function cached (fn) { const cache = Object.create(null) return function cachedFn (str) { @@ -43,7 +45,8 @@ export function stringify (data) { return CircularJSON.stringify(data, replacer) } -function replacer (key, val) { +function replacer (key) { + const val = this[key] if (val === undefined) { return UNDEFINED } else if (val === Infinity) { @@ -53,6 +56,10 @@ function replacer (key, val) { } else if (val instanceof RegExp) { // special handling of native type return `[native RegExp ${val.toString()}]` + } else if (val instanceof Date) { + return `[native Date ${val.toString()}]` + } else if (val && val._isVue) { + return getCustomInstanceDetails(val) } else { return sanitize(val) } @@ -64,6 +71,8 @@ export function parse (data, revive) { : CircularJSON.parse(data) } +const specialTypeRE = /^\[native (\w+) (.*)\]$/ + function reviver (key, val) { if (val === UNDEFINED) { return undefined @@ -71,6 +80,13 @@ function reviver (key, val) { return Infinity } else if (val === NAN) { return NaN + } else if (val && val._custom) { + if (val._custom.type === 'component') { + return instanceMap.get(val._custom.id) + } + } else if (specialTypeRE.test(val)) { + const [, type, string] = specialTypeRE.exec(val) + return new window[type](string) } else { return val } diff --git a/test/specs/test.js b/test/specs/test.js index 69b7a0211..d23706919 100644 --- a/test/specs/test.js +++ b/test/specs/test.js @@ -1,6 +1,6 @@ module.exports = { 'vue-devtools e2e tests': function (browser) { - var baseInstanceCount = 6 + var baseInstanceCount = 7 browser .url('http://localhost:' + (process.env.PORT || 8081)) @@ -199,9 +199,10 @@ module.exports = { .setValue('.import-state textarea', '{invalid: json}') .waitForElementVisible('.message.invalid-json', 100) .clearValue('.import-state textarea') - .setValue('.import-state textarea', '{"valid": "json"}') + .setValue('.import-state textarea', '{"count":42,"date":"[native Date Fri Dec 22 2017 10:12:04 GMT+0100 (CET)]"}') .waitForElementNotVisible('.message.invalid-json', 1000) - .assert.containsText('.vuex-state-inspector', 'valid:"json"') + .assert.containsText('.vuex-state-inspector', 'count:42') + .assert.containsText('.vuex-state-inspector', 'date:Fri Dec 22 2017 10:12:04 GMT+0100 (CET)') .click('.import') .waitForElementNotPresent('.import-state', 2000)