script.js 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255
  1. class ClassPage {
  2. static ModeCookieName = 'doczilla-js-docs-class-page-mode';
  3. static Mode = {
  4. Tabs: 'mode-tabs',
  5. List: 'mode-list'
  6. };
  7. static StyleClasses = {
  8. Selected: 'selected',
  9. Active: 'active',
  10. CtrlPressed: 'ctrl-pressed',
  11. ShiftPressed: 'shift-pressed',
  12. ParentsBranch: 'parents-branch',
  13. ClassItem: 'class-item',
  14. ClassName: 'class-name',
  15. ClassIcon: 'class-icon',
  16. Filler: 'filler',
  17. FullSourcePrompt: 'full-source-prompt',
  18. FullSourcePromptText: 'full-source-prompt-text',
  19. FullSourcePromptButton: 'full-source-prompt-button',
  20. Spacing: 'spacing',
  21. CmLink: 'cm-link',
  22. Z8Locale: 'z8-locale',
  23. Hidden: 'hidden',
  24. Collapsed: 'collapsed',
  25. Highlighted: 'highlighted',
  26. Readonly: 'readonly',
  27. Clickable: 'clickable',
  28. Empty: 'empty',
  29. White: 'white'
  30. };
  31. static Attributes = {
  32. DataTab: 'data-tab',
  33. DataDisplayMode: 'data-display-mode',
  34. DataClassName: 'data-class-name',
  35. OnClick: 'onclick',
  36. DataPropertyName: 'data-property-name',
  37. DataPropertyType: 'data-property-type',
  38. DataPropertyParent: 'data-property-parent',
  39. DataPropertyInherited: 'data-property-inherited',
  40. DataPropertyDynamic: 'data-property-dynamic'
  41. };
  42. static Messages = {
  43. NoMixins: 'This class has no mixins.',
  44. NoChildren: 'This class has no child classes.',
  45. NoMixedIn: 'This class is not mixed in any classes.',
  46. NoParents: 'This is a base class, which has no parent classes.',
  47. ShowFullSourceText: 'There were found another entities in the source file of this class. Would you like to see full source file?',
  48. HideFullSourceText: 'Full source file shown. Would you like to hide all entities except the target class?',
  49. PromptButtonText: 'OK',
  50. CmLinkTipPrefix: 'Ctrl+Click to go to class'
  51. };
  52. static TabNames = {
  53. Editor: 'Editor',
  54. Methods: 'Methods',
  55. Parents: 'Parents',
  56. Properties: 'Properties',
  57. Mixins: 'Mixins',
  58. Children: 'Children',
  59. MixedIn: 'MixedIn',
  60. Info: 'Info',
  61. Contribution: 'Contribution'
  62. };
  63. static PropertyType = {
  64. Statics: 'statics',
  65. Base: 'base',
  66. Overridden: 'overridden',
  67. Dynamic: 'dynamic',
  68. Inherited: 'inherited',
  69. ClassComment: 'ClassComment'
  70. };
  71. static PropertyLabel = {
  72. Statics: 'Static properties',
  73. Base: 'Base properties',
  74. Overridden: 'Overridden properties',
  75. Dynamic: 'Dynamic properties',
  76. Inherited: 'Inherited properties'
  77. };
  78. static MethodLabel = {
  79. Statics: 'Static methods',
  80. Base: 'Base methods',
  81. Overridden: 'Overridden methods',
  82. Dynamic: 'Dynamic methods',
  83. Inherited: 'Inherited methods'
  84. };
  85. static ClassProperties = {
  86. Name: 'name',
  87. Methods: 'methods',
  88. Properties: 'properties',
  89. Children: 'children',
  90. Mixins: 'mixins',
  91. MixedIn: 'mixedIn',
  92. ParentsBranch: 'parentsBranch',
  93. Statics: 'statics',
  94. DynamicProperties: 'dynamicProperties',
  95. Root: 'root',
  96. ShortName: 'shortName'
  97. };
  98. static __static__ = '__static__';
  99. static __self__ = '__self__';
  100. start() {
  101. if(!Class[ClassPage.ClassProperties.Root]) {
  102. return this;
  103. }
  104. this.tabElements = {
  105. [ClassPage.TabNames.Editor]: DOM.get('.tab.editor'),
  106. [ClassPage.TabNames.Methods]: DOM.get('.tab.methods'),
  107. [ClassPage.TabNames.Parents]: DOM.get('.tab.parents'),
  108. [ClassPage.TabNames.Properties]: DOM.get('.tab.properties'),
  109. [ClassPage.TabNames.Mixins]: DOM.get('.tab.mixins'),
  110. [ClassPage.TabNames.Children]: DOM.get('.tab.children'),
  111. [ClassPage.TabNames.MixedIn]: DOM.get('.tab.mixedin'),
  112. [ClassPage.TabNames.Info]: DOM.get('.tab.info')
  113. };
  114. this.contentElements = {
  115. [ClassPage.TabNames.Editor]: DOM.get('.content-tab#editor'),
  116. [ClassPage.TabNames.Methods]: DOM.get('.content-tab#methods'),
  117. [ClassPage.TabNames.Parents]: DOM.get('.content-tab#parents'),
  118. [ClassPage.TabNames.Properties]: DOM.get('.content-tab#properties'),
  119. [ClassPage.TabNames.Mixins]: DOM.get('.content-tab#mixins'),
  120. [ClassPage.TabNames.Children]: DOM.get('.content-tab#children'),
  121. [ClassPage.TabNames.MixedIn]: DOM.get('.content-tab#mixedin'),
  122. [ClassPage.TabNames.Info]: DOM.get('.content-tab#info')
  123. };
  124. if(isEditor) {
  125. this.tabElements[ClassPage.TabNames.Contribution] = DOM.get('.tab.contribution');
  126. this.contentElements[ClassPage.TabNames.Contribution] = DOM.get('.content-tab#contribution');
  127. }
  128. this.documented = 0; //Object.keys(Comments).filter((key) => { return Comments[key].text.length > 0; }).length;
  129. this.documentable = 1; // __self__ property is always included
  130. this.inheritedCommentsQuery = {};
  131. this.inheritedCommentsFields = {};
  132. this.propertyItemElements = {};
  133. this.documentedPercentage = DOM.get('.class-documented-percentage');
  134. const rightContainer = this.rightContainer = DOM.get('.right');
  135. const modeCookieValue = DOM.getCookieProperty(App.CookieName, ClassPage.ModeCookieName);
  136. if(!modeCookieValue)
  137. DOM.setCookieProperty(App.CookieName, ClassPage.ModeCookieName, ClassPage.Mode.Tabs);
  138. const mode = this.mode = modeCookieValue || ClassPage.Mode.Tabs;
  139. const tabsModeButton = this.tabsModeButton = DOM.get('.display-mode-button.mode-tabs');
  140. const listModeButton = this.listModeButton = DOM.get('.display-mode-button.mode-list');
  141. this.contextMenu = ContextMenu.init();
  142. (mode === ClassPage.Mode.Tabs ? tabsModeButton : listModeButton).addClass(ClassPage.StyleClasses.Selected);
  143. rightContainer.addClass(mode);
  144. this.loadDocumentedInfo();
  145. this.renderContent();
  146. this.markContentInEditor();
  147. this.registerEventListeners();
  148. this.applyHash();
  149. this.openSocket();
  150. return this;
  151. }
  152. switchMode(mode) {
  153. this.rightContainer.removeClass(this.mode);
  154. this.mode = mode;
  155. DOM.setCookieProperty(App.CookieName, ClassPage.ModeCookieName, mode);
  156. this.rightContainer.addClass(mode);
  157. if(!Class.isPackage)
  158. this.codeMirrorEditor.cmRefresh();
  159. }
  160. selectTab(tab) {
  161. tab = typeof tab === 'string' ? this.tabElements[tab] : tab;
  162. const selectedTab = this.selectedTab;
  163. let filler = selectedTab ? selectedTab.getFirstChild('.filler') : null;
  164. if(!filler)
  165. filler = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.Filler });
  166. selectedTab && selectedTab.removeClass(ClassPage.StyleClasses.Selected);
  167. this.selectedTab = tab.addClass(ClassPage.StyleClasses.Selected);
  168. this.selectedTab.append(filler);
  169. }
  170. activateContent(content) {
  171. content = typeof content === 'string' ? this.contentElements[content] : content;
  172. if(this.activeContent)
  173. this.activeContent.removeClass(ClassPage.StyleClasses.Active);
  174. this.activeContent = content.addClass(ClassPage.StyleClasses.Active);
  175. if(content === this.contentElements[ClassPage.TabNames.Editor])
  176. this.codeMirrorEditor.cmRefresh();
  177. if(content === this.contentElements[ClassPage.TabNames.Properties] || content === this.contentElements[ClassPage.TabNames.Methods])
  178. DOM.getAll('.property-item-comment-input').forEach((item) => {
  179. item.style('height', `${Math.min(422, item.get().scrollHeight + 2)}px`);
  180. });
  181. }
  182. registerEventListeners() {
  183. this.registerTabsEventListeners();
  184. this.registerModeButtonsEventListeners();
  185. }
  186. registerTabsEventListeners() {
  187. const tabElements = this.tabElements;
  188. for(const tabName of Object.keys(tabElements)) {
  189. if(tabElements[tabName])
  190. tabElements[tabName].on(DOM.Events.Click, this.onTabClick.bind(this));
  191. }
  192. }
  193. registerModeButtonsEventListeners() {
  194. this.tabsModeButton.on(DOM.Events.Click, this.onModeButtonClick.bind(this));
  195. this.listModeButton.on(DOM.Events.Click, this.onModeButtonClick.bind(this));
  196. }
  197. prepareSource() {
  198. const className = Class[ClassPage.ClassProperties.Name].replaceAll('.', '\\.');
  199. const classRx = new RegExp(`Z8\\.define\\(\'${className}\',\\s*\\{(?:.|[\r\n])+?^\\}\\);?`, 'gm');
  200. const classSourceMatch = ClassSource.match(classRx);
  201. if(!classSourceMatch) { // remove after `}\n);` issue fixed...
  202. this.sourceHasAnotherEntities = false;
  203. return this.classSource = ClassSource;
  204. }
  205. const classSource = this.classSource = classSourceMatch[0];
  206. this.sourceHasAnotherEntities = ClassSource.trim() !== classSource;
  207. return classSource;
  208. }
  209. renderContent() {
  210. if(!Class.isPackage) {
  211. this.renderEditor();
  212. this.renderParents();
  213. this.renderMixins();
  214. this.renderChildren();
  215. this.renderMixedIn();
  216. this.renderProperties();
  217. this.renderMethods();
  218. this.loadInheritedComments();
  219. if(isEditor)
  220. this.renderContribution();
  221. }
  222. this.renderDocumentedPercentage();
  223. this.renderInfo();
  224. }
  225. renderDocumentedPercentage() {
  226. this.documentedPercentage.setInnerHTML(`${Math.round(this.documented/this.documentable * 10000) / 100}%`);
  227. }
  228. renderEditor() {
  229. this.codeMirrorEditor = CodeMirror(DOM.get('#editor').get(), {
  230. [App.CodeMirrorProperties.Value]: this.prepareSource(ClassSource),
  231. [App.CodeMirrorProperties.Mode]: 'javascript',
  232. [App.CodeMirrorProperties.Theme]: 'darcula',
  233. [App.CodeMirrorProperties.Readonly]: true,
  234. [App.CodeMirrorProperties.LineNumbers]: true,
  235. [App.CodeMirrorProperties.MatchBrackets]: true,
  236. [App.CodeMirrorProperties.ScrollbarStyle]: 'overlay',
  237. [App.CodeMirrorProperties.ConfigureMouse]: (cm, repeat, ev) => {
  238. return { 'addNew': false };
  239. },
  240. });
  241. if(this.sourceHasAnotherEntities)
  242. this.renderFullSourcePrompt();
  243. this.codeMirrorEditorElement = DOM.get('.CodeMirror');
  244. }
  245. renderProperties() {
  246. const propertiesElement = this.contentElements[ClassPage.TabNames.Properties];
  247. const properties = this.getProperties(false);
  248. this.renderPropertiesType(properties, ClassPage.PropertyType.Statics, ClassPage.PropertyLabel.Statics, propertiesElement, false, false);
  249. this.renderPropertiesType(properties, ClassPage.PropertyType.Base, ClassPage.PropertyLabel.Base, propertiesElement, false, false);
  250. this.renderPropertiesType(properties, ClassPage.PropertyType.Overridden, ClassPage.PropertyLabel.Overridden, propertiesElement, false, false);
  251. this.renderPropertiesType(properties, ClassPage.PropertyType.Dynamic, ClassPage.PropertyLabel.Dynamic, propertiesElement, false, false);
  252. this.renderPropertiesType(properties, ClassPage.PropertyType.Inherited, ClassPage.PropertyLabel.Inherited, propertiesElement, true, false);
  253. }
  254. renderMethods() {
  255. const methodsElement = this.contentElements[ClassPage.TabNames.Methods];
  256. const properties = this.getProperties(true);
  257. this.renderPropertiesType(properties, ClassPage.PropertyType.Statics, ClassPage.MethodLabel.Statics, methodsElement, false, true);
  258. this.renderPropertiesType(properties, ClassPage.PropertyType.Base, ClassPage.MethodLabel.Base, methodsElement, false, true);
  259. this.renderPropertiesType(properties, ClassPage.PropertyType.Overridden, ClassPage.MethodLabel.Overridden, methodsElement, false, true);
  260. this.renderPropertiesType(properties, ClassPage.PropertyType.Dynamic, ClassPage.MethodLabel.Dynamic, methodsElement, false, true);
  261. this.renderPropertiesType(properties, ClassPage.PropertyType.Inherited, ClassPage.MethodLabel.Inherited, methodsElement, true, true);
  262. }
  263. // TODO: refactor! Make PropertyItem class
  264. renderPropertiesType(properties, type, headerText, container, initiallyHidden, isMethods) {
  265. const isStatics = type === ClassPage.PropertyType.Statics;
  266. const isInherited = type === ClassPage.PropertyType.Inherited;
  267. properties = properties[type];
  268. if(!properties || properties.length == 0)
  269. return;
  270. const propertyItemClickable = (element) => {
  271. let el = element;
  272. while(!el.hasClass('property-item-comment-static') && !el.hasClass('property-item'))
  273. el = el.getParent();
  274. return element.hasClass('property-item-nearest-parent-span')
  275. || element.hasClass('property-item-comment-input')
  276. || element.hasClass('property-item-comment-button')
  277. || element.hasClass('property-item-saving-filler')
  278. || (el.hasClass('property-item-comment-static') && el.hasClass(ClassPage.StyleClasses.Clickable))
  279. || element.getTag() === DOM.Tags.A;
  280. };
  281. if(!isInherited) {
  282. this.documentable += properties.length;
  283. } else {
  284. const inheritedCommentsQuery = this.inheritedCommentsQuery;
  285. properties.forEach((prop) => {
  286. if(inheritedCommentsQuery[prop.nearestParent]) {
  287. inheritedCommentsQuery[prop.nearestParent].properties.push(prop.key);
  288. } else {
  289. inheritedCommentsQuery[prop.nearestParent] = { root: prop.nearestParentRoot, className: prop.nearestParent, properties: [prop.key] };
  290. }
  291. });
  292. }
  293. const propertiesHeaderText = DOM.create({ tag: DOM.Tags.Span, cls: 'properties-header-text', innerHTML: headerText });
  294. const propertiesHeaderCollapsedIcon = DOM.create({ tag: DOM.Tags.Div, cls: 'properties-header-collapsed-icon' });
  295. const propertiesHeader = DOM.create({ tag: DOM.Tags.Div, cls: 'properties-header', cn: [propertiesHeaderText, propertiesHeaderCollapsedIcon] }, container);
  296. const propertiesList = DOM.create({ tag: DOM.Tags.Div, cls: 'properties-list' }, container);
  297. if(initiallyHidden) {
  298. propertiesList.addClass(ClassPage.StyleClasses.Hidden);
  299. propertiesHeader.addClass(ClassPage.StyleClasses.Collapsed);
  300. }
  301. for(const property of properties) {
  302. const onMouseDown = (e) => {
  303. const element = CDElement.get(e.target);
  304. const key = isStatics ? `${ClassPage.__static__}${property.key}` : property.key;
  305. if(e.buttons === DOM.MouseButtons.Right) {
  306. if(element.hasClass('property-item-saving-filler'))
  307. return;
  308. setTimeout(() => {
  309. this.showContextMenu(element, { x: e.pageX, y: e.pageY });
  310. }, 10);
  311. } else if (e.buttons === DOM.MouseButtons.Left) {
  312. if(propertyItemClickable(element))
  313. return;
  314. if(type === ClassPage.PropertyType.Inherited) {
  315. Url.goTo(`/class/${property.nearestParent}#${isMethods ? 'Methods' : 'Properties'}:${key}`);
  316. } else {
  317. this.searchPropertyInEditor(isMethods, property.dynamic, key);
  318. }
  319. }
  320. }
  321. const data = { type: property.type, nearestParent: property.nearestParent, static: isStatics, value: property.value };
  322. this.createPropertyItem(property.key, data, type, propertiesList, isMethods, onMouseDown);
  323. }
  324. propertiesHeader.on(DOM.Events.Click, (e) => {
  325. propertiesList.switchClass(ClassPage.StyleClasses.Hidden);
  326. propertiesHeader.switchClass(ClassPage.StyleClasses.Collapsed);
  327. });
  328. }
  329. updateComment(classRoot, className, propertyName, commentContent) {
  330. return fetch('/updateComment', {
  331. method: 'POST',
  332. headers: {
  333. 'Content-Type': 'application/x-www-form-urlencoded'
  334. },
  335. body: new URLSearchParams({
  336. 'root': classRoot,
  337. 'class': className,
  338. 'property': propertyName,
  339. 'comment': commentContent
  340. })
  341. });
  342. }
  343. createCommentDateElement(date, author) {
  344. const commentDateText = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-comment-date-text', innerHTML: `Commented by <b>${author}</b> on: ` });
  345. const commentDateDate = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-comment-date-date', innerHTML: CDUtils.dateFormatUTC(date, 3, 'D.M.Y, H:I:S') });
  346. return DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-comment-date', cn: [commentDateText, commentDateDate] });
  347. }
  348. loadInheritedComments() {
  349. if(Object.keys(this.inheritedCommentsQuery).length === 0 || Object.keys(this.inheritedCommentsFields).length === 0)
  350. return;
  351. fetch('/getInheritedComments', {
  352. method: 'POST',
  353. headers: {
  354. 'Content-Type': 'application/x-www-form-urlencoded'
  355. },
  356. body: new URLSearchParams({
  357. query: JSON.stringify(this.inheritedCommentsQuery)
  358. })
  359. }).then(res => res.json()).then((inheritedComments) => {
  360. for(const cls of Object.keys(inheritedComments)) {
  361. const props = inheritedComments[cls];
  362. for(const prop of Object.keys(props)) {
  363. const element = this.inheritedCommentsFields[`${cls}:${prop}`];
  364. if(element) {
  365. element.setInnerHTML(props[prop].text);
  366. element.removeClass(ClassPage.StyleClasses.Empty);
  367. element.getParent().append(this.createCommentDateElement(props[prop].timestamp, props[prop].author));
  368. }
  369. }
  370. }
  371. });
  372. }
  373. getProperties(methods) {
  374. const filter = methods ? (item) => item.type === 'method' : (item) => item.type !== 'method';
  375. const statics = Class[ClassPage.ClassProperties.Statics].filter(filter);
  376. const properties = Class[ClassPage.ClassProperties.Properties].sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())).sort((a, b) => {
  377. return Class[ClassPage.ClassProperties.ParentsBranch].indexOf(a.nearestParent) > Class[ClassPage.ClassProperties.ParentsBranch].indexOf(b.nearestParent) ? -1 : 1;
  378. }).filter(filter);
  379. const dynamicProperties = methods ? [] : Class[ClassPage.ClassProperties.DynamicProperties];
  380. const result = {
  381. [ClassPage.PropertyType.Statics]: statics,
  382. [ClassPage.PropertyType.Base]: properties.filter((item) => !item.inherited),
  383. [ClassPage.PropertyType.Overridden]: properties.filter((item) => item.overridden),
  384. [ClassPage.PropertyType.Inherited]: properties.filter((item) => item.inherited && !item.overridden),
  385. [ClassPage.PropertyType.Dynamic]: dynamicProperties
  386. };
  387. return result;
  388. }
  389. renderMixins() {
  390. const mixinsElement = this.contentElements[ClassPage.TabNames.Mixins];
  391. if(Class[ClassPage.ClassProperties.Mixins].length == 0) {
  392. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoMixins }, mixinsElement);
  393. mixinsElement.addClass(ClassPage.StyleClasses.Empty);
  394. return;
  395. }
  396. this.renderClassItems(Class[ClassPage.ClassProperties.Mixins], mixinsElement);
  397. }
  398. renderChildren() {
  399. const childrenElement = this.contentElements[ClassPage.TabNames.Children];
  400. if(Class[ClassPage.ClassProperties.Children].length == 0) {
  401. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoChildren }, childrenElement);
  402. childrenElement.addClass(ClassPage.StyleClasses.Empty);
  403. return;
  404. }
  405. this.renderClassItems(Class[ClassPage.ClassProperties.Children], childrenElement);
  406. }
  407. renderMixedIn() {
  408. const mixedInElement = this.contentElements[ClassPage.TabNames.MixedIn];
  409. if(Class[ClassPage.ClassProperties.MixedIn].length == 0) {
  410. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoMixedIn }, mixedInElement);
  411. mixedInElement.addClass(ClassPage.StyleClasses.Empty);
  412. return;
  413. }
  414. this.renderClassItems(Class[ClassPage.ClassProperties.MixedIn], mixedInElement);
  415. }
  416. renderInfo() {
  417. const infoContainer = this.contentElements[ClassPage.TabNames.Info];
  418. const propertiesList = DOM.create({ tag: DOM.Tags.Div, cls: 'properties-list' }, infoContainer);
  419. this.createPropertyItem(ClassPage.__self__, { value: '' }, ClassPage.PropertyType.ClassComment, propertiesList);
  420. }
  421. createPropertyItem(name, data, type, container, isMethod, onMouseDown) {
  422. const key = data.static ? `${ClassPage.__static__}${name}` : name;
  423. const propertyItem = DOM.create({ tag: DOM.Tags.Div, cls: 'property-item', attr: { [ClassPage.Attributes.DataPropertyName]: key, [ClassPage.Attributes.DataPropertyType]: data.type }}, container).on(DOM.Events.MouseDown, (e) => {
  424. if(onMouseDown)
  425. onMouseDown(e);
  426. });
  427. if(type !== ClassPage.PropertyType.ClassComment) {
  428. const itemNameText = DOM.create({ tag: DOM.Tags.Span, innerHTML: isMethod ? 'Signature: ' : 'Name: ' });
  429. const itemNameValue = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-name-span', innerHTML: isMethod ? data.value : name });
  430. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-name', cn: [itemNameText, itemNameValue] }, propertyItem);
  431. if(type !== ClassPage.PropertyType.Dynamic && !isMethod) {
  432. const itemTypeText = DOM.create({ tag: DOM.Tags.Span, innerHTML: 'Type: ' });
  433. const itemTypeValue = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-type-span', innerHTML: data.type });
  434. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-type', cn: [itemTypeText, itemTypeValue] }, propertyItem);
  435. }
  436. if(type !== ClassPage.PropertyType.Dynamic && !isMethod && data.type !== 'undefined') {
  437. const itemValueText = DOM.create({ tag: DOM.Tags.Span, innerHTML: 'Default value: ' });
  438. const itemValueValue = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-default-value-span', innerHTML: data.type === 'string' ? `'${data.value}'` : data.value });
  439. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-default-value', cn: [itemValueText, itemValueValue] }, propertyItem);
  440. }
  441. if(type !== ClassPage.PropertyType.Dynamic && type !== ClassPage.PropertyType.Statics && type !== ClassPage.PropertyType.Base) {
  442. const itemParentText = DOM.create({ tag: DOM.Tags.Span, innerHTML: 'Nearest parent: ' });
  443. const itemParentValue = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-nearest-parent-span', innerHTML: data.nearestParent }).on('click', (e) => {
  444. Url.goTo(`/class/${data.nearestParent}`);
  445. });
  446. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-nearest-parent', cn: [itemParentText, itemParentValue] }, propertyItem);
  447. propertyItem.setAttribute(ClassPage.Attributes.DataPropertyParent, data.nearestParent);
  448. }
  449. if(type === ClassPage.PropertyType.Dynamic) {
  450. propertyItem.setAttribute(ClassPage.Attributes.DataPropertyDynamic, 'true');
  451. }
  452. }
  453. const itemCommentText = DOM.create({ tag: DOM.Tags.Div, innerHTML: 'Comment:' });
  454. const itemCommentCn = [itemCommentText];
  455. const loadedComment = Comments[type === ClassPage.PropertyType.Statics ? `${ClassPage.__static__}${name}` : name];
  456. const loadedCommentText = loadedComment && typeof loadedComment === 'object' ? loadedComment.text : '';
  457. const hasComment = loadedComment && typeof loadedComment === 'object' && loadedCommentText.length > 0;
  458. const itemCommentStatic = DOM.create({ tag: DOM.Tags.Div, cls: `property-item-comment-static${!hasComment ? ' empty' : ''}`, innerHTML: hasComment ? CDUtils.nl2br(loadedCommentText) : 'Not commented yet...' });
  459. itemCommentCn.push(itemCommentStatic);
  460. if(type === ClassPage.PropertyType.Inherited) {
  461. this.inheritedCommentsFields[`${data.nearestParent}:${name}`] = itemCommentStatic;
  462. propertyItem.setAttribute(ClassPage.Attributes.DataPropertyInherited, 'true');
  463. }
  464. this.propertyItemElements[type === ClassPage.PropertyType.Statics ? `${ClassPage.__static__}${name}` : name] = propertyItem;
  465. if(isEditor) {
  466. const itemCommentInput = DOM.create({ tag: DOM.Tags.Textarea, cls: 'property-item-comment-input hidden', attr: { 'placeholder': 'Not commented yet...'} }).setValue(CDUtils.br2nl(loadedCommentText));
  467. itemCommentCn.push(itemCommentInput);
  468. if(type === ClassPage.PropertyType.Inherited) {
  469. itemCommentInput.addClass(ClassPage.StyleClasses.Readonly).setAttribute('readonly', 'true');
  470. } else {
  471. itemCommentStatic.addClass(ClassPage.StyleClasses.Clickable);
  472. itemCommentInput
  473. .on(DOM.Events.KeyDown, this.delayedAdjustCommentInputHeight.bind(this))
  474. .on(DOM.Events.Change, this.adjustCommentInputHeight.bind(this))
  475. .on(DOM.Events.Cut, this.delayedAdjustCommentInputHeight.bind(this))
  476. .on(DOM.Events.Paste, this.delayedAdjustCommentInputHeight.bind(this))
  477. .on(DOM.Events.Drop, this.delayedAdjustCommentInputHeight.bind(this));
  478. const onCommentSave = (e) => {
  479. const commentContent = itemCommentInput.getValue();
  480. if(commentContent === CDUtils.br2nl(itemCommentStatic.getValue()) || commentContent === '' && itemCommentStatic.hasClass('empty'))
  481. return;
  482. const propertyName = `${type === ClassPage.PropertyType.Statics ? ClassPage.__static__ : ''}${name}`;
  483. const className = Class[ClassPage.ClassProperties.Name];
  484. const classRoot = Class[ClassPage.ClassProperties.Root];
  485. propertyItem.addClass('saving');
  486. itemCommentInput.blur();
  487. this.updateComment(classRoot, className, propertyName, commentContent).then((res) => {
  488. if(res.status !== 202) {
  489. propertyItem.removeClass('saving');
  490. console.error(`Comment update failed (${res.status})`);
  491. }
  492. }).catch((e) => {
  493. propertyItem.removeClass('saving');
  494. console.error(`Comment update failed`);
  495. });
  496. };
  497. const itemCommentOkButton = DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-comment-button hidden', innerHTML: 'OK' }).on(DOM.Events.Click, onCommentSave);
  498. itemCommentCn.push(itemCommentOkButton);
  499. itemCommentInput.on(DOM.Events.KeyDown, (e) => {
  500. if(e.key === DOM.Keys.Escape) {
  501. const inputScrollTop = itemCommentInput.get().scrollTop;
  502. itemCommentInput.switchClass(ClassPage.StyleClasses.Hidden);
  503. itemCommentOkButton.switchClass(ClassPage.StyleClasses.Hidden);
  504. itemCommentStatic.switchClass(ClassPage.StyleClasses.Hidden);
  505. itemCommentStatic.get().scrollTop = inputScrollTop;
  506. if(!itemCommentStatic.hasClass('empty'))
  507. itemCommentInput.setValue(CDUtils.br2nl(itemCommentStatic.getValue()));
  508. }
  509. if(e.key === DOM.Keys.Enter && !e.shiftKey) {
  510. onCommentSave();
  511. e.preventDefault();
  512. }
  513. });
  514. itemCommentStatic.on(DOM.Events.Click, (e) => {
  515. if(CDElement.get(e.target).getTag() === DOM.Tags.A)
  516. return;
  517. itemCommentInput.switchClass(ClassPage.StyleClasses.Hidden);
  518. itemCommentInput.focus();
  519. itemCommentInput.style('height', `${Math.min(422, itemCommentStatic.get().scrollHeight + 2)}px`);
  520. itemCommentInput.get().scrollTop = itemCommentStatic.get().scrollTop;
  521. itemCommentOkButton.switchClass(ClassPage.StyleClasses.Hidden);
  522. itemCommentStatic.switchClass(ClassPage.StyleClasses.Hidden);
  523. });
  524. }
  525. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-saving-filler' }, propertyItem);
  526. }
  527. if(hasComment)
  528. itemCommentCn.push(this.createCommentDateElement(loadedComment.timestamp, loadedComment.author));
  529. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-comment', cn: itemCommentCn }, propertyItem);
  530. }
  531. renderFullSourcePrompt() {
  532. const editorContent = this.contentElements[ClassPage.TabNames.Editor];
  533. const prompt = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePrompt }, editorContent);
  534. const text = this.fullSourcePromptText = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePromptText, innerHTML: ClassPage.Messages.ShowFullSourceText }, prompt);
  535. const button = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePromptButton, innerHTML: ClassPage.Messages.PromptButtonText }, prompt);
  536. const onButtonClick = (e) => {
  537. this.switchFullSource();
  538. };
  539. button.on(DOM.Events.Click, onButtonClick.bind(this));
  540. }
  541. switchFullSource(show) {
  542. const shown = this.fullSourceCodeShown = show !== undefined ? !show : !this.fullSourceCodeShown;
  543. this.codeMirrorEditor.cmSetValue(shown ? ClassSource : this.classSource);
  544. if(shown)
  545. this.findAndScrollToTargetClass();
  546. this.codeMirrorEditor.cmRefresh();
  547. this.markContentInEditor();
  548. this.fullSourcePromptText && this.fullSourcePromptText.setInnerHTML(shown ? ClassPage.Messages.HideFullSourceText : ClassPage.Messages.ShowFullSourceText);
  549. return shown;
  550. }
  551. markContentInEditor() {
  552. if(Class.isPackage)
  553. return;
  554. const staticsRange = this.getStaticsRange();
  555. this.codeMirrorEditor.cmEachLine((lineHandle) => {
  556. this.markExtend(lineHandle);
  557. this.markMixins(lineHandle);
  558. this.markZ8Locales(lineHandle);
  559. this.markNew(lineHandle);
  560. this.markThis(lineHandle);
  561. this.markProperties(lineHandle, staticsRange);
  562. });
  563. }
  564. findAndScrollToTargetClass() {
  565. const className = Class[ClassPage.ClassProperties.Name].replaceAll('.', '\\.');
  566. const editor = this.codeMirrorEditor;
  567. const defineRx = new RegExp(`Z8\\.define\\(\'${className}\',`);
  568. editor.cmEachLine((lineHandle) => {
  569. const text = lineHandle.text;
  570. const match = text.match(defineRx);
  571. if(match) {
  572. editor.scrollIntoView({ line: lineHandle.lineNo(), ch: 0 }, 100);
  573. return;
  574. }
  575. });
  576. }
  577. renderParents() {
  578. const parentsContent = this.contentElements[ClassPage.TabNames.Parents];
  579. const parentsContainer = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ParentsBranch }, parentsContent);
  580. if(Class[ClassPage.ClassProperties.ParentsBranch].length == 0) {
  581. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoParents }, parentsContent);
  582. parentsContent.addClass(ClassPage.StyleClasses.Empty);
  583. return;
  584. }
  585. this.renderClassItems(Class[ClassPage.ClassProperties.ParentsBranch], parentsContainer, true);
  586. }
  587. renderClassItems(itemsList, container, withIndent) {
  588. let indent = 0;
  589. if(!withIndent)
  590. itemsList = itemsList.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
  591. for(const cls of itemsList) {
  592. const icon = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ClassIcon });
  593. const name = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ClassName, innerHTML: cls });
  594. const cn = [icon, name];
  595. if(indent > 0 && withIndent)
  596. cn.unshift(DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.Spacing, style: `width: ${10 * indent}px;` }));
  597. DOM.create({ tag: DOM.Tags.Div, cls: `${ClassPage.StyleClasses.ClassItem}${ withIndent ? ' indent' : '' }`, attr: { [ClassPage.Attributes.DataClassName]: cls }, cn: cn }, container).on(DOM.Events.Click, this.onClassClick.bind(this));
  598. indent++;
  599. }
  600. }
  601. markExtend(lineHandle) {
  602. const editor = this.codeMirrorEditor;
  603. const text = lineHandle.text;
  604. const match = text.match(/extend:\s*['"]?([\w\.]+)['"]?/);
  605. if (match) {
  606. const className = match[1];
  607. const from = { line: lineHandle.lineNo(), ch: match.index + 8 };
  608. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  609. editor.markText(from, to, {
  610. className: ClassPage.StyleClasses.CmLink,
  611. title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`,
  612. attributes: {
  613. [ClassPage.Attributes.DataClassName]: className,
  614. [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);'
  615. }
  616. });
  617. }
  618. }
  619. markMixins(lineHandle) {
  620. const editor = this.codeMirrorEditor;
  621. const text = lineHandle.text;
  622. const match = text.match(/mixins:\s*(\[.*?\]|\w+)/);
  623. if (match) {
  624. let mixins = match[1].replace(/\[|\]/g, "").split(/\s*,\s*/);
  625. const mixinsStr = match[1].replace(/\[|\]/g, "");
  626. mixins = mixinsStr.split(/\s*,\s*/);
  627. const startIndex = match.index + match[0].indexOf(mixinsStr);
  628. for (var i = 0; i < mixins.length; i++) {
  629. const className = mixins[i].trim().replace(/^['"]|['"]$/g, "");
  630. const classIndex = mixinsStr.indexOf(className);
  631. const from = { line: lineHandle.lineNo(), ch: startIndex + classIndex };
  632. const to = { line: lineHandle.lineNo(), ch: startIndex + classIndex + className.length };
  633. editor.markText(from, to, {
  634. className: ClassPage.StyleClasses.CmLink,
  635. title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`,
  636. attributes: {
  637. [ClassPage.Attributes.DataClassName]: className,
  638. [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);'
  639. }
  640. });
  641. }
  642. }
  643. }
  644. markZ8Locales(lineHandle) {
  645. const editor = this.codeMirrorEditor;
  646. const text = lineHandle.text;
  647. const regexp = /Z8\.\$\('([\S]+)'(?:\s*,\s*.*)?\)/g;
  648. let match;
  649. while ((match = regexp.exec(text)) !== null) {
  650. const messageId = match[1];
  651. const from = { line: lineHandle.lineNo(), ch: match.index };
  652. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  653. editor.markText(from, to, {
  654. className: ClassPage.StyleClasses.Z8Locale,
  655. title: `RU: ${Z8Locales['ru'][messageId]}\nEN: ${Z8Locales['en'][messageId]}`
  656. });
  657. }
  658. }
  659. markNew(lineHandle) {
  660. const editor = this.codeMirrorEditor;
  661. const text = lineHandle.text;
  662. const regexp = /new\s+([\w\.]+)/g;
  663. let match;
  664. while ((match = regexp.exec(text)) !== null) {
  665. const className = match[1];
  666. const from = { line: lineHandle.lineNo(), ch: match.index + 4 };
  667. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  668. if(!ClassList[className] && !this.shortNameExists(className))
  669. continue;
  670. editor.markText(from, to, {
  671. className: ClassPage.StyleClasses.CmLink,
  672. title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`,
  673. attributes: {
  674. [ClassPage.Attributes.DataClassName]: className,
  675. [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);'
  676. }
  677. });
  678. }
  679. }
  680. markThis(lineHandle) {
  681. const editor = this.codeMirrorEditor;
  682. const text = lineHandle.text;
  683. const regexp = /this\.([\w]+)/g;
  684. let match;
  685. while ((match = regexp.exec(text)) !== null) {
  686. const propertyName = match[1];
  687. const from = { line: lineHandle.lineNo(), ch: match.index + 5 };
  688. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  689. const foundProperty = this.findClassProperty(propertyName);
  690. if(!foundProperty)
  691. continue;
  692. editor.markText(from, to, {
  693. className: 'cm-this-prop',
  694. title: `Ctrl+Click to go to ${foundProperty.type === 'method' ? 'method' : 'property'} '${propertyName}'`,
  695. attributes: {
  696. [ClassPage.Attributes.DataPropertyName]: propertyName,
  697. [ClassPage.Attributes.DataPropertyType]: foundProperty.type === 'method' ? 'Methods' : 'Properties',
  698. [ClassPage.Attributes.DataPropertyParent]: foundProperty.inherited ? foundProperty.nearestParent : '',
  699. [ClassPage.Attributes.OnClick]: 'window.page.onPropertyClick(this);'
  700. }
  701. });
  702. }
  703. }
  704. markProperties(lineHandle, staticsRange) {
  705. const editor = this.codeMirrorEditor;
  706. const text = lineHandle.text;
  707. const lineNo = lineHandle.lineNo();
  708. const regexp = /\t([\w]+):/g;
  709. const isStatic = lineNo >= staticsRange.from && lineNo <= staticsRange.to;
  710. let match;
  711. while ((match = regexp.exec(text)) !== null) {
  712. const propertyName = isStatic ? `${ClassPage.__static__}${match[1]}` : match[1];
  713. const from = { line: lineNo, ch: match.index + 1 };
  714. const to = { line: lineNo, ch: match.index + match[0].length - 1 };
  715. const foundProperty = this.findClassProperty(propertyName);
  716. if(!foundProperty)
  717. continue;
  718. const titlePropertyName = isStatic ? `${Class[ClassPage.ClassProperties.ShortName] || Class[ClassPage.ClassProperties.Name]}.${propertyName.replace(ClassPage.__static__, '')}` : propertyName;
  719. editor.markText(from, to, {
  720. className: 'cm-this-prop',
  721. title: `Ctrl+Click to go to ${foundProperty.type === 'method' ? 'method' : 'property'} '${titlePropertyName}'`,
  722. attributes: {
  723. [ClassPage.Attributes.DataPropertyName]: propertyName,
  724. [ClassPage.Attributes.DataPropertyType]: foundProperty.type === 'method' ? 'Methods' : 'Properties',
  725. [ClassPage.Attributes.DataPropertyParent]: foundProperty.inherited && !foundProperty.overridden ? foundProperty.nearestParent : '',
  726. [ClassPage.Attributes.OnClick]: 'window.page.onPropertyClick(this);'
  727. }
  728. });
  729. }
  730. }
  731. getStaticsRange() {
  732. const range = { from: -1, to: -1 };
  733. let staticsParenthesisFlag = -1;
  734. let found = false;
  735. this.codeMirrorEditor.cmEachLine((lineHandle) => {
  736. if(found)
  737. return;
  738. const text = lineHandle.text;
  739. let ignoreFirstParenthesis = false;
  740. if(staticsParenthesisFlag === -1 && text.match(/statics:\s*\{/)) {
  741. staticsParenthesisFlag = 1;
  742. ignoreFirstParenthesis = true;
  743. range.from = lineHandle.lineNo();
  744. }
  745. if(staticsParenthesisFlag > 0) {
  746. for(let i = 0; i < text.length; i++) {
  747. if(text.charAt(i) === '{' && ignoreFirstParenthesis)
  748. ignoreFirstParenthesis = false;
  749. else if (text.charAt(i) === '{')
  750. staticsParenthesisFlag++;
  751. else if(text.charAt(i) === '}')
  752. staticsParenthesisFlag--;
  753. }
  754. }
  755. if(staticsParenthesisFlag === 0) {
  756. range.to = lineHandle.lineNo();
  757. found = true;
  758. }
  759. });
  760. return range;
  761. }
  762. shortNameExists(shortName) {
  763. return Object.keys(ClassList).map((key) => ClassList[key]).filter((item) => item[ClassPage.ClassProperties.ShortName] === shortName).length > 0;
  764. }
  765. findClassProperty(propertyName) {
  766. if(propertyName.startsWith(ClassPage.__static__)) {
  767. propertyName = propertyName.slice(10);
  768. const statics = Class[ClassPage.ClassProperties.Statics];
  769. const foundStatic = statics.filter((prop) => prop.key === propertyName)[0];
  770. return foundStatic;
  771. }
  772. const dynamicProperties = Class[ClassPage.ClassProperties.DynamicProperties];
  773. const properties = Class[ClassPage.ClassProperties.Properties];
  774. const foundDynamic = dynamicProperties.filter((prop) => prop.key === propertyName)[0];
  775. const foundProperty = properties.filter((prop) => prop.key === propertyName)[0];
  776. return foundDynamic || foundProperty;
  777. }
  778. onClassLinkClick(fragment) {
  779. const codeMirrorEditorElement = this.codeMirrorEditorElement;
  780. if(codeMirrorEditorElement.hasClass(ClassPage.StyleClasses.CtrlPressed))
  781. Url.goTo(`/class/${fragment.getAttribute(ClassPage.Attributes.DataClassName)}`, codeMirrorEditorElement.hasClass(ClassPage.StyleClasses.ShiftPressed));
  782. }
  783. onPropertyClick(fragment) {
  784. const codeMirrorEditorElement = this.codeMirrorEditorElement;
  785. if(codeMirrorEditorElement.hasClass(ClassPage.StyleClasses.CtrlPressed)) {
  786. const parentClassName = fragment.getAttribute(ClassPage.Attributes.DataPropertyParent);
  787. const propertyType = fragment.getAttribute(ClassPage.Attributes.DataPropertyType);
  788. const propertyName = fragment.getAttribute(ClassPage.Attributes.DataPropertyName);
  789. if(parentClassName.length > 0)
  790. Url.goTo(`/class/${parentClassName}#${propertyType}:${propertyName}`);
  791. else
  792. Url.setHash(`${propertyType}:${propertyName}`).updateLocation();
  793. }
  794. }
  795. onTabClick(e) {
  796. const element = CDElement.get(e.target);
  797. if(element.hasClass(ClassPage.StyleClasses.Selected))
  798. return;
  799. this.openTab(element.getAttribute(ClassPage.Attributes.DataTab));
  800. }
  801. openTab(tabName) {
  802. this.selectTab(tabName);
  803. this.activateContent(tabName);
  804. Url.setHash(tabName).updateLocation();
  805. }
  806. onClassClick(e) {
  807. let element = CDElement.get(e.target);
  808. while(!element.hasClass(ClassPage.StyleClasses.ClassItem))
  809. element = element.getParent();
  810. Url.goTo(`/class/${element.getAttribute(ClassPage.Attributes.DataClassName)}`);
  811. }
  812. onModeButtonClick(e) {
  813. const button = CDElement.get(e.target);
  814. const mode = button.getAttribute(ClassPage.Attributes.DataDisplayMode);
  815. this.switchMode(mode);
  816. button.addClass(ClassPage.StyleClasses.Selected);
  817. (mode === ClassPage.Mode.Tabs ? this.listModeButton : this.tabsModeButton).removeClass(ClassPage.StyleClasses.Selected);
  818. }
  819. searchInEditor(isStatic, ...queries) {
  820. this.switchFullSource(true);
  821. const editor = this.codeMirrorEditor;
  822. const staticsRange = this.getStaticsRange();
  823. const staticsStart = staticsRange.from;
  824. const staticsEnd = staticsRange.to;
  825. for(const query of queries) {
  826. const cursor = editor.getSearchCursor(query, CodeMirror.Pos(editor.cmFirstLine(), 0), { caseFold: false, multiline: true });
  827. while(cursor.find()) {
  828. const from = cursor.from();
  829. const to = cursor.to();
  830. const lineIndex = from.line;
  831. if((isStatic && lineIndex >= staticsStart && lineIndex <= staticsEnd) || (!isStatic && (lineIndex < staticsStart || lineIndex > staticsEnd))) {
  832. this.openTab('Editor');
  833. editor.setSelection(from, to);
  834. editor.scrollIntoView({ from: from, to: to }, 100);
  835. return;
  836. }
  837. }
  838. }
  839. }
  840. searchPropertyInEditor(isMethod, isDynamic, propertyName) {
  841. const isStatic = propertyName.startsWith(ClassPage.__static__);
  842. propertyName = isStatic ? propertyName.slice(10) : propertyName;
  843. if(isMethod) {
  844. this.searchInEditor(isStatic, `${propertyName}: function`);
  845. } else {
  846. if(isDynamic)
  847. this.searchInEditor(false, `this.${propertyName} =`, `this.${propertyName}`);
  848. else
  849. this.searchInEditor(isStatic, `${propertyName}: `);
  850. }
  851. }
  852. scrollToProperty(hashTab, hashProp) {
  853. if(!hashProp)
  854. return;
  855. const item = this.contentElements[hashTab].getFirstChild(`.property-item[data-property-name="${hashProp}"]`);
  856. if(!item)
  857. return;
  858. const categoryList = item.getParent();
  859. const categoryHeader = categoryList.previousSibling();
  860. if(categoryList.hasClass(ClassPage.StyleClasses.Hidden))
  861. categoryList.removeClass(ClassPage.StyleClasses.Hidden);
  862. if(categoryHeader.hasClass(ClassPage.StyleClasses.Collapsed))
  863. categoryHeader.removeClass(ClassPage.StyleClasses.Collapsed);
  864. item.addClass(ClassPage.StyleClasses.Highlighted);
  865. item.addClass(ClassPage.StyleClasses.White);
  866. if(this.mode === ClassPage.Mode.Tabs)
  867. this.contentElements[hashTab].scrollTo(`.property-item[data-property-name="${hashProp}"]`);
  868. else
  869. item.scrollIntoView();
  870. setTimeout(() => {
  871. item.removeClass(ClassPage.StyleClasses.White);
  872. }, 1000);
  873. setTimeout(() => {
  874. item.removeClass(ClassPage.StyleClasses.Highlighted);
  875. }, 2000);
  876. }
  877. delayedAdjustCommentInputHeight(e) {
  878. setTimeout(() => { this.adjustCommentInputHeight(e) }, 0);
  879. }
  880. adjustCommentInputHeight(e) {
  881. const textArea = e.target;
  882. if(textArea.scrollHeight < 422 || e.key === DOM.Keys.Backspace) {
  883. textArea.style.height = 'auto';
  884. textArea.style.height = `${Math.min(422, textArea.scrollHeight + 2)}px`;
  885. }
  886. textArea.scrollTop = textArea.scrollHeight;
  887. }
  888. applyHash() {
  889. const hash = (Url.getHash() || '').split(':');
  890. const hashTab = hash[0];
  891. const hashProp = hash[1];
  892. const tabElements = this.tabElements;
  893. const contentElements = this.contentElements;
  894. const selectedTab = tabElements[hashTab] || (Class.isPackage ? tabElements[ClassPage.TabNames.Info] : tabElements[ClassPage.TabNames.Editor]);
  895. const activeContent = contentElements[hashTab] || (Class.isPackage ? contentElements[ClassPage.TabNames.Info] : contentElements[ClassPage.TabNames.Editor]);
  896. this.selectTab(selectedTab);
  897. this.activateContent(activeContent);
  898. this.scrollToProperty(hashTab, hashProp);
  899. }
  900. openSocket() {
  901. this.socket = new Socket('/ws').onMessage(this.onSocketMessage.bind(this));
  902. }
  903. onSocketMessage(e) {
  904. const changes = JSON.parse(e.data) || [];
  905. changes.forEach((changedComment) => {
  906. this.processChange(changedComment);
  907. });
  908. if(this.statistics)
  909. this.statistics.load();
  910. }
  911. processChange(changedComment) {
  912. if(changedComment.root !== Class[ClassPage.ClassProperties.Root] || changedComment.className !== Class[ClassPage.ClassProperties.Name])
  913. return;
  914. const propertyItem = this.propertyItemElements[changedComment.propertyName];
  915. propertyItem.removeClass('saving');
  916. switch(changedComment.action) {
  917. case 'create':
  918. this.documented++;
  919. this.renderDocumentedPercentage();
  920. Comments[changedComment.propertyName] = changedComment;
  921. propertyItem.getFirstChild('.property-item-comment').append(this.createCommentDateElement(changedComment.timestamp, changedComment.author));
  922. break;
  923. case 'update':
  924. if(Comments[changedComment.propertyName].text.length === 0) {
  925. this.documented++;
  926. this.renderDocumentedPercentage();
  927. }
  928. Comments[changedComment.propertyName].text = changedComment.text;
  929. const dateElement = propertyItem.getFirstChild('.property-item-comment-date > .property-item-comment-date-date');
  930. if(dateElement) {
  931. dateElement.setInnerHTML(CDUtils.dateFormatUTC(changedComment.timestamp, 3, 'D.M.Y, H:I:S'));
  932. } else {
  933. propertyItem.getFirstChild('.property-item-comment').append(this.createCommentDateElement(changedComment.timestamp, changedComment.author));
  934. }
  935. break;
  936. case 'remove':
  937. this.documented--;
  938. if(Comments[changedComment.propertyName])
  939. Comments[changedComment.propertyName].text = '';
  940. this.renderDocumentedPercentage();
  941. propertyItem.getFirstChild('.property-item-comment-date').remove();
  942. break;
  943. }
  944. const commentContent = changedComment.text;
  945. const itemCommentStatic = propertyItem.getFirstChild('.property-item-comment-static');
  946. const itemCommentInput = propertyItem.getFirstChild('.property-item-comment-input');
  947. const itemCommentOkButton = propertyItem.getFirstChild('.property-item-comment-button');
  948. if(itemCommentInput) {
  949. itemCommentInput.setValue(commentContent);
  950. itemCommentInput.addClass(ClassPage.StyleClasses.Hidden);
  951. itemCommentOkButton.addClass(ClassPage.StyleClasses.Hidden);
  952. }
  953. itemCommentStatic.setInnerHTML(commentContent.length > 0 ? CDUtils.nl2br(commentContent) : 'Not commented yet...');
  954. itemCommentStatic.switchClass(ClassPage.StyleClasses.Empty, commentContent.length === 0);
  955. itemCommentStatic.removeClass(ClassPage.StyleClasses.Hidden);
  956. }
  957. renderContribution() {
  958. const contributionElement = this.contentElements[ClassPage.TabNames.Contribution];
  959. this.statistics = Statistics.init(contributionElement, Statistics.Modes.CLASS);
  960. }
  961. /* >>> Context menu */
  962. showContextMenu(target, pos) {
  963. while(!target.hasClass('property-item'))
  964. target = target.getParent();
  965. const contextMenu = this.contextMenu;
  966. const propertyItemName = target.getAttribute(ClassPage.Attributes.DataPropertyName);
  967. const propertyItemType = target.getAttribute(ClassPage.Attributes.DataPropertyType);
  968. const propertyItemParent = target.getAttribute(ClassPage.Attributes.DataPropertyParent);
  969. const propertyItemDynamic = target.getAttribute(ClassPage.Attributes.DataPropertyDynamic);
  970. const propertyItemInhertied = target.getAttribute(ClassPage.Attributes.DataPropertyInherited);
  971. const propertyVisualName = target.getFirstChild('.property-item-name-span').getValue();
  972. if(propertyItemInhertied !== 'true') {
  973. contextMenu.addItem('ShowInEditor', 'Show in Editor', () => {
  974. this.searchPropertyInEditor(propertyItemType === 'method', propertyItemDynamic === 'true', propertyItemName);
  975. });
  976. }
  977. if(propertyItemParent != null) {
  978. contextMenu.addItem('MoveToParent', 'Move to parent', () => {
  979. Url.goTo(`/class/${propertyItemParent}#${propertyItemType === 'method' ? 'Methods' : 'Properties'}:${propertyItemName}`);
  980. });
  981. }
  982. if(isEditor && !target.getFirstChild('.property-item-comment-static').hasClass(ClassPage.StyleClasses.Hidden)) {
  983. contextMenu.addDelimiter();
  984. contextMenu.addItem('EditComment', 'Edit comment', () => {
  985. target.getFirstChild('.property-item-comment-static').click();
  986. target.getFirstChild('.property-item-comment-input').focus();
  987. });
  988. }
  989. contextMenu.addDelimiter();
  990. contextMenu.addItem('CopyLink', 'Copy link', () => {
  991. DOM.copyToClipboard(`${Url.getFullPath()}#${propertyItemType === 'method' ? 'Methods' : 'Properties'}:${propertyItemName}`);
  992. });
  993. contextMenu.addDelimiter();
  994. contextMenu.addItem('CopyHtmlLink', 'Copy HTML link', () => {
  995. let linkText = propertyVisualName;
  996. if(propertyItemName.startsWith(ClassPage.__static__))
  997. linkText = `${Class[ClassPage.ClassProperties.ShortName] || Class[ClassPage.ClassProperties.Name]}.${propertyVisualName}`;
  998. DOM.copyToClipboard(`<a href="${Url.getPath()}#${propertyItemType === 'method' ? 'Methods' : 'Properties'}:${propertyItemName}">${linkText}</a>`);
  999. });
  1000. contextMenu.show(pos);
  1001. }
  1002. /* <<< Context menu */
  1003. loadDocumentedInfo() {
  1004. let requestBody;
  1005. if(Class.isPackage) {
  1006. requestBody = new URLSearchParams({
  1007. 'root': Class[ClassPage.ClassProperties.Root],
  1008. 'packageName': Class[ClassPage.ClassProperties.Name]
  1009. });
  1010. } else {
  1011. requestBody = new URLSearchParams({
  1012. 'className': Class[ClassPage.ClassProperties.Name]
  1013. });
  1014. }
  1015. fetch(`/docinfo`, {
  1016. method: 'POST',
  1017. headers: {
  1018. 'Content-Type': 'application/x-www-form-urlencoded'
  1019. },
  1020. body: requestBody
  1021. }).then(res => res.json()).then((res) => {
  1022. this.documented = res['documented'];
  1023. this.documentable = res['documentable'];
  1024. this.renderDocumentedPercentage();
  1025. });
  1026. }
  1027. };
  1028. window_.on(DOM.Events.Load, (e) => {
  1029. window.page = new ClassPage().start();
  1030. });
  1031. window_.on(DOM.Events.KeyDown, (e) => {
  1032. if(Class.isPackage)
  1033. return;
  1034. if(window.page && (e.key === DOM.Keys.Control || e.key === DOM.Keys.Meta))
  1035. window.page.codeMirrorEditorElement.addClass(ClassPage.StyleClasses.CtrlPressed);
  1036. if(window.page && e.key === DOM.Keys.Shift)
  1037. window.page.codeMirrorEditorElement.addClass(ClassPage.StyleClasses.ShiftPressed);
  1038. });
  1039. window_.on(DOM.Events.KeyUp, (e) => {
  1040. if(Class.isPackage)
  1041. return;
  1042. if(window.page && (e.key === DOM.Keys.Control || e.key === DOM.Keys.Meta))
  1043. window.page.codeMirrorEditorElement.removeClass(ClassPage.StyleClasses.CtrlPressed);
  1044. if(window.page && e.key === DOM.Keys.Shift)
  1045. window.page.codeMirrorEditorElement.removeClass(ClassPage.StyleClasses.ShiftPressed);
  1046. });
  1047. window_.on(DOM.Events.HashChange, (e) => {
  1048. if(window.page)
  1049. window.page.applyHash();
  1050. });