ng-zorro-antd-code-editor.mjs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
  2. import * as i0 from '@angular/core';
  3. import { inject, Injectable, EventEmitter, forwardRef, booleanAttribute, Output, Input, ViewEncapsulation, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
  4. import { NG_VALUE_ACCESSOR } from '@angular/forms';
  5. import { ReplaySubject, BehaviorSubject, of, Subject, combineLatest } from 'rxjs';
  6. import { tap, map, takeUntil, debounceTime, filter, distinctUntilChanged } from 'rxjs/operators';
  7. import { warn, PREFIX } from 'ng-zorro-antd/core/logger';
  8. import { inNextTick, fromEventOutsideAngular } from 'ng-zorro-antd/core/util';
  9. import { NzSpinComponent } from 'ng-zorro-antd/spin';
  10. import * as i1 from 'ng-zorro-antd/core/config';
  11. import * as i2 from '@angular/cdk/platform';
  12. /**
  13. * Use of this source code is governed by an MIT-style license that can be
  14. * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
  15. */
  16. /**
  17. * Use of this source code is governed by an MIT-style license that can be
  18. * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
  19. */
  20. const NZ_CONFIG_MODULE_NAME = 'codeEditor';
  21. function tryTriggerFunc(fn) {
  22. return (...args) => {
  23. if (fn) {
  24. fn(...args);
  25. }
  26. };
  27. }
  28. // Caretaker note: previously, these were `NzCodeEditorService` properties.
  29. // They're kept as static variables because this will allow loading Monaco only once.
  30. // This applies to micro frontend apps with multiple Angular apps or a single Angular app
  31. // that can be bootstrapped and destroyed multiple times (e.g. using Webpack module federation).
  32. // Root providers are re-initialized each time the app is bootstrapped. Platform providers aren't.
  33. // We can't make the `NzCodeEditorService` to be a platform provider (`@Injectable({ providedIn: 'platform' })`)
  34. // since it depends on other root providers.
  35. const loaded$ = new ReplaySubject(1);
  36. let loadingStatus = "unload" /* NzCodeEditorLoadingStatus.UNLOAD */;
  37. class NzCodeEditorService {
  38. nzConfigService;
  39. document = inject(DOCUMENT);
  40. firstEditorInitialized = false;
  41. option = {};
  42. config;
  43. subscription;
  44. option$ = new BehaviorSubject(this.option);
  45. constructor(nzConfigService) {
  46. this.nzConfigService = nzConfigService;
  47. const globalConfig = this.nzConfigService.getConfigForComponent(NZ_CONFIG_MODULE_NAME);
  48. this.config = { ...globalConfig };
  49. if (this.config.monacoEnvironment) {
  50. window.MonacoEnvironment = { ...this.config.monacoEnvironment };
  51. }
  52. this.option = this.config.defaultEditorOption || {};
  53. this.subscription = this.nzConfigService.getConfigChangeEventForComponent(NZ_CONFIG_MODULE_NAME).subscribe(() => {
  54. const newGlobalConfig = this.nzConfigService.getConfigForComponent(NZ_CONFIG_MODULE_NAME);
  55. if (newGlobalConfig) {
  56. this._updateDefaultOption(newGlobalConfig.defaultEditorOption);
  57. }
  58. });
  59. }
  60. ngOnDestroy() {
  61. this.subscription.unsubscribe();
  62. this.subscription = null;
  63. }
  64. _updateDefaultOption(option) {
  65. this.option = { ...this.option, ...option };
  66. this.option$.next(this.option);
  67. if ('theme' in option && option.theme) {
  68. monaco.editor.setTheme(option.theme);
  69. }
  70. }
  71. requestToInit() {
  72. if (loadingStatus === "LOADED" /* NzCodeEditorLoadingStatus.LOADED */) {
  73. this.onInit();
  74. return of(this.getLatestOption());
  75. }
  76. if (loadingStatus === "unload" /* NzCodeEditorLoadingStatus.UNLOAD */) {
  77. if (this.config.useStaticLoading && typeof monaco === 'undefined') {
  78. warn('You choose to use static loading but it seems that you forget ' +
  79. 'to config webpack plugin correctly. Please refer to our official website' +
  80. 'for more details about static loading.');
  81. }
  82. else {
  83. this.loadMonacoScript();
  84. }
  85. }
  86. return loaded$.pipe(tap(() => this.onInit()), map(() => this.getLatestOption()));
  87. }
  88. loadMonacoScript() {
  89. if (this.config.useStaticLoading) {
  90. Promise.resolve().then(() => this.onLoad());
  91. return;
  92. }
  93. if (loadingStatus === "loading" /* NzCodeEditorLoadingStatus.LOADING */) {
  94. return;
  95. }
  96. loadingStatus = "loading" /* NzCodeEditorLoadingStatus.LOADING */;
  97. const assetsRoot = this.config.assetsRoot;
  98. const vs = assetsRoot ? `${assetsRoot}/vs` : 'assets/vs';
  99. const windowAsAny = window;
  100. const loadScript = this.document.createElement('script');
  101. loadScript.type = 'text/javascript';
  102. loadScript.src = `${vs}/loader.js`;
  103. const onLoad = () => {
  104. cleanup();
  105. windowAsAny.require.config({
  106. paths: { vs },
  107. ...this.config.extraConfig
  108. });
  109. windowAsAny.require(['vs/editor/editor.main'], () => {
  110. this.onLoad();
  111. });
  112. };
  113. const onError = () => {
  114. cleanup();
  115. throw new Error(`${PREFIX} cannot load assets of monaco editor from source "${vs}".`);
  116. };
  117. const cleanup = () => {
  118. // Caretaker note: we have to remove these listeners once the `<script>` is loaded successfully
  119. // or not since the `onLoad` listener captures `this`, which will prevent the `NzCodeEditorService`
  120. // from being garbage collected.
  121. loadScript.removeEventListener('load', onLoad);
  122. loadScript.removeEventListener('error', onError);
  123. // We don't need to keep the `<script>` element within the `<body>` since JavaScript has
  124. // been executed and Monaco is available globally. E.g. Webpack, always removes `<script>`
  125. // elements after loading chunks (see its `LoadScriptRuntimeModule`).
  126. this.document.documentElement.removeChild(loadScript);
  127. };
  128. loadScript.addEventListener('load', onLoad);
  129. loadScript.addEventListener('error', onError);
  130. this.document.documentElement.appendChild(loadScript);
  131. }
  132. onLoad() {
  133. loadingStatus = "LOADED" /* NzCodeEditorLoadingStatus.LOADED */;
  134. loaded$.next(true);
  135. loaded$.complete();
  136. tryTriggerFunc(this.config.onLoad)();
  137. }
  138. onInit() {
  139. if (!this.firstEditorInitialized) {
  140. this.firstEditorInitialized = true;
  141. tryTriggerFunc(this.config.onFirstEditorInit)();
  142. }
  143. tryTriggerFunc(this.config.onInit)();
  144. }
  145. getLatestOption() {
  146. return { ...this.option };
  147. }
  148. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorService, deps: [{ token: i1.NzConfigService }], target: i0.ɵɵFactoryTarget.Injectable });
  149. static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorService, providedIn: 'root' });
  150. }
  151. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorService, decorators: [{
  152. type: Injectable,
  153. args: [{
  154. providedIn: 'root'
  155. }]
  156. }], ctorParameters: () => [{ type: i1.NzConfigService }] });
  157. class NzCodeEditorComponent {
  158. nzCodeEditorService;
  159. ngZone;
  160. platform;
  161. nzEditorMode = 'normal';
  162. nzOriginalText = '';
  163. nzLoading = false;
  164. nzFullControl = false;
  165. nzToolkit;
  166. set nzEditorOption(value) {
  167. this.editorOption$.next(value);
  168. }
  169. nzEditorInitialized = new EventEmitter();
  170. editorOptionCached = {};
  171. el;
  172. destroy$ = new Subject();
  173. resize$ = new Subject();
  174. editorOption$ = new BehaviorSubject({});
  175. editorInstance = null;
  176. value = '';
  177. modelSet = false;
  178. onDidChangeContentDisposable = null;
  179. constructor(nzCodeEditorService, ngZone, elementRef, platform) {
  180. this.nzCodeEditorService = nzCodeEditorService;
  181. this.ngZone = ngZone;
  182. this.platform = platform;
  183. this.el = elementRef.nativeElement;
  184. this.el.classList.add('ant-code-editor');
  185. }
  186. /**
  187. * Initialize a monaco editor instance.
  188. */
  189. ngAfterViewInit() {
  190. if (!this.platform.isBrowser) {
  191. return;
  192. }
  193. this.nzCodeEditorService
  194. .requestToInit()
  195. .pipe(takeUntil(this.destroy$))
  196. .subscribe(option => this.setup(option));
  197. }
  198. ngOnDestroy() {
  199. if (this.onDidChangeContentDisposable) {
  200. this.onDidChangeContentDisposable.dispose();
  201. this.onDidChangeContentDisposable = null;
  202. }
  203. if (this.editorInstance) {
  204. this.editorInstance.dispose();
  205. this.editorInstance = null;
  206. }
  207. this.destroy$.next();
  208. this.destroy$.complete();
  209. }
  210. writeValue(value) {
  211. this.value = value;
  212. this.setValue();
  213. }
  214. registerOnChange(fn) {
  215. this.onChange = fn;
  216. }
  217. registerOnTouched(fn) {
  218. this.onTouch = fn;
  219. }
  220. onChange = (_value) => { };
  221. onTouch = () => { };
  222. layout() {
  223. this.resize$.next();
  224. }
  225. setup(option) {
  226. // The `setup()` is invoked when the Monaco editor is loaded. This may happen asynchronously for the first
  227. // time, and it'll always happen synchronously afterwards. The first `setup()` invokation is outside the Angular
  228. // zone, but further invokations will happen within the Angular zone. We call the `setModel()` on the editor
  229. // instance, which tells Monaco to add event listeners lazily internally (`mousemove`, `mouseout`, etc.).
  230. // We should avoid adding them within the Angular zone since this will drastically affect the performance.
  231. this.ngZone.runOutsideAngular(() => inNextTick()
  232. .pipe(takeUntil(this.destroy$))
  233. .subscribe(() => {
  234. this.editorOptionCached = option;
  235. this.registerOptionChanges();
  236. this.initMonacoEditorInstance();
  237. this.registerResizeChange();
  238. this.setValue();
  239. if (!this.nzFullControl) {
  240. this.setValueEmitter();
  241. }
  242. if (this.nzEditorInitialized.observers.length) {
  243. this.ngZone.run(() => this.nzEditorInitialized.emit(this.editorInstance));
  244. }
  245. }));
  246. }
  247. registerOptionChanges() {
  248. combineLatest([this.editorOption$, this.nzCodeEditorService.option$])
  249. .pipe(takeUntil(this.destroy$))
  250. .subscribe(([selfOpt, defaultOpt]) => {
  251. this.editorOptionCached = {
  252. ...this.editorOptionCached,
  253. ...defaultOpt,
  254. ...selfOpt
  255. };
  256. this.updateOptionToMonaco();
  257. });
  258. }
  259. initMonacoEditorInstance() {
  260. this.ngZone.runOutsideAngular(() => {
  261. this.editorInstance =
  262. this.nzEditorMode === 'normal'
  263. ? monaco.editor.create(this.el, { ...this.editorOptionCached })
  264. : monaco.editor.createDiffEditor(this.el, {
  265. ...this.editorOptionCached
  266. });
  267. });
  268. }
  269. registerResizeChange() {
  270. fromEventOutsideAngular(window, 'resize')
  271. .pipe(debounceTime(300), takeUntil(this.destroy$))
  272. .subscribe(() => {
  273. this.layout();
  274. });
  275. this.resize$
  276. .pipe(takeUntil(this.destroy$), filter(() => !!this.editorInstance), map(() => ({
  277. width: this.el.clientWidth,
  278. height: this.el.clientHeight
  279. })), distinctUntilChanged((a, b) => a.width === b.width && a.height === b.height), debounceTime(50))
  280. .subscribe(() => {
  281. this.editorInstance.layout();
  282. });
  283. }
  284. setValue() {
  285. if (!this.editorInstance) {
  286. return;
  287. }
  288. if (this.nzFullControl && this.value) {
  289. warn(`should not set value when you are using full control mode! It would result in ambiguous data flow!`);
  290. return;
  291. }
  292. if (this.nzEditorMode === 'normal') {
  293. if (this.modelSet) {
  294. const model = this.editorInstance.getModel();
  295. this.preservePositionAndSelections(() => model.setValue(this.value));
  296. }
  297. else {
  298. this.editorInstance.setModel(monaco.editor.createModel(this.value, this.editorOptionCached.language));
  299. this.modelSet = true;
  300. }
  301. }
  302. else {
  303. if (this.modelSet) {
  304. const model = this.editorInstance.getModel();
  305. this.preservePositionAndSelections(() => {
  306. model.modified.setValue(this.value);
  307. model.original.setValue(this.nzOriginalText);
  308. });
  309. }
  310. else {
  311. const language = this.editorOptionCached.language;
  312. this.editorInstance.setModel({
  313. original: monaco.editor.createModel(this.nzOriginalText, language),
  314. modified: monaco.editor.createModel(this.value, language)
  315. });
  316. this.modelSet = true;
  317. }
  318. }
  319. }
  320. /**
  321. * {@link editor.ICodeEditor}#setValue resets the cursor position to the start of the document.
  322. * This helper memorizes the cursor position and selections and restores them after the given
  323. * function has been called.
  324. */
  325. preservePositionAndSelections(fn) {
  326. if (!this.editorInstance) {
  327. fn();
  328. return;
  329. }
  330. const position = this.editorInstance.getPosition();
  331. const selections = this.editorInstance.getSelections();
  332. fn();
  333. if (position) {
  334. this.editorInstance.setPosition(position);
  335. }
  336. if (selections) {
  337. this.editorInstance.setSelections(selections);
  338. }
  339. }
  340. setValueEmitter() {
  341. const model = (this.nzEditorMode === 'normal'
  342. ? this.editorInstance.getModel()
  343. : this.editorInstance.getModel().modified);
  344. // The `onDidChangeContent` returns a disposable object (an object with `dispose()` method) which will cleanup
  345. // the listener. The callback, that we pass to `onDidChangeContent`, captures `this`. This leads to a circular reference
  346. // (`nz-code-editor -> monaco -> nz-code-editor`) and prevents the `nz-code-editor` from being GC'd.
  347. this.onDidChangeContentDisposable = model.onDidChangeContent(() => {
  348. this.emitValue(model.getValue());
  349. });
  350. }
  351. emitValue(value) {
  352. if (this.value === value) {
  353. // If the value didn't change there's no reason to send an update.
  354. // Specifically this may happen during an update from the model (writeValue) where sending an update to the model would actually be incorrect.
  355. return;
  356. }
  357. this.value = value;
  358. // We're re-entering the Angular zone only if the value has been changed since there's a `return` expression previously.
  359. // This won't cause "dead" change detections (basically when the `tick()` has been run, but there's nothing to update).
  360. this.ngZone.run(() => {
  361. this.onChange(value);
  362. });
  363. }
  364. updateOptionToMonaco() {
  365. if (this.editorInstance) {
  366. this.editorInstance.updateOptions({ ...this.editorOptionCached });
  367. }
  368. }
  369. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorComponent, deps: [{ token: NzCodeEditorService }, { token: i0.NgZone }, { token: i0.ElementRef }, { token: i2.Platform }], target: i0.ɵɵFactoryTarget.Component });
  370. static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.2", type: NzCodeEditorComponent, isStandalone: true, selector: "nz-code-editor", inputs: { nzEditorMode: "nzEditorMode", nzOriginalText: "nzOriginalText", nzLoading: ["nzLoading", "nzLoading", booleanAttribute], nzFullControl: ["nzFullControl", "nzFullControl", booleanAttribute], nzToolkit: "nzToolkit", nzEditorOption: "nzEditorOption" }, outputs: { nzEditorInitialized: "nzEditorInitialized" }, providers: [
  371. {
  372. provide: NG_VALUE_ACCESSOR,
  373. useExisting: forwardRef(() => NzCodeEditorComponent),
  374. multi: true
  375. }
  376. ], exportAs: ["nzCodeEditor"], ngImport: i0, template: `
  377. @if (nzLoading) {
  378. <div class="ant-code-editor-loading">
  379. <nz-spin />
  380. </div>
  381. }
  382. @if (nzToolkit) {
  383. <div class="ant-code-editor-toolkit">
  384. <ng-template [ngTemplateOutlet]="nzToolkit" />
  385. </div>
  386. }
  387. `, isInline: true, dependencies: [{ kind: "component", type: NzSpinComponent, selector: "nz-spin", inputs: ["nzIndicator", "nzSize", "nzTip", "nzDelay", "nzSimple", "nzSpinning"], exportAs: ["nzSpin"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
  388. }
  389. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorComponent, decorators: [{
  390. type: Component,
  391. args: [{
  392. changeDetection: ChangeDetectionStrategy.OnPush,
  393. encapsulation: ViewEncapsulation.None,
  394. selector: 'nz-code-editor',
  395. exportAs: 'nzCodeEditor',
  396. template: `
  397. @if (nzLoading) {
  398. <div class="ant-code-editor-loading">
  399. <nz-spin />
  400. </div>
  401. }
  402. @if (nzToolkit) {
  403. <div class="ant-code-editor-toolkit">
  404. <ng-template [ngTemplateOutlet]="nzToolkit" />
  405. </div>
  406. }
  407. `,
  408. providers: [
  409. {
  410. provide: NG_VALUE_ACCESSOR,
  411. useExisting: forwardRef(() => NzCodeEditorComponent),
  412. multi: true
  413. }
  414. ],
  415. imports: [NzSpinComponent, NgTemplateOutlet]
  416. }]
  417. }], ctorParameters: () => [{ type: NzCodeEditorService }, { type: i0.NgZone }, { type: i0.ElementRef }, { type: i2.Platform }], propDecorators: { nzEditorMode: [{
  418. type: Input
  419. }], nzOriginalText: [{
  420. type: Input
  421. }], nzLoading: [{
  422. type: Input,
  423. args: [{ transform: booleanAttribute }]
  424. }], nzFullControl: [{
  425. type: Input,
  426. args: [{ transform: booleanAttribute }]
  427. }], nzToolkit: [{
  428. type: Input
  429. }], nzEditorOption: [{
  430. type: Input
  431. }], nzEditorInitialized: [{
  432. type: Output
  433. }] } });
  434. /**
  435. * Use of this source code is governed by an MIT-style license that can be
  436. * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
  437. */
  438. class NzCodeEditorModule {
  439. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
  440. static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorModule, imports: [NzCodeEditorComponent], exports: [NzCodeEditorComponent] });
  441. static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorModule, imports: [NzCodeEditorComponent] });
  442. }
  443. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: NzCodeEditorModule, decorators: [{
  444. type: NgModule,
  445. args: [{
  446. imports: [NzCodeEditorComponent],
  447. exports: [NzCodeEditorComponent]
  448. }]
  449. }] });
  450. /**
  451. * Use of this source code is governed by an MIT-style license that can be
  452. * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
  453. */
  454. /**
  455. * Generated bundle index. Do not edit.
  456. */
  457. export { NzCodeEditorComponent, NzCodeEditorModule, NzCodeEditorService };
  458. //# sourceMappingURL=ng-zorro-antd-code-editor.mjs.map