script.js 46 KB

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