Skip to content

Commit 0a73b70

Browse files
authored
Merge pull request #5725 from BookStackApp/md_plaintext
MarkDown Editor: TypeScript Conversion & Plaintext Editor
2 parents 61f8d18 + 2668aae commit 0a73b70

33 files changed

+1457
-881
lines changed

dev/build/esbuild.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const entryPoints = {
1313
app: path.join(__dirname, '../../resources/js/app.ts'),
1414
code: path.join(__dirname, '../../resources/js/code/index.mjs'),
1515
'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'),
16-
markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'),
16+
markdown: path.join(__dirname, '../../resources/js/markdown/index.mts'),
1717
wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.ts'),
1818
};
1919

lang/en/entities.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@
268268
'pages_md_insert_drawing' => 'Insert Drawing',
269269
'pages_md_show_preview' => 'Show preview',
270270
'pages_md_sync_scroll' => 'Sync preview scroll',
271+
'pages_md_plain_editor' => 'Plaintext editor',
271272
'pages_drawing_unsaved' => 'Unsaved Drawing Found',
272273
'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',
273274
'pages_not_in_chapter' => 'Page is not in a chapter',

package-lock.json

Lines changed: 392 additions & 325 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"devDependencies": {
2222
"@eslint/js": "^9.21.0",
2323
"@lezer/generator": "^1.7.2",
24+
"@types/markdown-it": "^14.1.2",
2425
"@types/sortablejs": "^1.15.8",
2526
"chokidar-cli": "^3.0",
2627
"esbuild": "^0.25.0",

resources/js/components/entity-selector-popup.js renamed to resources/js/components/entity-selector-popup.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
11
import {Component} from './component';
2+
import {EntitySelector, EntitySelectorEntity, EntitySelectorSearchOptions} from "./entity-selector";
3+
import {Popup} from "./popup";
4+
5+
export type EntitySelectorPopupCallback = (entity: EntitySelectorEntity) => void;
26

37
export class EntitySelectorPopup extends Component {
48

9+
protected container!: HTMLElement;
10+
protected selectButton!: HTMLElement;
11+
protected selectorEl!: HTMLElement;
12+
13+
protected callback: EntitySelectorPopupCallback|null = null;
14+
protected selection: EntitySelectorEntity|null = null;
15+
516
setup() {
617
this.container = this.$el;
718
this.selectButton = this.$refs.select;
819
this.selectorEl = this.$refs.selector;
920

10-
this.callback = null;
11-
this.selection = null;
12-
1321
this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));
1422
window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));
1523
window.$events.listen('entity-select-confirm', this.handleConfirmedSelection.bind(this));
1624
}
1725

1826
/**
1927
* Show the selector popup.
20-
* @param {Function} callback
21-
* @param {EntitySelectorSearchOptions} searchOptions
2228
*/
23-
show(callback, searchOptions = {}) {
29+
show(callback: EntitySelectorPopupCallback, searchOptions: Partial<EntitySelectorSearchOptions> = {}) {
2430
this.callback = callback;
2531
this.getSelector().configureSearchOptions(searchOptions);
2632
this.getPopup().show();
@@ -32,34 +38,28 @@ export class EntitySelectorPopup extends Component {
3238
this.getPopup().hide();
3339
}
3440

35-
/**
36-
* @returns {Popup}
37-
*/
38-
getPopup() {
39-
return window.$components.firstOnElement(this.container, 'popup');
41+
getPopup(): Popup {
42+
return window.$components.firstOnElement(this.container, 'popup') as Popup;
4043
}
4144

42-
/**
43-
* @returns {EntitySelector}
44-
*/
45-
getSelector() {
46-
return window.$components.firstOnElement(this.selectorEl, 'entity-selector');
45+
getSelector(): EntitySelector {
46+
return window.$components.firstOnElement(this.selectorEl, 'entity-selector') as EntitySelector;
4747
}
4848

4949
onSelectButtonClick() {
5050
this.handleConfirmedSelection(this.selection);
5151
}
5252

53-
onSelectionChange(entity) {
54-
this.selection = entity;
55-
if (entity === null) {
53+
onSelectionChange(entity: EntitySelectorEntity|{}) {
54+
this.selection = (entity.hasOwnProperty('id') ? entity : null) as EntitySelectorEntity|null;
55+
if (!this.selection) {
5656
this.selectButton.setAttribute('disabled', 'true');
5757
} else {
5858
this.selectButton.removeAttribute('disabled');
5959
}
6060
}
6161

62-
handleConfirmedSelection(entity) {
62+
handleConfirmedSelection(entity: EntitySelectorEntity|null): void {
6363
this.hide();
6464
this.getSelector().reset();
6565
if (this.callback && entity) this.callback(entity);

resources/js/components/entity-selector.js renamed to resources/js/components/entity-selector.ts

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
1-
import {onChildEvent} from '../services/dom.ts';
1+
import {onChildEvent} from '../services/dom';
22
import {Component} from './component';
33

4-
/**
5-
* @typedef EntitySelectorSearchOptions
6-
* @property entityTypes string
7-
* @property entityPermission string
8-
* @property searchEndpoint string
9-
* @property initialValue string
10-
*/
11-
12-
/**
13-
* Entity Selector
14-
*/
4+
export interface EntitySelectorSearchOptions {
5+
entityTypes: string;
6+
entityPermission: string;
7+
searchEndpoint: string;
8+
initialValue: string;
9+
}
10+
11+
export type EntitySelectorEntity = {
12+
id: number,
13+
name: string,
14+
link: string,
15+
};
16+
1517
export class EntitySelector extends Component {
18+
protected elem!: HTMLElement;
19+
protected input!: HTMLInputElement;
20+
protected searchInput!: HTMLInputElement;
21+
protected loading!: HTMLElement;
22+
protected resultsContainer!: HTMLElement;
23+
24+
protected searchOptions!: EntitySelectorSearchOptions;
25+
26+
protected search = '';
27+
protected lastClick = 0;
1628

1729
setup() {
1830
this.elem = this.$el;
1931

20-
this.input = this.$refs.input;
21-
this.searchInput = this.$refs.search;
32+
this.input = this.$refs.input as HTMLInputElement;
33+
this.searchInput = this.$refs.search as HTMLInputElement;
2234
this.loading = this.$refs.loading;
2335
this.resultsContainer = this.$refs.results;
2436

@@ -29,9 +41,6 @@ export class EntitySelector extends Component {
2941
initialValue: this.searchInput.value || '',
3042
};
3143

32-
this.search = '';
33-
this.lastClick = 0;
34-
3544
this.setupListeners();
3645
this.showLoading();
3746

@@ -40,16 +49,13 @@ export class EntitySelector extends Component {
4049
}
4150
}
4251

43-
/**
44-
* @param {EntitySelectorSearchOptions} options
45-
*/
46-
configureSearchOptions(options) {
52+
configureSearchOptions(options: Partial<EntitySelectorSearchOptions>): void {
4753
Object.assign(this.searchOptions, options);
4854
this.reset();
4955
this.searchInput.value = this.searchOptions.initialValue;
5056
}
5157

52-
setupListeners() {
58+
setupListeners(): void {
5359
this.elem.addEventListener('click', this.onClick.bind(this));
5460

5561
let lastSearch = 0;
@@ -67,7 +73,7 @@ export class EntitySelector extends Component {
6773
});
6874

6975
// Keyboard navigation
70-
onChildEvent(this.$el, '[data-entity-type]', 'keydown', event => {
76+
onChildEvent(this.$el, '[data-entity-type]', 'keydown', ((event: KeyboardEvent) => {
7177
if (event.ctrlKey && event.code === 'Enter') {
7278
const form = this.$el.closest('form');
7379
if (form) {
@@ -83,7 +89,7 @@ export class EntitySelector extends Component {
8389
if (event.code === 'ArrowUp') {
8490
this.focusAdjacent(false);
8591
}
86-
});
92+
}) as (event: Event) => void);
8793

8894
this.searchInput.addEventListener('keydown', event => {
8995
if (event.code === 'ArrowDown') {
@@ -93,10 +99,10 @@ export class EntitySelector extends Component {
9399
}
94100

95101
focusAdjacent(forward = true) {
96-
const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
102+
const items: (Element|null)[] = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
97103
const selectedIndex = items.indexOf(document.activeElement);
98104
const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0];
99-
if (newItem) {
105+
if (newItem instanceof HTMLElement) {
100106
newItem.focus();
101107
}
102108
}
@@ -132,7 +138,7 @@ export class EntitySelector extends Component {
132138
}
133139

134140
window.$http.get(this.searchUrl()).then(resp => {
135-
this.resultsContainer.innerHTML = resp.data;
141+
this.resultsContainer.innerHTML = resp.data as string;
136142
this.hideLoading();
137143
});
138144
}
@@ -142,15 +148,15 @@ export class EntitySelector extends Component {
142148
return `${this.searchOptions.searchEndpoint}?${query}`;
143149
}
144150

145-
searchEntities(searchTerm) {
151+
searchEntities(searchTerm: string) {
146152
if (!this.searchOptions.searchEndpoint) {
147153
throw new Error('Search endpoint not set for entity-selector load');
148154
}
149155

150156
this.input.value = '';
151157
const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
152158
window.$http.get(url).then(resp => {
153-
this.resultsContainer.innerHTML = resp.data;
159+
this.resultsContainer.innerHTML = resp.data as string;
154160
this.hideLoading();
155161
});
156162
}
@@ -162,16 +168,16 @@ export class EntitySelector extends Component {
162168
return answer;
163169
}
164170

165-
onClick(event) {
166-
const listItem = event.target.closest('[data-entity-type]');
167-
if (listItem) {
171+
onClick(event: MouseEvent) {
172+
const listItem = (event.target as HTMLElement).closest('[data-entity-type]');
173+
if (listItem instanceof HTMLElement) {
168174
event.preventDefault();
169175
event.stopPropagation();
170176
this.selectItem(listItem);
171177
}
172178
}
173179

174-
selectItem(item) {
180+
selectItem(item: HTMLElement): void {
175181
const isDblClick = this.isDoubleClick();
176182
const type = item.getAttribute('data-entity-type');
177183
const id = item.getAttribute('data-entity-id');
@@ -180,14 +186,14 @@ export class EntitySelector extends Component {
180186
this.unselectAll();
181187
this.input.value = isSelected ? `${type}:${id}` : '';
182188

183-
const link = item.getAttribute('href');
184-
const name = item.querySelector('.entity-list-item-name').textContent;
185-
const data = {id: Number(id), name, link};
189+
const link = item.getAttribute('href') || '';
190+
const name = item.querySelector('.entity-list-item-name')?.textContent || '';
191+
const data: EntitySelectorEntity = {id: Number(id), name, link};
186192

187193
if (isSelected) {
188194
item.classList.add('selected');
189195
} else {
190-
window.$events.emit('entity-select-change', null);
196+
window.$events.emit('entity-select-change');
191197
}
192198

193199
if (!isDblClick && !isSelected) return;
@@ -200,7 +206,7 @@ export class EntitySelector extends Component {
200206
}
201207
}
202208

203-
confirmSelection(data) {
209+
confirmSelection(data: EntitySelectorEntity) {
204210
window.$events.emit('entity-select-confirm', data);
205211
}
206212

resources/js/components/image-manager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ export class ImageManager extends Component {
127127
});
128128
}
129129

130+
/**
131+
* @param {({ thumbs: { display: string; }; url: string; name: string; }) => void} callback
132+
* @param {String} type
133+
*/
130134
show(callback, type = 'gallery') {
131135
this.resetAll();
132136

resources/js/global.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ declare global {
1515
baseUrl: (path: string) => string;
1616
importVersioned: (module: string) => Promise<object>;
1717
}
18-
}
18+
}
19+
20+
export type CodeModule = (typeof import('./code/index.mjs'));

0 commit comments

Comments
 (0)