element.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. /*
  2. Tencent is pleased to support the open source community by making vConsole available.
  3. Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
  4. Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
  5. http://opensource.org/licenses/MIT
  6. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
  7. */
  8. /**
  9. * vConsole Element Tab
  10. */
  11. import './style.less';
  12. import VConsolePlugin from '../lib/plugin.js';
  13. import tplTabbox from './tabbox.html';
  14. import NodeView from './node_view.js';
  15. import * as tool from '../lib/tool.js';
  16. import $ from '../lib/query.js';
  17. class VConsoleElementsTab extends VConsolePlugin {
  18. constructor(...args) {
  19. super(...args);
  20. let that = this;
  21. that.isInited = false;
  22. that.node = {};
  23. that.$tabbox = $.render(tplTabbox, {});
  24. that.nodes = [];
  25. that.activedElem = {}; // actived by user
  26. let MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  27. that.observer = new MutationObserver(function(mutations) {
  28. for (let i=0; i<mutations.length; i++) {
  29. let mutation = mutations[i];
  30. if (that._isInVConsole(mutation.target)) {
  31. continue;
  32. }
  33. that.onMutation(mutation);
  34. }
  35. });
  36. }
  37. onRenderTab(callback) {
  38. callback(this.$tabbox);
  39. }
  40. onAddTool(callback) {
  41. let that = this;
  42. let toolList = [{
  43. name: 'Expand',
  44. global: false,
  45. onClick: function(e) {
  46. if (that.activedElem) {
  47. if (!$.hasClass(that.activedElem, 'vc-toggle')) {
  48. // $.addClass(that.activedElem, 'vc-toggle');
  49. $.one('.vcelm-node', that.activedElem).click();
  50. } else {
  51. for (let i=0; i<that.activedElem.childNodes.length; i++) {
  52. let $child = that.activedElem.childNodes[i];
  53. if ($.hasClass($child, 'vcelm-l') && !$.hasClass($child, 'vcelm-noc') && !$.hasClass($child, 'vc-toggle')) {
  54. $.one('.vcelm-node', $child).click();
  55. break;
  56. }
  57. }
  58. }
  59. }
  60. }
  61. }, {
  62. name: 'Collapse',
  63. global: false,
  64. onClick: function(e) {
  65. if (that.activedElem) {
  66. if ($.hasClass(that.activedElem, 'vc-toggle')) {
  67. $.one('.vcelm-node', that.activedElem).click();
  68. } else {
  69. if (that.activedElem.parentNode && $.hasClass(that.activedElem.parentNode, 'vcelm-l')) {
  70. $.one('.vcelm-node', that.activedElem.parentNode).click();
  71. }
  72. }
  73. }
  74. }
  75. }];
  76. callback(toolList);
  77. }
  78. onShow() {
  79. if (this.isInited) {
  80. return;
  81. }
  82. this.isInited = true;
  83. this.node = this.getNode(document.documentElement);
  84. // console.log(this.node);
  85. // render root view
  86. let $rootView = this.renderView(this.node, $.one('.vc-log', this.$tabbox));
  87. // auto open first level
  88. let $node = $.one('.vcelm-node', $rootView);
  89. $node && $node.click();
  90. // start observing
  91. let config = {
  92. attributes: true,
  93. childList: true,
  94. characterData: true,
  95. subtree: true
  96. };
  97. this.observer.observe(document.documentElement, config);
  98. }
  99. onRemove() {
  100. this.observer.disconnect();
  101. }
  102. // handle mutation
  103. onMutation(mutation) {
  104. // console.log(mutation.type, mutation);
  105. switch (mutation.type) {
  106. case 'childList':
  107. if (mutation.removedNodes.length > 0) {
  108. this.onChildRemove(mutation);
  109. }
  110. if (mutation.addedNodes.length > 0) {
  111. this.onChildAdd(mutation);
  112. }
  113. break;
  114. case 'attributes':
  115. this.onAttributesChange(mutation);
  116. break;
  117. case 'characterData':
  118. this.onCharacterDataChange(mutation);
  119. break;
  120. default:
  121. break;
  122. }
  123. }
  124. onChildRemove(mutation) {
  125. let $parent = mutation.target,
  126. parentNode = $parent.__vconsole_node;
  127. if (!parentNode) {
  128. return;
  129. }
  130. for (let i=0; i<mutation.removedNodes.length; i++) {
  131. let $target = mutation.removedNodes[i],
  132. node = $target.__vconsole_node;
  133. if (!node) {
  134. continue;
  135. }
  136. // remove view
  137. if (node.view) {
  138. node.view.parentNode.removeChild(node.view);
  139. }
  140. }
  141. // update parent node
  142. this.getNode($parent);
  143. }
  144. onChildAdd(mutation) {
  145. let $parent = mutation.target,
  146. parentNode = $parent.__vconsole_node;
  147. // console.log('parentNode', parentNode)
  148. if (!parentNode) {
  149. return;
  150. }
  151. // update parent node
  152. this.getNode($parent);
  153. // update parent view
  154. if (parentNode.view) {
  155. $.removeClass(parentNode.view, 'vcelm-noc');
  156. }
  157. for (let i=0; i<mutation.addedNodes.length; i++) {
  158. let $target = mutation.addedNodes[i],
  159. node = $target.__vconsole_node; // added right now
  160. // console.log('node:', node)
  161. if (!node) {
  162. continue;
  163. }
  164. // create view
  165. if (mutation.nextSibling !== null) {
  166. // insert before it's sibling
  167. let siblingNode = mutation.nextSibling.__vconsole_node;
  168. if (siblingNode.view) {
  169. this.renderView(node, siblingNode.view, 'insertBefore');
  170. }
  171. } else {
  172. // append to parent view
  173. if (parentNode.view) {
  174. if (parentNode.view.lastChild) {
  175. // insert before last child, eg: </tag>
  176. this.renderView(node, parentNode.view.lastChild, 'insertBefore');
  177. } else {
  178. this.renderView(node, parentNode.view);
  179. }
  180. }
  181. }
  182. }
  183. }
  184. onAttributesChange(mutation) {
  185. let node = mutation.target.__vconsole_node;
  186. if (!node) {
  187. return;
  188. }
  189. // update node
  190. node = this.getNode(mutation.target);
  191. // update view
  192. if (node.view) {
  193. this.renderView(node, node.view, true);
  194. }
  195. }
  196. onCharacterDataChange(mutation) {
  197. let node = mutation.target.__vconsole_node;
  198. if (!node) {
  199. return;
  200. }
  201. // update node
  202. node = this.getNode(mutation.target);
  203. // update view
  204. if (node.view) {
  205. this.renderView(node, node.view, true);
  206. }
  207. }
  208. renderView(node, $related, renderMethod) {
  209. let that = this;
  210. let $view = (new NodeView(node)).get();
  211. // connect to node
  212. node.view = $view;
  213. // expand
  214. $.delegate($view, 'click', '.vcelm-node', function(event) {
  215. event.stopPropagation();
  216. let $parent = this.parentNode;
  217. if ($.hasClass($parent, 'vcelm-noc')) {
  218. return;
  219. }
  220. that.activedElem = $parent;
  221. if ($.hasClass($parent, 'vc-toggle')) {
  222. $.removeClass($parent, 'vc-toggle');
  223. } else {
  224. $.addClass($parent, 'vc-toggle');
  225. }
  226. // render child views
  227. let childIdx = -1;
  228. for (let i=0; i<$parent.children.length; i++) {
  229. let $child = $parent.children[i];
  230. if (!$.hasClass($child, 'vcelm-l')) {
  231. // not a child view, skip
  232. continue;
  233. }
  234. childIdx++;
  235. if ($child.children.length > 0) {
  236. // already been rendered, skip
  237. continue;
  238. }
  239. if (!node.childNodes[childIdx]) {
  240. // cannot find related node, hide it
  241. $child.style.display = 'none';
  242. continue;
  243. }
  244. that.renderView(node.childNodes[childIdx], $child, 'replace');
  245. }
  246. });
  247. // output to page
  248. switch (renderMethod) {
  249. case 'replace':
  250. $related.parentNode.replaceChild($view, $related);
  251. break;
  252. case 'insertBefore':
  253. $related.parentNode.insertBefore($view, $related);
  254. break;
  255. default:
  256. $related.appendChild($view);
  257. break;
  258. }
  259. return $view;
  260. }
  261. // convert an element to a simple object
  262. getNode(elem) {
  263. if (this._isIgnoredElement(elem)) {
  264. return undefined;
  265. }
  266. let node = elem.__vconsole_node || {};
  267. // basic node info
  268. node.nodeType = elem.nodeType;
  269. node.nodeName = elem.nodeName;
  270. node.tagName = elem.tagName || '';
  271. node.textContent = '';
  272. if (
  273. node.nodeType == elem.TEXT_NODE ||
  274. node.nodeType == elem.DOCUMENT_TYPE_NODE
  275. ) {
  276. node.textContent = elem.textContent;
  277. }
  278. // attrs
  279. node.id = elem.id || '';
  280. node.className = elem.className || '';
  281. node.attributes = [];
  282. if (elem.hasAttributes && elem.hasAttributes()) {
  283. for (let i=0; i<elem.attributes.length; i++) {
  284. node.attributes.push({
  285. name: elem.attributes[i].name,
  286. value: elem.attributes[i].value || ''
  287. });
  288. }
  289. }
  290. // child nodes
  291. node.childNodes = [];
  292. if (elem.childNodes.length > 0) {
  293. for (let i=0; i<elem.childNodes.length; i++) {
  294. let child = this.getNode(elem.childNodes[i]);
  295. if (!child) {
  296. continue;
  297. }
  298. node.childNodes.push(child);
  299. }
  300. }
  301. // save node to element for further actions
  302. elem.__vconsole_node = node;
  303. return node;
  304. }
  305. _isIgnoredElement(elem) {
  306. // empty or line-break text
  307. if (elem.nodeType == elem.TEXT_NODE) {
  308. if (elem.textContent.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$|\n+/g, '') == '') { // trim
  309. return true;
  310. }
  311. }
  312. return false;
  313. }
  314. _isInVConsole(elem) {
  315. let target = elem;
  316. while (target != undefined) {
  317. if (target.id == '__vconsole') {
  318. return true;
  319. }
  320. target = target.parentNode || undefined;
  321. }
  322. return false;
  323. }
  324. } // END class
  325. export default VConsoleElementsTab;