script.js 43 KB

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