mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Move ckeditor components into one and add markdown source
This commit is contained in:
@@ -36,7 +36,7 @@
|
||||
|
||||
subject.val(result.subject);
|
||||
|
||||
$('op-ckeditor-form')
|
||||
$('ckeditor-augmented-textarea')
|
||||
.data('editor')
|
||||
.then(function(editor) {
|
||||
editor.setData(result.content);
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -143,23 +143,10 @@ h1:hover, h2:hover, h3:hover
|
||||
overflow-wrap: break-word
|
||||
word-wrap: break-word
|
||||
|
||||
#wiki_form .attributes-group
|
||||
.attributes-group--header-text
|
||||
color: lighten($body-font-color, 10)
|
||||
.form--label
|
||||
display: none
|
||||
|
||||
.wiki--content--attribute
|
||||
.form--field-container
|
||||
max-width: 100%
|
||||
|
||||
.form--text-area-container
|
||||
margin-top: -46px
|
||||
|
||||
.jstElements
|
||||
margin-bottom: 18px
|
||||
position: relative
|
||||
top: -5px
|
||||
|
||||
#wiki_page_parent_id
|
||||
overflow: auto
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ module TextFormattingHelper
|
||||
#TODO remove
|
||||
def current_formatting_helper
|
||||
helper_class = OpenProject::TextFormatting::Formats.rich_helper
|
||||
helper_class.new
|
||||
helper_class.new(self)
|
||||
end
|
||||
|
||||
def project_preview_context(object, project)
|
||||
|
||||
@@ -10,12 +10,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="attributes-group">
|
||||
<div class="attributes-group--header">
|
||||
<div class="attributes-group--header-container">
|
||||
<h3 class="attributes-group--header-text"><%= WikiPage.human_attribute_name(:text) %></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attributes-group wiki--content--attribute">
|
||||
<%= f.text_area :text,
|
||||
cols: 100,
|
||||
rows: 25,
|
||||
@@ -23,6 +18,7 @@
|
||||
accesskey: accesskey(:edit),
|
||||
with_text_formatting: true,
|
||||
resource: resource,
|
||||
label_options: { class: 'hidden-for-sighted' },
|
||||
preview_context: preview_context(@page, @project) %>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -89,7 +89,11 @@ en:
|
||||
description_subwork_package: "Child of work package #%{id}"
|
||||
editor:
|
||||
preview: 'Toggle preview mode'
|
||||
error_saving_failed: 'Saving the document failed with the following error: %{e}'
|
||||
source_code: 'Toggle Markdown source mode'
|
||||
error_saving_failed: 'Saving the document failed with the following error: %{error}'
|
||||
mode:
|
||||
manual: 'Switch to Markdown source'
|
||||
wysiwyg: 'Switch to WYSIWYG editor'
|
||||
macro:
|
||||
child_pages:
|
||||
button: 'Links to child pages'
|
||||
|
||||
@@ -182,7 +182,6 @@ import {ActivityLinkComponent} from "core-components/wp-activity/activity-link.c
|
||||
import {RevisionActivityComponent} from "core-components/wp-activity/revision/revision-activity.component";
|
||||
import {CommentService} from "core-components/wp-activity/comment-service";
|
||||
import {WorkPackageCommentComponent} from "core-components/work-packages/work-package-comment/work-package-comment.component";
|
||||
import {OpCkeditorFormComponent} from "core-components/ckeditor/op-ckeditor-form.component";
|
||||
import {OpDragScrollDirective} from "core-app/modules/common/ui/op-drag-scroll.directive";
|
||||
import {UIRouterModule} from "@uirouter/angular";
|
||||
import {initializeUiRouterConfiguration} from "core-components/routing/ui-router.config";
|
||||
@@ -206,15 +205,14 @@ import {WpButtonMacroModal} from "core-components/modals/editor/macro-wp-button-
|
||||
import {EditorMacrosService} from "core-components/modals/editor/editor-macros.service";
|
||||
import {WikiIncludePageMacroModal} from "core-components/modals/editor/macro-wiki-include-page-modal/wiki-include-page-macro.modal";
|
||||
import {CodeBlockMacroModal} from "core-components/modals/editor/macro-code-block-modal/code-block-macro.modal";
|
||||
import {CKEditorSetupService} from "core-components/ckeditor/ckeditor-setup.service";
|
||||
import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
|
||||
import {CKEditorPreviewService} from "core-components/ckeditor/ckeditor-preview.service";
|
||||
import {ChildPagesMacroModal} from "core-components/modals/editor/macro-child-pages-modal/child-pages-macro.modal";
|
||||
import {AttachmentListComponent} from 'core-components/attachments/attachment-list/attachment-list.component';
|
||||
import {AttachmentListItemComponent} from 'core-components/attachments/attachment-list/attachment-list-item.component';
|
||||
import {AttachmentsUploadComponent} from 'core-components/attachments/attachments-upload/attachments-upload.component';
|
||||
import {AttachmentsComponent} from 'core-components/attachments/attachments.component';
|
||||
import {CurrentUserService} from 'core-components/user/current-user.service';
|
||||
import {CkeditorAugmentedTextareaComponent} from "core-app/ckeditor/ckeditor-augmented-textarea.component";
|
||||
import {WpTableConfigurationHighlightingTab} from "core-components/wp-table/configuration-modal/tabs/highlighting-tab.component";
|
||||
import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table/state/wp-table-highlighting.service";
|
||||
|
||||
@@ -313,9 +311,7 @@ import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table
|
||||
ConfirmDialogService,
|
||||
|
||||
// CKEditor
|
||||
CKEditorSetupService,
|
||||
EditorMacrosService,
|
||||
CKEditorPreviewService,
|
||||
|
||||
// Main Menu
|
||||
MainMenuToggleService,
|
||||
@@ -477,9 +473,9 @@ import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table
|
||||
// Form configuration
|
||||
OpDragScrollDirective,
|
||||
|
||||
// CkEditor and Macros
|
||||
OpCkeditorFormComponent,
|
||||
// CKEditor and Macros
|
||||
EmbeddedTablesMacroComponent,
|
||||
CkeditorAugmentedTextareaComponent,
|
||||
|
||||
// Attachments
|
||||
AttachmentsComponent,
|
||||
@@ -563,7 +559,7 @@ import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table
|
||||
MainMenuToggleComponent,
|
||||
|
||||
// CKEditor and macros
|
||||
OpCkeditorFormComponent,
|
||||
CkeditorAugmentedTextareaComponent,
|
||||
EmbeddedTablesMacroComponent,
|
||||
|
||||
// Attachments
|
||||
|
||||
+29
-41
@@ -26,10 +26,9 @@
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import {Component, ElementRef, OnInit, OnDestroy} from '@angular/core';
|
||||
import {Component, ElementRef, OnInit, OnDestroy, ViewChild, AfterContentInit} from '@angular/core';
|
||||
import {ConfigurationService} from 'core-app/modules/common/config/configuration.service';
|
||||
import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service';
|
||||
import {CKEditorSetupService, ICKEditorInstance} from 'core-components/ckeditor/ckeditor-setup.service';
|
||||
import {HalResource} from 'core-app/modules/hal/resources/hal-resource';
|
||||
import {HalResourceService} from 'core-app/modules/hal/services/hal-resource.service';
|
||||
import {DynamicBootstrapper} from 'core-app/globals/dynamic-bootstrapper';
|
||||
@@ -38,18 +37,19 @@ import {componentDestroyed} from 'ng2-rx-componentdestroyed';
|
||||
import {takeUntil, filter} from 'rxjs/operators';
|
||||
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
|
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
|
||||
import {ICKEditorContext, ICKEditorInstance} from "core-app/modules/common/ckeditor/ckeditor-setup.service";
|
||||
import {OpCkeditorComponent} from "core-app/modules/common/ckeditor/op-ckeditor.component";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'op-ckeditor-form',
|
||||
templateUrl: './op-ckeditor-form.html'
|
||||
selector: 'ckeditor-augmented-textarea',
|
||||
templateUrl: './ckeditor-augmented-textarea.html'
|
||||
})
|
||||
export class OpCkeditorFormComponent implements OnInit, OnDestroy {
|
||||
export class CkeditorAugmentedTextareaComponent implements OnInit, OnDestroy {
|
||||
public textareaSelector:string;
|
||||
public previewContext:string;
|
||||
|
||||
// Which template to include
|
||||
public ckeditor:any;
|
||||
public $element:JQuery;
|
||||
public formElement:JQuery;
|
||||
public wrappedTextArea:JQuery;
|
||||
@@ -59,22 +59,25 @@ export class OpCkeditorFormComponent implements OnInit, OnDestroy {
|
||||
public changed:boolean = false;
|
||||
public inFlight:boolean = false;
|
||||
|
||||
public text:any;
|
||||
public initialContent:string;
|
||||
public resource?:HalResource;
|
||||
public context:ICKEditorContext;
|
||||
|
||||
// Reference to the actual ckeditor instance component
|
||||
@ViewChild(OpCkeditorComponent) private ckEditorInstance:OpCkeditorComponent;
|
||||
|
||||
private attachments:HalResource[];
|
||||
|
||||
constructor(protected elementRef:ElementRef,
|
||||
protected pathHelper:PathHelperService,
|
||||
protected halResourceService:HalResourceService,
|
||||
protected ckEditorSetup:CKEditorSetupService,
|
||||
protected Notifications:NotificationsService,
|
||||
protected I18n:I18nService,
|
||||
protected states:States,
|
||||
protected ConfigurationService:ConfigurationService) {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
ngOnInit() {
|
||||
this.$element = jQuery(this.elementRef.nativeElement);
|
||||
|
||||
// Parse the attribute explicitly since this is likely a bootstrapped element
|
||||
@@ -87,44 +90,29 @@ export class OpCkeditorFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.formElement = this.$element.closest('form');
|
||||
this.wrappedTextArea = this.formElement.find(this.textareaSelector);
|
||||
this.wrappedTextArea.hide();
|
||||
this.wrappedTextArea
|
||||
.removeAttr('required')
|
||||
.hide();
|
||||
this.initialContent = this.wrappedTextArea.val();
|
||||
|
||||
this.$attachmentsElement = this.formElement.find('#attachments_fields');
|
||||
const wrapper = this.$element.find(`.__op_ckeditor_replacement_container`);
|
||||
const context = { resource: this.resource,
|
||||
previewContext: this.previewContext };
|
||||
|
||||
const editorPromise = this.ckEditorSetup
|
||||
.create('full',
|
||||
wrapper[0],
|
||||
context)
|
||||
.then(this.setup.bind(this));
|
||||
|
||||
this.$element.data('editor', editorPromise);
|
||||
this.context = { resource: this.resource, previewContext: this.previewContext };
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
ngOnDestroy() {
|
||||
this.formElement.off('submit.ckeditor');
|
||||
}
|
||||
|
||||
public setup(editor:ICKEditorInstance) {
|
||||
this.ckeditor = editor;
|
||||
(window as any).ckeditor = editor;
|
||||
const rawValue = this.wrappedTextArea.val();
|
||||
|
||||
if (rawValue) {
|
||||
editor.setData(rawValue);
|
||||
}
|
||||
|
||||
if (this.resource && this.resource.attachments) {
|
||||
this.setupAttachmentAddedCallback();
|
||||
this.setupAttachmentRemovalSignal();
|
||||
this.setupAttachmentAddedCallback(editor);
|
||||
this.setupAttachmentRemovalSignal(editor);
|
||||
}
|
||||
|
||||
// Listen for form submission to set textarea content
|
||||
this.formElement.on('submit.ckeditor change.ckeditor', () => {
|
||||
this.formElement.on('submit.ckeditor', () => {
|
||||
try {
|
||||
const value = this.ckeditor.getData();
|
||||
this.wrappedTextArea.val(value);
|
||||
this.wrappedTextArea.val(this.ckEditorInstance.getRawData());
|
||||
} catch (e) {
|
||||
console.error(`Failed to save CKEditor body to textarea: ${e}.`)
|
||||
this.Notifications.addError(e || this.I18n.t('js.errors.internal'));
|
||||
@@ -144,13 +132,13 @@ export class OpCkeditorFormComponent implements OnInit, OnDestroy {
|
||||
return editor;
|
||||
}
|
||||
|
||||
private setupAttachmentAddedCallback() {
|
||||
this.ckeditor.model.on('op:attachment-added', () => {
|
||||
private setupAttachmentAddedCallback(editor:ICKEditorInstance) {
|
||||
editor.model.on('op:attachment-added', () => {
|
||||
this.states.forResource(this.resource!).putValue(this.resource!);
|
||||
});
|
||||
}
|
||||
|
||||
private setupAttachmentRemovalSignal() {
|
||||
private setupAttachmentRemovalSignal(editor:ICKEditorInstance) {
|
||||
this.attachments = _.clone(this.resource!.attachments.elements);
|
||||
|
||||
this.states.forResource(this.resource!).changes$()
|
||||
@@ -165,7 +153,7 @@ export class OpCkeditorFormComponent implements OnInit, OnDestroy {
|
||||
let removedUrls = missingAttachments.map(attachment => attachment.downloadLocation.$href);
|
||||
|
||||
if (removedUrls.length) {
|
||||
this.ckeditor.model.fire('op:attachment-removed', removedUrls);
|
||||
editor.model.fire('op:attachment-removed', removedUrls);
|
||||
}
|
||||
|
||||
this.attachments = _.clone(resource!.attachments.elements);
|
||||
@@ -212,7 +200,7 @@ export class OpCkeditorFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
DynamicBootstrapper.register({
|
||||
selector: 'op-ckeditor-form',
|
||||
cls: OpCkeditorFormComponent,
|
||||
selector: 'ckeditor-augmented-textarea',
|
||||
cls: CkeditorAugmentedTextareaComponent,
|
||||
embeddable: true
|
||||
});
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
<ng-container>
|
||||
<div class="op-ckeditor--wrapper">
|
||||
<div class="op-ckeditor-source-element __op_ckeditor_replacement_container"></div>
|
||||
<op-ckeditor [context]="context"
|
||||
[content]="initialContent"
|
||||
(onInitialized)="setup($event)"
|
||||
ckEditorType="full">
|
||||
</op-ckeditor>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="resource && resource.attachments">
|
||||
-5
@@ -31,11 +31,6 @@ import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service";
|
||||
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, Inject, ViewChild} from "@angular/core";
|
||||
import {OpModalLocalsMap} from "core-components/op-modals/op-modal.types";
|
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
|
||||
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
|
||||
import {IWorkPackageCreateServiceToken} from "core-components/wp-new/wp-create.service.interface";
|
||||
import {TypeResource} from "core-app/modules/hal/resources/type-resource";
|
||||
import {CurrentProjectService} from "core-components/projects/current-project.service";
|
||||
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: './code-block-macro.modal.html'
|
||||
|
||||
+13
-4
@@ -4,18 +4,19 @@ import {Injectable} from "@angular/core";
|
||||
|
||||
export interface ICKEditorInstance {
|
||||
getData():string;
|
||||
|
||||
setData(content:string):void;
|
||||
on(event:string, callback:Function):void;
|
||||
|
||||
model:any;
|
||||
editing:any;
|
||||
config:any;
|
||||
element:HTMLElement;
|
||||
}
|
||||
|
||||
export interface ICKEditorStatic {
|
||||
create(el:HTMLElement, config?:any):Promise<ICKEditorInstance>;
|
||||
|
||||
createCustomized(el:HTMLElement, config?:any):Promise<ICKEditorInstance>;
|
||||
createCustomized(el:string|HTMLElement, config?:any):Promise<ICKEditorInstance>;
|
||||
}
|
||||
|
||||
export interface ICKEditorContext {
|
||||
@@ -50,15 +51,23 @@ export class CKEditorSetupService {
|
||||
* @param {ICKEditorContext} context
|
||||
* @returns {Promise<ICKEditorInstance>}
|
||||
*/
|
||||
public create(type:'full' | 'constrained', wrapper:HTMLElement, context:ICKEditorContext) {
|
||||
public create(type:'full' | 'constrained', wrapper:HTMLElement, context:ICKEditorContext, initialData:string|null = null) {
|
||||
const editor = type === 'constrained' ? window.OPConstrainedEditor : window.OPClassicEditor;
|
||||
wrapper.classList.add(`ckeditor-type-${type}`);
|
||||
|
||||
let initialDataSet = initialData !== null;
|
||||
let param = initialDataSet ? initialData! : wrapper;
|
||||
|
||||
return editor
|
||||
.createCustomized(wrapper, {
|
||||
.createCustomized(param, {
|
||||
openProject: this.createConfig(context)
|
||||
})
|
||||
.then((editor) => {
|
||||
// If initial data was passed, add to wrapper element
|
||||
if (initialDataSet) {
|
||||
wrapper.appendChild(editor.element);
|
||||
}
|
||||
|
||||
// Allow custom events on wrapper to set/get data for debugging
|
||||
jQuery(wrapper)
|
||||
.on('op:ckeditor:setData', (event:any, data:string) => editor.setData(data))
|
||||
@@ -0,0 +1,229 @@
|
||||
// -- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import {Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild} from '@angular/core';
|
||||
import {
|
||||
CKEditorSetupService,
|
||||
ICKEditorContext,
|
||||
ICKEditorInstance
|
||||
} from "core-app/modules/common/ckeditor/ckeditor-setup.service";
|
||||
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
|
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
|
||||
import {ConfigurationService} from "core-app/modules/common/config/configuration.service";
|
||||
|
||||
declare module 'codemirror';
|
||||
|
||||
const manualModeLocalStorageKey = 'op-ckeditor-uses-manual-mode';
|
||||
|
||||
@Component({
|
||||
selector: 'op-ckeditor',
|
||||
templateUrl: './op-ckeditor.html',
|
||||
styleUrls: ['./op-ckeditor.sass']
|
||||
})
|
||||
export class OpCkeditorComponent implements OnInit, OnDestroy {
|
||||
@Input() ckEditorType:'full' | 'constrained' = 'full';
|
||||
@Input() context:ICKEditorContext;
|
||||
@Input('content') _content:string;
|
||||
|
||||
// Output notification once ready
|
||||
@Output() onInitialized = new EventEmitter<ICKEditorInstance>();
|
||||
|
||||
// Output notification at max once/s for data changes
|
||||
@Output() onContentChange = new EventEmitter<string>();
|
||||
|
||||
// View container of the replacement used to initialize CKEditor5
|
||||
@ViewChild('opCkeditorReplacementContainer') opCkeditorReplacementContainer:ElementRef;
|
||||
@ViewChild('codeMirrorPane') codeMirrorPane:ElementRef;
|
||||
|
||||
// CKEditor instance once initialized
|
||||
public ckEditorInstance:ICKEditorInstance;
|
||||
public error:string | null = null;
|
||||
public allowManualMode = false;
|
||||
public manualMode = false;
|
||||
|
||||
// Codemirror instance, initialized lazily when running source mode
|
||||
public codeMirrorInstance:undefined | any;
|
||||
|
||||
// Debounce change listener for both CKE and codemirror
|
||||
// to read back changes as they happen
|
||||
private debouncedEmitter = _.debounce(
|
||||
async () => {
|
||||
this.getTransformedContent(false)
|
||||
.then(val => {
|
||||
this.onContentChange.emit(val);
|
||||
});
|
||||
},
|
||||
1000,
|
||||
{leading: true}
|
||||
);
|
||||
|
||||
private $element:JQuery;
|
||||
|
||||
constructor(private readonly elementRef:ElementRef,
|
||||
private readonly Notifications:NotificationsService,
|
||||
private readonly I18n:I18nService,
|
||||
private readonly configurationService:ConfigurationService,
|
||||
private readonly ckEditorSetup:CKEditorSetupService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current live data from CKEditor. This may raise in cases
|
||||
* the data cannot be loaded (MS Edge!)
|
||||
*/
|
||||
public getRawData() {
|
||||
if (this.manualMode) {
|
||||
return this._content = this.codeMirrorInstance!.getValue();
|
||||
} else {
|
||||
return this._content = this.ckEditorInstance!.getData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a promise with the transformed content, will wrap errors in the promise.
|
||||
* @param notificationOnError
|
||||
*/
|
||||
public getTransformedContent(notificationOnError = true):Promise<string> {
|
||||
if (!this.initialized) {
|
||||
throw "Tried to access CKEditor instance before initialization.";
|
||||
}
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
resolve(this.getRawData());
|
||||
} catch (e) {
|
||||
console.error(`Failed to save CKEditor content: ${e}.`);
|
||||
let error = this.I18n.t(
|
||||
'js.editor.error_saving_failed',
|
||||
{error: e || this.I18n.t('js.errors.internal')}
|
||||
);
|
||||
|
||||
if (notificationOnError) {
|
||||
this.Notifications.addError(error);
|
||||
}
|
||||
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public set content(newVal:string) {
|
||||
if (!this.initialized) {
|
||||
throw "Tried to access CKEditor instance before initialization.";
|
||||
}
|
||||
|
||||
this._content = newVal;
|
||||
this.ckEditorInstance!.setData(newVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current content. This may be outdated a tiny bit.
|
||||
*/
|
||||
public get content() {
|
||||
return this._content;
|
||||
}
|
||||
|
||||
public get initialized():boolean {
|
||||
return this.ckEditorInstance !== undefined;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.$element = jQuery(this.elementRef.nativeElement);
|
||||
|
||||
const editorPromise = this.ckEditorSetup
|
||||
.create(
|
||||
this.ckEditorType,
|
||||
this.opCkeditorReplacementContainer.nativeElement,
|
||||
this.context,
|
||||
this.content
|
||||
)
|
||||
.catch((error:string) => {
|
||||
this.error = error;
|
||||
})
|
||||
.then((editor:ICKEditorInstance) => {
|
||||
this.ckEditorInstance = editor;
|
||||
|
||||
// Save changes while in wysiwyg mode
|
||||
editor.model.document.on('change', this.debouncedEmitter);
|
||||
|
||||
// Switch mode
|
||||
editor.on('op:source-code-enabled', () => this.enableManualMode());
|
||||
editor.on('op:source-code-disabled', () => this.disableManualMode());
|
||||
|
||||
this.onInitialized.emit(editor);
|
||||
});
|
||||
|
||||
this.$element.data('editor', editorPromise);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disable the manual mode, kill the codeMirror instance and switch back to CKEditor
|
||||
*/
|
||||
private disableManualMode() {
|
||||
const current = this.getRawData();
|
||||
|
||||
// Apply content to ckeditor
|
||||
this.ckEditorInstance.setData(current);
|
||||
this.codeMirrorInstance = null;
|
||||
this.manualMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable manual mode, get data from WYSIWYG and show CodeMirror instance.
|
||||
*/
|
||||
private enableManualMode() {
|
||||
const current = this.getRawData();
|
||||
const cmMode = 'gfm';
|
||||
|
||||
Promise
|
||||
.all([
|
||||
import('codemirror'),
|
||||
import(`codemirror/mode/${cmMode}/${cmMode}`)
|
||||
])
|
||||
.then((imported:any[]) => {
|
||||
const CodeMirror = imported[0].default;
|
||||
this.codeMirrorInstance = CodeMirror(
|
||||
this.$element.find('.ck-editor__source')[0],
|
||||
{
|
||||
lineNumbers: true,
|
||||
smartIndent: true,
|
||||
value: current,
|
||||
mode: ''
|
||||
}
|
||||
);
|
||||
|
||||
this.codeMirrorInstance.on('change', this.debouncedEmitter);
|
||||
setTimeout(() => this.codeMirrorInstance.refresh(), 100);
|
||||
this.manualMode = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<ng-container>
|
||||
<div *ngIf="error" class="notification-box -error">
|
||||
<div class="notification-box--content">
|
||||
<span [textContent]="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="op-ckeditor-source-element"
|
||||
#opCkeditorReplacementContainer>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,5 @@
|
||||
.op-ckeditor--mode-switch
|
||||
text-align: right
|
||||
|
||||
button.button
|
||||
margin: 0 0 5px 0
|
||||
@@ -58,6 +58,9 @@ import {CopyToClipboardDirective} from "core-app/modules/common/copy-to-clipboar
|
||||
import {highlightColBootstrap} from "./highlight-col/highlight-col.directive";
|
||||
import {HookService} from "../plugins/hook-service";
|
||||
import {HTMLSanitizeService} from "./html-sanitize/html-sanitize.service";
|
||||
import {OpCkeditorComponent} from "core-app/modules/common/ckeditor/op-ckeditor.component";
|
||||
import {CKEditorSetupService} from "core-app/modules/common/ckeditor/ckeditor-setup.service";
|
||||
import {CKEditorPreviewService} from "core-app/modules/common/ckeditor/ckeditor-preview.service";
|
||||
import {ColorsAutocompleter} from "core-app/modules/common/colors/colors-autocompleter.component";
|
||||
import {DynamicCssService} from "./dynamic-css/dynamic-css.service";
|
||||
|
||||
@@ -101,6 +104,9 @@ export function bootstrapModule(injector:Injector) {
|
||||
|
||||
// Table highlight
|
||||
HighlightColDirective,
|
||||
|
||||
// CKEditor
|
||||
OpCkeditorComponent,
|
||||
],
|
||||
declarations: [
|
||||
OpDatePickerComponent,
|
||||
@@ -127,6 +133,11 @@ export function bootstrapModule(injector:Injector) {
|
||||
HighlightColDirective,
|
||||
|
||||
// Add functionality to rails rendered templates
|
||||
CopyToClipboardDirective,
|
||||
|
||||
// CKEditor
|
||||
OpCkeditorComponent,
|
||||
|
||||
CopyToClipboardDirective,
|
||||
ColorsAutocompleter,
|
||||
],
|
||||
@@ -135,6 +146,7 @@ export function bootstrapModule(injector:Injector) {
|
||||
CopyToClipboardDirective,
|
||||
NotificationsContainerComponent,
|
||||
HighlightColDirective,
|
||||
HighlightColDirective,
|
||||
ColorsAutocompleter,
|
||||
],
|
||||
providers: [
|
||||
@@ -148,7 +160,9 @@ export function bootstrapModule(injector:Injector) {
|
||||
AttributeHelpTextsService,
|
||||
ConfigurationService,
|
||||
PathHelperService,
|
||||
HTMLSanitizeService
|
||||
HTMLSanitizeService,
|
||||
CKEditorSetupService,
|
||||
CKEditorPreviewService
|
||||
]
|
||||
})
|
||||
export class OpenprojectCommonModule { }
|
||||
|
||||
@@ -96,6 +96,10 @@ export class EditFieldComponent implements OnDestroy {
|
||||
return this.field.schema;
|
||||
}
|
||||
|
||||
public get resource() {
|
||||
return this.field.resource;
|
||||
}
|
||||
|
||||
public get changeset() {
|
||||
return this.field.changeset;
|
||||
}
|
||||
|
||||
+61
-3
@@ -25,19 +25,28 @@
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import {Component} from "@angular/core";
|
||||
import {Component, ViewChild} from "@angular/core";
|
||||
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
|
||||
import {FormattableEditField} from "core-app/modules/fields/edit/field-types/formattable-edit-field";
|
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
|
||||
import {ICKEditorContext, ICKEditorInstance} from "core-app/modules/common/ckeditor/ckeditor-setup.service";
|
||||
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
|
||||
import {OpCkeditorComponent} from "core-app/modules/common/ckeditor/op-ckeditor.component";
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div class="textarea-wrapper">
|
||||
<div class="op-ckeditor--wrapper op-ckeditor-element">
|
||||
<textarea class="op-ckeditor-source-element" hidden [value]="field.rawValue"></textarea>
|
||||
<op-ckeditor [context]="context"
|
||||
[content]="field.rawValue || ''"
|
||||
(onContentChange)="onContentChange($event)"
|
||||
(onInitialized)="onCkeditorSetup($event)"
|
||||
[ckEditorType]="editorType">
|
||||
</op-ckeditor>
|
||||
</div>
|
||||
<edit-field-controls *ngIf="!handler.inEditMode"
|
||||
[fieldController]="handler"
|
||||
(onSave)="handler.handleUserSubmit()"
|
||||
(onSave)="handleUserSubmit()"
|
||||
(onCancel)="handler.handleUserCancel()"
|
||||
[saveTitle]="field.text.save"
|
||||
[cancelTitle]="field.text.cancel">
|
||||
@@ -47,4 +56,53 @@ import {FormattableEditField} from "core-app/modules/fields/edit/field-types/for
|
||||
})
|
||||
export class FormattableEditFieldComponent extends EditFieldComponent {
|
||||
public field:FormattableEditField;
|
||||
private readonly pathHelper:PathHelperService = this.injector.get(PathHelperService);
|
||||
private readonly Notifications = this.injector.get(NotificationsService);
|
||||
|
||||
@ViewChild(OpCkeditorComponent) instance:OpCkeditorComponent;
|
||||
|
||||
public onContentChange(value:string) {
|
||||
this.field.rawValue = value;
|
||||
}
|
||||
|
||||
public onCkeditorSetup(editor:ICKEditorInstance) {
|
||||
if (!this.resource.isNew) {
|
||||
setTimeout(() => editor.editing.view.focus());
|
||||
}
|
||||
}
|
||||
|
||||
public handleUserSubmit() {
|
||||
this.instance
|
||||
.getTransformedContent()
|
||||
.then((value:string) => {
|
||||
this.field.rawValue = value;
|
||||
this.handler.handleUserSubmit();
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public get context():ICKEditorContext {
|
||||
return {
|
||||
resource: this.resource,
|
||||
macros: 'none' as 'none',
|
||||
previewContext: this.previewContext
|
||||
};
|
||||
}
|
||||
|
||||
public get editorType() {
|
||||
if (this.field.name === 'description') {
|
||||
return 'full';
|
||||
} else {
|
||||
return 'constrained';
|
||||
}
|
||||
}
|
||||
|
||||
public get previewContext() {
|
||||
if (this.resource.isNew && this.resource.project) {
|
||||
return this.resource.project.href;
|
||||
} else if (!this.resource.isNew) {
|
||||
return this.pathHelper.api.v3.work_packages.id(this.resource.id).path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,25 +27,9 @@
|
||||
// ++
|
||||
|
||||
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
|
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
|
||||
import {FormattableEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component";
|
||||
import {
|
||||
CKEditorSetupService,
|
||||
ICKEditorInstance,
|
||||
ICKEditorStatic
|
||||
} from "core-components/ckeditor/ckeditor-setup.service";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
OPBalloonEditor:ICKEditorStatic;
|
||||
OPClassicEditor:ICKEditorStatic;
|
||||
}
|
||||
}
|
||||
|
||||
export class FormattableEditField extends EditField {
|
||||
readonly pathHelper:PathHelperService = this.$injector.get(PathHelperService);
|
||||
readonly ckEditorSetup:CKEditorSetupService = this.$injector.get(CKEditorSetupService);
|
||||
|
||||
// Values used in template
|
||||
public isBusy:boolean = false;
|
||||
public isPreview:boolean = false;
|
||||
@@ -56,64 +40,10 @@ export class FormattableEditField extends EditField {
|
||||
cancel: this.I18n.t('js.inplace.button_cancel', { attribute: this.schema.name })
|
||||
};
|
||||
|
||||
// CKEditor instance
|
||||
public ckeditor:any;
|
||||
|
||||
public get component() {
|
||||
return FormattableEditFieldComponent;
|
||||
}
|
||||
|
||||
public $onInit(container:HTMLElement) {
|
||||
this.setupMarkdownEditor(container);
|
||||
}
|
||||
|
||||
public setupMarkdownEditor(container:HTMLElement) {
|
||||
const element = container.querySelector('.op-ckeditor-source-element') as HTMLElement;
|
||||
|
||||
const context = { resource: this.resource,
|
||||
macros: 'none' as 'none',
|
||||
previewContext: this.previewContext };
|
||||
|
||||
this.ckEditorSetup
|
||||
.create(this.editorType,
|
||||
element,
|
||||
context)
|
||||
.then((editor:ICKEditorInstance) => {
|
||||
this.ckeditor = editor;
|
||||
if (!this.resource.isNew) {
|
||||
setTimeout(() => editor.editing.view.focus());
|
||||
}
|
||||
|
||||
this.updateValueOnEditorChange(editor);
|
||||
});
|
||||
}
|
||||
|
||||
private updateValueOnEditorChange(editor:any) {
|
||||
editor.model.document.on('change', () => {
|
||||
this.rawValue = this.ckeditor.getData();
|
||||
} );
|
||||
}
|
||||
|
||||
private get editorType() {
|
||||
if (this.name === 'description') {
|
||||
return 'full';
|
||||
} else {
|
||||
return 'constrained';
|
||||
}
|
||||
}
|
||||
|
||||
private get previewContext() {
|
||||
if (this.resource.isNew && this.resource.project) {
|
||||
return this.resource.project.href;
|
||||
} else if (!this.resource.isNew) {
|
||||
return this.pathHelper.api.v3.work_packages.id(this.resource.id).path;
|
||||
}
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.ckeditor.setData(this.rawValue);
|
||||
}
|
||||
|
||||
public get rawValue() {
|
||||
if (this.value && this.value.raw) {
|
||||
return this.value.raw;
|
||||
@@ -131,11 +61,7 @@ export class FormattableEditField extends EditField {
|
||||
}
|
||||
|
||||
public isEmpty():boolean {
|
||||
if (this.ckeditor) {
|
||||
return this.ckeditor.getData() === '';
|
||||
} else {
|
||||
return !(this.value && this.value.raw);
|
||||
}
|
||||
return !(this.value && this.value.raw);
|
||||
}
|
||||
|
||||
public submitUnlessInPreview(form:any) {
|
||||
|
||||
@@ -21,9 +21,9 @@ import {OpenProjectFileUploadService} from "core-components/api/op-file-upload/o
|
||||
import {EditorMacrosService} from "core-components/modals/editor/editor-macros.service";
|
||||
import {HTMLSanitizeService} from "../common/html-sanitize/html-sanitize.service";
|
||||
import {PathHelperService} from "../common/path-helper/path-helper.service";
|
||||
import {CKEditorPreviewService} from "core-components/ckeditor/ckeditor-preview.service";
|
||||
import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
|
||||
import {States} from 'core-components/states.service';
|
||||
import {CKEditorPreviewService} from "core-app/modules/common/ckeditor/ckeditor-preview.service";
|
||||
|
||||
/**
|
||||
* Plugin context bridge for plugins outside the CLI compiler context
|
||||
|
||||
@@ -88,7 +88,7 @@ module OpenProject
|
||||
def text_formatting_wrapper(target_id, options)
|
||||
return ''.html_safe unless target_id.present?
|
||||
|
||||
helper = ::OpenProject::TextFormatting::Formats.rich_helper.new
|
||||
helper = ::OpenProject::TextFormatting::Formats.rich_helper.new(self)
|
||||
helper.wikitoolbar_for target_id, options
|
||||
end
|
||||
|
||||
|
||||
@@ -32,7 +32,11 @@ module OpenProject::TextFormatting::Formats
|
||||
module Markdown
|
||||
class Helper
|
||||
|
||||
def initialize; end
|
||||
attr_reader :view_context
|
||||
|
||||
def initialize(view_context)
|
||||
@view_context = view_context
|
||||
end
|
||||
|
||||
def text_formatting_js_includes
|
||||
helpers.javascript_include_tag 'vendor/ckeditor/ckeditor.js'
|
||||
@@ -40,7 +44,7 @@ module OpenProject::TextFormatting::Formats
|
||||
|
||||
def wikitoolbar_for(field_id, **context)
|
||||
# Hide the original textarea
|
||||
helpers.content_for(:additional_js_dom_ready) do
|
||||
view_context.content_for(:additional_js_dom_ready) do
|
||||
js = <<-JAVASCRIPT
|
||||
var field = document.getElementById('#{field_id}');
|
||||
field.style.display = 'none';
|
||||
@@ -52,7 +56,7 @@ module OpenProject::TextFormatting::Formats
|
||||
|
||||
# Pass an optional resource to the CKEditor instance
|
||||
resource = context.fetch(:resource, {})
|
||||
helpers.content_tag 'op-ckeditor-form',
|
||||
helpers.content_tag 'ckeditor-augmented-textarea',
|
||||
'',
|
||||
'textarea-selector': "##{field_id}",
|
||||
'preview-context': context[:preview_context],
|
||||
|
||||
@@ -211,7 +211,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
|
||||
def text_formatting_wrapper(target_id, options)
|
||||
return ''.html_safe unless target_id.present?
|
||||
|
||||
helper = ::OpenProject::TextFormatting::Formats.rich_helper.new
|
||||
helper = ::OpenProject::TextFormatting::Formats.rich_helper.new(@template)
|
||||
helper.wikitoolbar_for target_id, options
|
||||
end
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ JJ Abrams</textarea>
|
||||
context 'an id is missing' do
|
||||
it 'outputs the wysiwyg wrapper' do
|
||||
expect(output).to have_selector 'textarea'
|
||||
expect(output).to have_selector 'op-ckeditor-form'
|
||||
expect(output).to have_selector 'ckeditor-augmented-textarea'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -203,7 +203,7 @@ JJ Abrams</textarea>
|
||||
|
||||
it 'outputs the wysiwyg wrapper' do
|
||||
expect(output).to have_selector 'textarea'
|
||||
expect(output).to have_selector 'op-ckeditor-form'
|
||||
expect(output).to have_selector 'ckeditor-augmented-textarea'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user