script.js 44 KB

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