Move ckeditor components into one and add markdown source

This commit is contained in:
Oliver Günther
2018-09-17 12:22:20 +02:00
parent fe21fd7992
commit 03448e758e
25 changed files with 401 additions and 171 deletions
+1 -1
View File
@@ -36,7 +36,7 @@
subject.val(result.subject);
$('op-ckeditor-form')
$('ckeditor-augmented-textarea')
.data('editor')
.then(function(editor) {
editor.setData(result.content);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -14
View File
@@ -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
+1 -1
View File
@@ -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)
+2 -6
View File
@@ -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>
+5 -1
View File
@@ -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'
+4 -8
View File
@@ -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
@@ -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
});
@@ -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">
@@ -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'
@@ -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;
}
@@ -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
+1 -1
View File
@@ -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],
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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