Skip to content

Commit c573cb5

Browse files
authored
Merge pull request #758 from ZenUml/feat/project-management
Feat/pages
2 parents 744eb8d + 81a8f69 commit c573cb5

File tree

9 files changed

+709
-73
lines changed

9 files changed

+709
-73
lines changed

src/components/ContentWrap.jsx

Lines changed: 166 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { saveAs } from 'file-saver';
33
import UserCodeMirror from './UserCodeMirror.jsx';
44
import Toolbox from './Toolbox.jsx';
55
import Tabs from './Tabs.jsx';
6+
import PageTabs from './PageTabs.jsx';
67
import { computeCss, computeHtml, computeJs } from '../computes';
78
import { CssModes, HtmlModes, JsModes, modes } from '../codeModes';
89
import { getCompleteHtml, loadJS, log } from '../utils';
@@ -41,7 +42,15 @@ export default class ContentWrap extends Component {
4142
this.jsMode = JsModes.JS;
4243
this.prefs = {};
4344
this.codeInPreview = { html: null, css: null, js: null };
44-
this.cmCodes = { html: props.currentItem.html, css: '', js: '' };
45+
46+
// Initialize with the current page's content if available
47+
const currentPage = this.getCurrentPage();
48+
this.cmCodes = {
49+
html: props.currentItem.html,
50+
css: currentPage ? currentPage.css : props.currentItem.css || '',
51+
js: currentPage ? currentPage.js : props.currentItem.js || ''
52+
};
53+
4554
this.cm = {};
4655
this.logCount = 0;
4756

@@ -94,31 +103,90 @@ export default class ContentWrap extends Component {
94103
this.onCodeChange(editor, change);
95104
}
96105

97-
onCssCodeChange(editor, change) {
98-
this.cmCodes.css = editor.getValue();
99-
this.props.onCodeChange(
100-
'css',
101-
this.cmCodes.css,
102-
change.origin !== 'setValue',
103-
);
104-
this.onCodeChange(editor, change);
105-
}
106-
107106
async onJsCodeChange(editor, change) {
108107
await this.setState({ lineOfCode: editor.doc.size });
109108
this.cmCodes.js = editor.getValue();
110-
this.props.onCodeChange(
111-
'js',
112-
this.cmCodes.js,
113-
change.origin !== 'setValue',
114-
);
109+
110+
// Update the current page's JS content
111+
const currentPage = this.getCurrentPage();
112+
if (currentPage) {
113+
const updatedPage = {
114+
...currentPage,
115+
js: editor.getValue()
116+
};
117+
118+
// Find the index of the current page
119+
const pageIndex = this.props.currentItem.pages.findIndex(
120+
page => page.id === currentPage.id
121+
);
122+
123+
if (pageIndex !== -1) {
124+
// Create updated pages array
125+
const updatedPages = [...this.props.currentItem.pages];
126+
updatedPages[pageIndex] = updatedPage;
127+
128+
// Update the current item with the new pages array
129+
const updatedItem = {
130+
...this.props.currentItem,
131+
pages: updatedPages,
132+
// Also update the js field for backward compatibility
133+
js: editor.getValue()
134+
};
135+
136+
// Only call onCodeChange once with the updated item
137+
this.props.onCodeChange('js', editor.getValue(), change.origin !== 'setValue', updatedItem);
138+
} else {
139+
this.props.onCodeChange('js', editor.getValue(), change.origin !== 'setValue');
140+
}
141+
} else {
142+
this.props.onCodeChange('js', editor.getValue(), change.origin !== 'setValue');
143+
}
115144

116145
const targetWindow =
117146
this.detachedWindow ||
118147
document.getElementById('demo-frame').contentWindow;
119148
targetWindow.postMessage({ code: this.cmCodes.js }, '*');
120149
}
121150

151+
async onCssCodeChange(editor, change) {
152+
this.cmCodes.css = editor.getValue();
153+
154+
// Update the current page's CSS content
155+
const currentPage = this.getCurrentPage();
156+
if (currentPage) {
157+
const updatedPage = {
158+
...currentPage,
159+
css: editor.getValue()
160+
};
161+
162+
// Find the index of the current page
163+
const pageIndex = this.props.currentItem.pages.findIndex(
164+
page => page.id === currentPage.id
165+
);
166+
167+
if (pageIndex !== -1) {
168+
// Create updated pages array
169+
const updatedPages = [...this.props.currentItem.pages];
170+
updatedPages[pageIndex] = updatedPage;
171+
172+
// Update the current item with the new pages array
173+
const updatedItem = {
174+
...this.props.currentItem,
175+
pages: updatedPages,
176+
// Also update the css field for backward compatibility
177+
css: editor.getValue()
178+
};
179+
180+
// Only call onCodeChange once with the updated item
181+
this.props.onCodeChange('css', editor.getValue(), change.origin !== 'setValue', updatedItem);
182+
} else {
183+
this.props.onCodeChange('css', editor.getValue(), change.origin !== 'setValue');
184+
}
185+
} else {
186+
this.props.onCodeChange('css', editor.getValue(), change.origin !== 'setValue');
187+
}
188+
}
189+
122190
onCursorMove(editor) {
123191
const cursor = editor.getCursor();
124192
const line = cursor.line;
@@ -159,6 +227,19 @@ export default class ContentWrap extends Component {
159227
}, this.updateDelay);
160228
}
161229

230+
/**
231+
* Gets the current active page from the current item
232+
* @returns {Object|null} The current page or null if not found
233+
*/
234+
getCurrentPage() {
235+
const { currentItem } = this.props;
236+
if (!currentItem || !currentItem.pages || !currentItem.currentPageId) {
237+
return null;
238+
}
239+
240+
return currentItem.pages.find(page => page.id === currentItem.currentPageId) || null;
241+
}
242+
162243
// Called for both detached window and non-detached window
163244
async createPreviewFile(html, css, js) {
164245
// isNotChrome
@@ -303,10 +384,18 @@ export default class ContentWrap extends Component {
303384
}
304385

305386
refreshEditor() {
306-
this.cmCodes.css = this.props.currentItem.css;
307-
this.cmCodes.js = this.props.currentItem.js;
308-
this.cm.css.setValue(this.cmCodes.css || '');
309-
this.cm.js.setValue(this.cmCodes.js || '');
387+
const currentPage = this.getCurrentPage();
388+
389+
if (currentPage) {
390+
this.cmCodes.css = currentPage.css || '';
391+
this.cmCodes.js = currentPage.js || '';
392+
} else {
393+
this.cmCodes.css = this.props.currentItem.css || '';
394+
this.cmCodes.js = this.props.currentItem.js || '';
395+
}
396+
397+
this.cm.css.setValue(this.cmCodes.css);
398+
this.cm.js.setValue(this.cmCodes.js);
310399
this.cm.css.refresh();
311400
this.cm.js.refresh();
312401

@@ -558,6 +647,13 @@ export default class ContentWrap extends Component {
558647
const baseTranspilerPath = 'lib/transpilers';
559648
// Exit if already loaded
560649
var d = deferred();
650+
651+
// Add null check for modes[mode]
652+
if (!mode || !modes[mode]) {
653+
d.resolve();
654+
return d.promise;
655+
}
656+
561657
if (modes[mode].hasLoaded) {
562658
d.resolve();
563659
return d.promise;
@@ -599,36 +695,57 @@ export default class ContentWrap extends Component {
599695
updateHtmlMode(value) {
600696
this.props.onCodeModeChange('html', value);
601697
this.props.currentItem.htmlMode = value;
602-
CodeMirror.autoLoadMode(
603-
this.cm.html,
604-
modes[value].cmPath || modes[value].cmMode,
605-
);
698+
699+
// Add null check to prevent "Cannot read properties of undefined (reading 'cmPath')" error
700+
if (this.cm && this.cm.html && modes[value]) {
701+
CodeMirror.autoLoadMode(
702+
this.cm.html,
703+
modes[value].cmPath || modes[value].cmMode,
704+
);
705+
}
706+
606707
return this.handleModeRequirements(value);
607708
}
608709

609710
updateCssMode(value) {
610711
this.props.onCodeModeChange('css', value);
611712
this.props.currentItem.cssMode = value;
612-
this.cm.css.setOption('mode', modes[value].cmMode);
613-
this.cm.css.setOption('readOnly', modes[value].cmDisable);
614-
window.cssSettingsBtn.classList[
615-
modes[value].hasSettings ? 'remove' : 'add'
616-
]('hide');
617-
CodeMirror.autoLoadMode(
618-
this.cm.css,
619-
modes[value].cmPath || modes[value].cmMode,
620-
);
713+
714+
// Add null check to prevent "Cannot read properties of undefined" error
715+
if (this.cm && this.cm.css && modes[value]) {
716+
this.cm.css.setOption('mode', modes[value].cmMode);
717+
this.cm.css.setOption('readOnly', modes[value].cmDisable);
718+
719+
CodeMirror.autoLoadMode(
720+
this.cm.css,
721+
modes[value].cmPath || modes[value].cmMode,
722+
);
723+
}
724+
725+
// Only modify DOM if the element exists
726+
if (window.cssSettingsBtn && modes[value]) {
727+
window.cssSettingsBtn.classList[
728+
modes[value].hasSettings ? 'remove' : 'add'
729+
]('hide');
730+
}
731+
621732
return this.handleModeRequirements(value);
622733
}
623734

624735
updateJsMode(value) {
625736
this.props.onCodeModeChange('js', value);
626737
this.props.currentItem.jsMode = value;
627-
this.cm.js.setOption('mode', modes[value].cmMode);
628-
CodeMirror.autoLoadMode(
629-
this.cm.js,
630-
modes[value].cmPath || modes[value].cmMode,
631-
);
738+
739+
// Add null check to prevent "Cannot read properties of undefined" error
740+
if (this.cm && this.cm.js && modes[value]) {
741+
this.cm.js.setOption('mode', modes[value].cmMode);
742+
743+
CodeMirror.autoLoadMode(
744+
this.cm.js,
745+
modes[value].cmPath || modes[value].cmMode,
746+
);
747+
}
748+
632749
return this.handleModeRequirements(value);
633750
}
634751

@@ -899,17 +1016,15 @@ export default class ContentWrap extends Component {
8991016
<UserCodeMirror
9001017
ref={(dslEditor) => (this.dslEditor = dslEditor)}
9011018
options={{
902-
mode: 'htmlmixed',
903-
profile: 'xhtml',
1019+
mode: 'javascript',
9041020
gutters: [
9051021
'CodeMirror-linenumbers',
9061022
'CodeMirror-foldgutter',
9071023
],
9081024
noAutocomplete: true,
909-
matchTags: { bothTags: true },
9101025
prettier: true,
911-
prettierParser: 'html',
912-
emmet: true,
1026+
prettierParser: 'babel',
1027+
emmet: false,
9131028
}}
9141029
prefs={this.props.prefs}
9151030
autoComplete={this.props.prefs.autoComplete}
@@ -995,6 +1110,14 @@ export default class ContentWrap extends Component {
9951110
</div>
9961111
<div class="demo-side" id="js-demo-side">
9971112
<div className="h-full flex flex-col">
1113+
{this.props.currentItem && this.props.currentItem.pages && this.props.currentItem.pages.length > 0 && (
1114+
<PageTabs
1115+
pages={this.props.currentItem.pages}
1116+
currentPageId={this.props.currentItem.currentPageId}
1117+
onTabClick={this.props.onPageSwitch}
1118+
onAddPage={this.props.onAddPage}
1119+
/>
1120+
)}
9981121
<div
9991122
className="flex-grow"
10001123
style="overflow-y: auto; -webkit-overflow-scrolling: touch; "

src/components/MainHeader.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ export function MainHeader(props) {
158158
<svg className="h-5 w-5">
159159
<use xlinkHref="#icon-pen" />
160160
</svg>
161+
{props.currentItem && props.currentItem.pages && (
162+
<span className="ml-2 px-2 py-0.5 text-xs rounded-full bg-black-600 text-gray-300">
163+
{props.currentItem.pages.length} {props.currentItem.pages.length === 1 ? 'page' : 'pages'}
164+
</span>
165+
)}
161166
</div>
162167
)}
163168
</div>

src/components/PageTabs.jsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'preact';
2+
3+
/**
4+
* PageTabs component displays tabs for each page and handles tab switching
5+
*
6+
* @param {Object} props - Component props
7+
* @param {Array} props.pages - Array of page objects
8+
* @param {String} props.currentPageId - ID of the currently active page
9+
* @param {Function} props.onTabClick - Callback function when a tab is clicked
10+
* @param {Function} props.onAddPage - Callback function when the add page button is clicked
11+
*/
12+
export function PageTabs({ pages, currentPageId, onTabClick, onAddPage }) {
13+
if (!pages || pages.length === 0) {
14+
return null;
15+
}
16+
17+
/**
18+
* Wrapper for the onAddPage callback to properly handle the event object.
19+
* This prevents the synthetic event from being passed to the App's addNewPage method,
20+
* which expects either no parameters or a title string, not an event object.
21+
* Without this wrapper, the event object would cause errors when passed to addNewPage.
22+
*/
23+
const handleAddPage = (e) => {
24+
e.preventDefault();
25+
e.stopPropagation();
26+
if (typeof onAddPage === 'function') {
27+
onAddPage();
28+
}
29+
};
30+
31+
return (
32+
<div className="page-tabs bg-black-500 border-b border-black-700 px-2 py-1 flex overflow-x-auto items-center">
33+
{pages.map(page => (
34+
<button
35+
key={page.id}
36+
className={`px-4 py-2 mx-1 rounded-t-lg text-sm font-medium transition-colors duration-200 ${
37+
page.id === currentPageId
38+
? 'bg-primary text-white'
39+
: 'bg-black-600 text-gray-400 hover:bg-black-700 hover:text-gray-300'
40+
}`}
41+
onClick={() => onTabClick(page.id)}
42+
>
43+
{page.title || 'Untitled'}
44+
</button>
45+
))}
46+
<button
47+
className="ml-2 px-3 py-2 bg-black-600 text-gray-400 hover:bg-black-700 hover:text-gray-300 rounded-lg text-sm font-medium transition-colors duration-200 flex items-center"
48+
onClick={handleAddPage}
49+
title="Add new page"
50+
>
51+
<span className="mr-1 font-bold text-lg leading-none">+</span>
52+
Add Page
53+
</button>
54+
</div>
55+
);
56+
}
57+
58+
export default PageTabs;

0 commit comments

Comments
 (0)