Going back from TinyMCE to CKEditor

- Update CKEditor toolbar
- CKEditor: new formating options and stripping empty paragraphs from imports
- fix for other clipboard html cases not beginning with 'p' tag
- Added RemoveFormat button to ckditor toolbar.
- Reorder bower.json
- inline editor and working on line numbers and language setting
- line numbering in inline edit
- changed allowed content, line number display, editor toggling
- included "justify" in ckeditor
- reload original text after ckeditor is disabled
- Reorder and extend ckeditor toolbar.
- fixed save button trigger and inserted a revert button
- set language on editor load (works for inline case only)
This commit is contained in:
Maximilian Krambach 2016-12-01 18:13:09 +01:00 committed by Emanuel Schütze
parent 4ebb8023e3
commit 58b8066249
14 changed files with 209 additions and 136 deletions

View File

@ -178,6 +178,7 @@ OpenSlides uses the following projects or parts of them:
* `angular-bootstrap <http://angular-ui.github.io/bootstrap>`_, License: MIT
* `angular-bootstrap-colorpicker <https://github.com/buberdds/angular-bootstrap-colorpicker>`_, License: MIT
* `angular-chosen-localytics <http://github.com/leocaseiro/angular-chosen>`_, License: MIT
* `angular-ckeditor <https://github.com/lemonde/angular-ckeditor/>`_, License: MIT
* `angular-csv-import <https://github.com/bahaaldine/angular-csv-import>`_, License: MIT
* `angular-formly <http://formly-js.github.io/angular-formly/>`_, License: MIT
* `angular-formly-templates-bootstrap <https://github.com/formly-js/angular-formly-templates-bootstrap>`_, License: MIT
@ -189,13 +190,13 @@ OpenSlides uses the following projects or parts of them:
* `angular-sanitize <http://angularjs.org>`_, License: MIT
* `angular-scroll-glue <https://github.com/Luegg/angularjs-scroll-glue>`_, License: MIT
* `angular-ui-router <http://angular-ui.github.io/ui-router/>`_, License: MIT
* `angular-ui-tinymce <http://angular-ui.github.com>`_, License: MIT
* `angular-ui-tree <https://github.com/angular-ui-tree/angular-ui-tree>`_, License: MIT
* `angular-xeditable <https://github.com/vitalets/angular-xeditable>`_, License: MIT
* `api-check <https://github.com/kentcdodds/api-check>`_, License: MIT
* `bootstrap <http://getbootstrap.com>`_, License: MIT
* `bootstrap-ui-datetime-picker <https://github.com/Gillardo/bootstrap-ui-datetime-picker>`_, License: MIT
* `chosen <http://harvesthq.github.io/chosen/>`_, License: MIT
* `ckeditor <http://ckeditor.com>`_, License: For licensing, see LICENSE.md or http://ckeditor.com/license.
* `font-awesome-bower <https://github.com/tdg5/font-awesome-bower>`_, License: MIT
* `jquery <https://jquery.com>`_, License: MIT
* `jquery.cookie <https://plugins.jquery.com/cookie>`_, License: MIT

View File

@ -8,6 +8,7 @@
"angular-bootstrap": "~2.1.3",
"angular-bootstrap-colorpicker": "~3.0.25",
"angular-chosen-localytics": "~1.5.0",
"angular-ckeditor": "~1.0.3",
"angular-csv-import": "0.0.36",
"angular-file-saver": "~1.1.2",
"angular-formly": "~8.4.0",
@ -19,11 +20,11 @@
"angular-sanitize": "~1.5.8",
"angular-scroll-glue": "~2.0.7",
"angular-ui-router": "~0.3.1",
"angular-ui-tinymce": "~0.0.17",
"angular-ui-tree": "~2.22.0",
"angular-xeditable": "~0.5.0",
"bootstrap-css-only": "~3.3.6",
"bootstrap-ui-datetime-picker": "~2.4.0",
"ckeditor": "~4.6.1",
"docxtemplater": "~2.1.5",
"font-awesome-bower": "~4.5.0",
"jquery.cookie": "~1.4.1",
@ -35,9 +36,7 @@
"ngstorage": "~0.3.11",
"ngBootbox": "~0.1.3",
"pdfmake": "bpampuch/pdfmake#214ec161c11fadb8f02c08f2e3bea0576ac4c9fb",
"roboto-fontface": "~0.6.0",
"tinymce": "~4.4.3",
"tinymce-i18n": "OpenSlides/tinymce-i18n#a186ad61e0aa30fdf657e88f405f966d790f0805"
"roboto-fontface": "~0.6.0"
},
"overrides": {
"pdfmake": {
@ -57,11 +56,28 @@
"fonts/Roboto-Condensed/Roboto-Condensed-Light.woff"
]
},
"tinymce": {
"ckeditor": {
"main": [
"tinymce.js",
"themes/modern/theme.js",
"plugins/*/plugin.js"
"ckeditor.js",
"skins/moono-lisa/*",
"lang/en.js",
"lang/de.js",
"lang/pt.js",
"lang/es.js",
"lang/fr.js",
"lang/cs.js",
"plugins/about/*",
"plugins/clipboard/*",
"plugins/dialog/*",
"plugins/find/*",
"plugins/image/*",
"plugins/justify/*",
"plugins/liststyle/*",
"plugins/magicline/*",
"plugins/pastefromword/*",
"plugins/showblocks/*",
"plugins/table/*",
"plugins/tabletools/*"
]
}
},

View File

@ -108,37 +108,12 @@ gulp.task('angular-chosen-img', function () {
.pipe(gulp.dest(path.join(output_directory, 'css')));
});
// Catches all skins files for TinyMCE editor.
gulp.task('tinymce-skins', function () {
return gulp.src(path.join('bower_components', 'tinymce', 'skins', '**'))
.pipe(gulp.dest(path.join(output_directory, 'tinymce', 'skins')));
// Extra task only for CKEditor
gulp.task('ckeditor', function () {
return gulp.src(path.join('bower_components', 'ckeditor', '**'))
.pipe(gulp.dest(path.join(output_directory, 'ckeditor')));
});
// Catches all required i18n files for TinyMCE editor.
gulp.task('tinymce-i18n', function () {
return gulp.src([
'bower_components/tinymce-i18n/langs/en_GB.js',
'bower_components/tinymce-i18n/langs/cs.js',
'bower_components/tinymce-i18n/langs/de.js',
'bower_components/tinymce-i18n/langs/es.js',
'bower_components/tinymce-i18n/langs/fr_FR.js',
'bower_components/tinymce-i18n/langs/pt_PT.js',
])
.pipe(rename(function (path) {
if (path.basename === 'en_GB') {
path.basename = 'en';
} else if (path.basename === 'fr_FR') {
path.basename = 'fr';
} else if (path.basename === 'pt_PT') {
path.basename = 'pt';
}
}))
.pipe(gulpif(argv.production, uglify()))
.pipe(gulp.dest(path.join(output_directory, 'tinymce', 'i18n')));
});
// Combines all TinyMCE related tasks.
gulp.task('tinymce', ['tinymce-skins', 'tinymce-i18n'], function () {});
// Compiles translation files (*.po) to *.json and saves them in the directory
// openslides/static/i18n/.
@ -151,7 +126,7 @@ gulp.task('translations', function () {
});
// Gulp default task. Runs all other tasks before.
gulp.task('default', ['js', 'js-libs', 'templates', 'css-libs', 'fonts-libs', 'tinymce', 'angular-chosen-img', 'translations'], function () {});
gulp.task('default', ['js', 'js-libs', 'templates', 'css-libs', 'fonts-libs', 'ckeditor', 'angular-chosen-img', 'translations'], function () {});
/**

View File

@ -548,33 +548,96 @@ angular.module('OpenSlidesApp.core', [
}
])
// Options for TinyMCE editor used in various create and edit views.
// Required in core/base.js because MotionComment factory which used this
// factory has to placed in motions/base.js.
// Options for CKEditor used in various create and edit views.
.factory('Editor', [
'gettextCatalog',
function (gettextCatalog) {
function(gettextCatalog) {
return {
getOptions: function (images, inlineMode) {
if (inlineMode === undefined) {
inlineMode = false;
}
getOptions: function (images) {
return {
language_url: '/static/tinymce/i18n/' + gettextCatalog.getCurrentLanguage() + '.js',
theme_url: '/static/js/openslides-libs.js',
skin_url: '/static/tinymce/skins/lightgray/',
inline: inlineMode,
browser_spellcheck: true,
image_advtab: true,
image_list: images,
plugins: [
'lists link autolink charmap preview searchreplace code fullscreen',
'paste textcolor colorpicker image imagetools wordcount'
],
menubar: '',
toolbar: 'undo redo searchreplace | styleselect | bold italic underline strikethrough ' +
'forecolor backcolor removeformat | bullist numlist | outdent indent | ' +
'link image charmap table | code preview fullscreen'
on: {
instanceReady: function() {
// add a listener to ckeditor that parses the clipboard content and, after the regular filter,
// additionally strips out all empty <p> paragraphs
// TODO: check all kind of clipboard html content if "isEmpty" is a reliable property
this.on('paste', function(evt) {
if (evt.data.type == 'html') {
var fragment = CKEDITOR.htmlParser.fragment.fromHtml(evt.data.dataValue);
var writer = new CKEDITOR.htmlParser.basicWriter();
// html content will now be in a dom-like structure inside 'fragment'.
this.filter.applyTo(fragment);
if (fragment.children) {
var new_content_children = [];
for (var i = 0; i < fragment.children.length; i++) {
var empty = true;
if (fragment.children[i].children){
for (var j = 0; j < fragment.children[i].children.length; j++) {
var child = fragment.children[i].children[j];
if (child.name != 'p' && child.name != 'br') {
empty = false;
} else if (child.isEmpty !== true) {
empty = false;
}
}
if (empty === false) {
new_content_children.push(fragment.children[i]);
}
} else {
if (fragment.children[i].name != 'p' && fragment.children[i].name != 'br' &&
fragment.children[i].isEmpty !== true){
new_content_children.push(fragment.children[i]);
}
}
}
fragment.children = new_content_children;
}
fragment.writeHtml(writer);
evt.data.dataValue = writer.getHtml();
}
});
}
},
customConfig: '',
disableNativeSpellChecker: false,
language_list: [
'fr:français',
'es:español',
'pt:português',
'en:english',
'de:deutsch',
'cs:čeština'],
language: gettextCatalog.getCurrentLanguage(),
allowedContent:
'h1 h2 h3 b i u strike sup sub strong em;' +
'blockquote p pre table' +
'(text-align-left,text-align-center,text-align-right,text-align-justify){text-align};' +
'a[!href];' +
'img[!src,alt]{width,height,float};' +
'tr th td caption;' +
'li; ol[start]{list-style-type};' +
'ul{list-style};' +
'span[data-line-number,contenteditable]{color,background-color}(os-line-number,line-number-*);' +
'br(os-line-break);',
// there seems to be an error in CKeditor that parses spaces in extraPlugins as part of the plugin name.
extraPlugins: 'colorbutton,find,liststyle,sourcedialog,justify,showblocks',
removePlugins: 'wsc,scayt,a11yhelp,filebrowser,sourcearea',
removeButtons: 'Scayt,Anchor,Styles,HorizontalRule',
toolbarGroups: [
{ name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
{ name: 'editing', groups: [ 'find', 'selection', 'spellchecker', 'editing' ] },
{ name: 'links', groups: [ 'links' ] },
{ name: 'insert', groups: [ 'insert' ] },
{ name: 'tools', groups: [ 'tools' ] },
{ name: 'document', groups: [ 'mode' ] },
'/',
{ name: 'styles', groups: [ 'styles' ] },
{ name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
{ name: 'colors', groups: [ 'colors' ] },
{ name: 'paragraph', groups: [ 'list', 'indent' ] },
{ name: 'align'},
{ name: 'paragraph', groups: [ 'blocks' ] }
]
};
}
};

View File

@ -18,7 +18,7 @@ angular.module('OpenSlidesApp.core.site', [
'ngMessages',
'ngCsvImport',
'ngStorage',
'ui.tinymce',
'ckeditor',
'luegg.directives',
'xeditable',
])

View File

@ -1,2 +1,2 @@
<!-- custom angular formly template for tinymce textarea field -->
<textarea ui-tinymce="options.data.tinymceOption" ng-model="model[options.key]" class="form-control"></textarea>
<!-- custom angular formly template for ckeditor textarea field -->
<textarea ckeditor="options.data.ckeditorOptions" ng-model="model[options.key]" class="form-control"></textarea>

View File

@ -10,10 +10,19 @@
<link rel="stylesheet" href="static/css/openslides-libs.css">
<link rel="stylesheet" href="static/css/app.css">
<link rel="icon" href="/static/img/favicon.png">
<!-- TODO: there is probably a better place for it:-->
<script>
window.CKEDITOR_BASEPATH = '/static/ckeditor/';
</script>
<!-- -->
<script src="static/js/openslides-libs.js"></script>
<script src="static/js/openslides.js"></script>
<script src="static/js/openslides-templates.js"></script>
<!-- TODO: move inside openslides-libs (?):-->
<script>
CKEDITOR.disableAutoInline = true;
</script>
<!-- -->
<div id="wrapper" ng-cloak>
<!-- Header -->

View File

@ -546,7 +546,7 @@ angular.module('OpenSlidesApp.motions', [
label: field.name,
},
data: {
tinymceOption: Editor.getOptions()
ckeditorOptions: Editor.getOptions()
},
hide: !operator.hasPerms("motions.can_see_and_manage_comments")
};

View File

@ -61,13 +61,12 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
'Motion',
'Config',
'$timeout',
function (Editor, Motion, Config, $timeout) {
'gettextCatalog',
function (Editor, Motion, Config, $timeout, gettextCatalog) {
var obj = {
active: false,
changed: false,
trivialChange: false,
editor: null,
lineBrokenText: null,
originalHtml: null
};
@ -76,62 +75,77 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
obj.init = function (_scope, _motion) {
$scope = _scope;
motion = _motion;
obj.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
obj.originalHtml = obj.lineBrokenText;
};
obj.tinymceOptions = Editor.getOptions(null, true);
obj.tinymceOptions.readonly = 1;
obj.tinymceOptions.setup = function (editor) {
obj.editor = editor;
editor.on('init', function () {
obj.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
obj.editor.setContent(obj.lineBrokenText);
obj.originalHtml = obj.editor.getContent();
obj.changed = false;
});
editor.on('change', function () {
obj.changed = (editor.getContent() != obj.originalHtml);
});
editor.on('undo', function () {
obj.changed = (editor.getContent() != obj.originalHtml);
});
obj.ckeditorOptions = Editor.getOptions();
obj.ckeditorOptions.readOnly = true;
obj.isEditable = false;
obj.changed = false;
};
obj.setVersion = function (_motion, versionId) {
motion = _motion; // If this is not updated,
obj.lineBrokenText = motion.getTextWithLineBreaks(versionId);
obj.originalHtml = motion.getTextWithLineBreaks(versionId);
obj.changed = false;
obj.active = false;
obj.editor.setReadOnly(true);
if (obj.editor) {
obj.editor.setContent(obj.lineBrokenText);
obj.editor.setMode('readonly');
obj.originalHtml = obj.editor.getContent();
} else {
obj.originalHtml = obj.lineBrokenText;
obj.editor.setData(obj.originalHtml);
}
};
obj.enable = function () {
obj.editor.setMode('design');
obj.active = true;
obj.changed = false;
obj.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
obj.editor.setContent(obj.lineBrokenText);
obj.originalHtml = obj.editor.getContent();
$timeout(function () {
obj.editor.focus();
}, 100);
if (motion.isAllowed('update')) {
obj.active = true;
obj.isEditable = true;
obj.ckeditorOptions.language = gettextCatalog.getCurrentLanguage();
obj.editor = CKEDITOR.inline('view-original-inline-editor', obj.ckeditorOptions);
obj.editor.on('change', function () {
$timeout(function() {
if (obj.editor.getData() != obj.originalHtml) {
obj.changed = true;
} else {
obj.changed = false;
}
});
});
obj.revert();
} else {
obj.disable();
}
};
obj.disable = function () {
obj.editor.setMode('readonly');
obj.active = false;
obj.changed = false;
obj.lineBrokenText = obj.originalHtml;
obj.editor.setContent(obj.originalHtml);
if (obj.editor) {
obj.editor.setReadOnly(true);
obj.editor.setData(obj.originalHtml, {
callback: function() {
obj.editor.destroy();
}
});
}
$timeout(function() {
obj.active = false;
obj.changed = false;
obj.isEditable = false;
});
};
// sets editor content to the initial motion state
obj.revert = function() {
if (obj.editor) {
obj.originalHtml = motion.getTextWithLineBreaks($scope.version);
obj.editor.setData(
motion.getTextWithLineBreaks($scope.version), {
callback: function() {
obj.originalHtml = obj.editor.getData();
obj.editor.setReadOnly(false);
$timeout(function() {
obj.changed = false;
});
$timeout(function () {
obj.editor.focus();
}, 100);
}
});
}
};
obj.save = function () {
@ -139,7 +153,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
throw 'No permission to update motion';
}
motion.setTextStrippingLineBreaks(obj.editor.getContent());
motion.setTextStrippingLineBreaks(obj.editor.getData());
motion.disable_versioning = (obj.trivialChange && Config.get('motions_allow_disable_versioning').value);
Motion.inject(motion);
@ -147,11 +161,13 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
Motion.save(motion, {method: 'PATCH'}).then(
function (success) {
$scope.showVersion(motion.getVersion(-1));
obj.revert();
},
function (error) {
// save error: revert all changes by restore
// (refresh) original motion object from server
Motion.refresh(motion);
obj.revert();
var message = '';
for (var e in error.data) {
message += e + ': ' + error.data[e] + ' ';

View File

@ -397,7 +397,7 @@ angular.module('OpenSlidesApp.motions.site', [
required: false
},
data: {
tinymceOption: Editor.getOptions()
ckeditorOptions: Editor.getOptions()
}
}
];
@ -492,7 +492,7 @@ angular.module('OpenSlidesApp.motions.site', [
required: true
},
data: {
tinymceOption: Editor.getOptions(images)
ckeditorOptions: Editor.getOptions(images)
}
},
{
@ -502,7 +502,7 @@ angular.module('OpenSlidesApp.motions.site', [
label: gettextCatalog.getString('Reason'),
},
data: {
tinymceOption: Editor.getOptions(images)
ckeditorOptions: Editor.getOptions(images)
}
},
{

View File

@ -1,30 +1,23 @@
<!-- Original view, Inline Editing is possible -->
<div ng-if="viewChangeRecommendations.mode == 'original' && motion.isAllowed('update') && version == motion.getVersion(-1).id">
<div ng-show="inlineEditing.active">
<div ui-tinymce="inlineEditing.tinymceOptions" ng-model="inlineEditing.lineBrokenText"
class="motion-text line-numbers-{{ lineNumberMode }}"></div>
<!-- Original view -->
<div ng-if="viewChangeRecommendations.mode == 'original' && version == motion.getVersion(-1).id">
<div id="view-original-inline-editor" ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}"
contenteditable= "{{ inlineEditing.isEditable }}">
</div>
<div ng-show="!inlineEditing.active" ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}"></div>
<div class="motion-save-toolbar" ng-class="{ 'visible': (inlineEditing.changed && inlineEditing.active) }">
<div class="changed-hint" translate>The text has been changed.</div>
<button type="button" ng-click="inlineEditing.save()" class="btn btn-primary" translate>
Save
</button>
<button type="button" ng-click="inlineEditing.revert()" class="btn btn-primary" translate>
Revert
</button>
<label ng-if="motion.state.versioning && config('motions_allow_disable_versioning')">
<input type="checkbox" ng-model="inlineEditing.trivialChange" value="1">
<span translate>Trivial change</span>
</label>
</div>
</div>
<!-- Original view, Inline Editing is NOT possible -->
<div ng-if="viewChangeRecommendations.mode == 'original' && !(motion.isAllowed('update') && version == motion.getVersion(-1).id)">
<div ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}"></div>
</div>
<!-- Original view, Change list -->
<ul ng-if="viewChangeRecommendations.mode == 'original'" ng-show="lineNumberMode != 'none'"
class="change-recommendation-list">

View File

@ -118,7 +118,7 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics'])
label: gettextCatalog.getString('Text')
},
data: {
tinymceOption: Editor.getOptions(images)
ckeditorOptions: Editor.getOptions(images)
}
}];
// attachments

View File

@ -446,7 +446,7 @@ angular.module('OpenSlidesApp.users.site', [
label: gettextCatalog.getString('About me'),
},
data: {
tinymceOption: Editor.getOptions(images)
ckeditorOptions: Editor.getOptions(images)
},
hideExpression: '!model.more'
}
@ -801,7 +801,7 @@ angular.module('OpenSlidesApp.users.site', [
'user',
function($scope, $state, Editor, User, user) {
$scope.user = user; // autoupdate is not activated
$scope.tinymceOption = Editor.getOptions();
$scope.ckeditorOptions = Editor.getOptions();
$scope.save = function (user) {
User.save(user).then(
function(success) {

View File

@ -37,7 +37,7 @@
</div>
<div class="form-group">
<label for="textAbout" translate>About me</label>
<textarea ng-model="user.about_me" ui-tinymce="tinymceOption" class="form-control" name="textAbout" />
<textarea ng-model="user.about_me" class="form-control" name="textAbout" />
</div>
<button type="submit" ng-click="save(user)" class="btn btn-primary" translate>