script.js 46 KB

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