Merge pull request #1710 from aignatov-bio/feature/marvinjs-integration

MarvinJS integration
This commit is contained in:
aignatov-bio 2019-05-08 15:05:55 +02:00 committed by GitHub
commit 3cbabf1ffd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1265 additions and 40 deletions

5
.gitignore vendored
View file

@ -74,3 +74,8 @@ spec/addons
# RVM/rbenv ruby version for local development
.ruby-version
#ignore marvinJs
public/marvinjs
public/marvin4js-license.cxl

View file

@ -41,6 +41,7 @@
//= require select2_customization
//= require shared/inline_editing
//= require turbolinks
//= require marvinjslauncher
// Initialize links for submitting forms. This is useful for submitting

View file

@ -0,0 +1,320 @@
/* global MarvinJSUtil, I18n, FilePreviewModal, tinymce */
/* global TinyMCE, PerfectScrollbar, ChemicalizeMarvinJs */
/* eslint-disable no-param-reassign */
/* eslint-disable wrap-iife */
var MarvinJsEditor;
if (typeof ChemicalizeMarvinJs !== 'undefined') {
ChemicalizeMarvinJs.createEditor('#marvinjs-sketch');
}
MarvinJsEditor = (function() {
var marvinJsModal = $('#MarvinJsModal');
var marvinJsContainer = $('#marvinjs-editor');
var marvinJsObject = $('#marvinjs-sketch');
var emptySketch = '<cml><MDocument></MDocument></cml>';
var sketchName = marvinJsModal.find('.file-name input');
var loadEditor = () => {
if (marvinJsObject[0].nodeName === 'DIV') {
return MarvinJSUtil.getEditor('#' + marvinJsObject.children()[0].id);
}
return MarvinJSUtil.getEditor('#marvinjs-sketch');
};
var loadPackages = () => {
if (marvinJsObject[0].nodeName === 'DIV') {
return MarvinJSUtil.getPackage('#' + marvinJsObject.children()[0].id);
}
return MarvinJSUtil.getPackage('#marvinjs-sketch');
};
function preloadActions(config) {
if (config.mode === 'new' || config.mode === 'new-tinymce') {
loadEditor().then(function(sketcherInstance) {
sketcherInstance.importStructure('mrv', emptySketch);
sketchName.val(I18n.t('marvinjs.new_sketch'));
});
} else if (config.mode === 'edit') {
loadEditor().then(function(sketcherInstance) {
sketcherInstance.importStructure('mrv', config.data);
sketchName.val(config.name);
});
} else if (config.mode === 'edit-tinymce') {
loadEditor().then(function(sketcherInstance) {
$.get(config.marvinUrl, function(result) {
sketcherInstance.importStructure('mrv', result.description);
sketchName.val(result.name);
});
});
}
}
function createExporter(marvin, imageType) {
var inputFormat = 'mrv';
var settings = {
width: 900,
height: 900
};
var params = {
imageType: imageType,
settings: settings,
inputFormat: inputFormat
};
return new marvin.ImageExporter(params);
}
function assignImage(target, data) {
target.attr('src', data);
return data;
}
function TinyMceBuildHTML(json) {
var imgstr = "<img src='" + json.image.url + "'";
imgstr += " data-mce-token='" + json.image.token + "'";
imgstr += " data-source-id='" + json.image.source_id + "'";
imgstr += " data-source-type='" + json.image.source_type + "'";
imgstr += " alt='description-" + json.image.token + "' />";
return imgstr;
}
return Object.freeze({
open: function(config) {
MarvinJsEditor().team_sketches();
preloadActions(config);
$(marvinJsModal).modal('show');
$(marvinJsObject)
.css('width', (marvinJsContainer.width() - 200) + 'px')
.css('height', marvinJsContainer.height() + 'px');
marvinJsModal.find('.file-save-link').off('click').on('click', () => {
if (config.mode === 'new') {
MarvinJsEditor().save(config);
} else if (config.mode === 'edit') {
MarvinJsEditor().update(config);
} else if (config.mode === 'new-tinymce') {
config.objectType = 'TinyMceAsset';
MarvinJsEditor().save_with_image(config);
} else if (config.mode === 'edit-tinymce') {
MarvinJsEditor().update_tinymce(config);
}
});
},
initNewButton: function(selector) {
$(selector).off('click').on('click', function() {
var objectId = this.dataset.objectId;
var objectType = this.dataset.objectType;
var marvinUrl = this.dataset.marvinUrl;
var container = this.dataset.sketchContainer;
MarvinJsEditor().open({
mode: 'new',
objectId: objectId,
objectType: objectType,
marvinUrl: marvinUrl,
container: container
});
});
},
save: function(config) {
loadEditor().then(function(sketcherInstance) {
sketcherInstance.exportStructure('mrv').then(function(source) {
$.post(config.marvinUrl, {
description: source,
object_id: config.objectId,
object_type: config.objectType,
name: sketchName.val()
}, function(result) {
var newAsset;
if (config.objectType === 'Step') {
newAsset = $(result.html);
newAsset.find('.file-preview-link').css('top', '-300px');
newAsset.addClass('new').prependTo($(config.container));
setTimeout(function() {
newAsset.find('.file-preview-link').css('top', '0px');
}, 200);
FilePreviewModal.init();
}
$(marvinJsModal).modal('hide');
});
});
});
},
save_with_image: function(config) {
loadEditor().then(function(sketcherInstance) {
sketcherInstance.exportStructure('mrv').then(function(mrvDescription) {
loadPackages().then(function(sketcherPackage) {
sketcherPackage.onReady(function() {
var exporter = createExporter(sketcherPackage, 'image/jpeg');
exporter.render(mrvDescription).then(function(image) {
$.post(config.marvinUrl, {
description: mrvDescription,
object_id: config.objectId,
object_type: config.objectType,
name: sketchName.val(),
image: image
}, function(result) {
var json = tinymce.util.JSON.parse(result);
config.editor.execCommand('mceInsertContent', false, TinyMceBuildHTML(json));
TinyMCE.updateImages(config.editor);
$(marvinJsModal).modal('hide');
});
});
});
});
});
});
},
update: function(config) {
loadEditor().then(function(sketcherInstance) {
sketcherInstance.exportStructure('mrv').then(function(source) {
$.ajax({
url: config.marvinUrl,
data: {
description: source,
name: sketchName.val()
},
dataType: 'json',
type: 'PUT',
success: function(json) {
$(marvinJsModal).modal('hide');
config.reloadImage.src.val(json.description);
$(config.reloadImage.sketch).find('.attachment-label').text(json.name);
MarvinJsEditor().create_preview(
config.reloadImage.src,
$(config.reloadImage.sketch).find('img')
);
}
});
});
});
},
update_tinymce: function(config) {
loadEditor().then(function(sketcherInstance) {
sketcherInstance.exportStructure('mrv').then(function(mrvDescription) {
loadPackages().then(function(sketcherPackage) {
sketcherPackage.onReady(function() {
var exporter = createExporter(sketcherPackage, 'image/jpeg');
exporter.render(mrvDescription).then(function(image) {
$.ajax({
url: config.marvinUrl,
data: {
description: mrvDescription,
name: sketchName.val(),
object_type: 'TinyMceAsset',
image: image
},
dataType: 'json',
type: 'PUT',
success: function(json) {
config.image[0].src = json.url;
$(marvinJsModal).modal('hide');
}
});
});
});
});
});
});
},
create_preview: function(source, target) {
loadPackages().then(function(sketcherInstance) {
sketcherInstance.onReady(function() {
var exporter = createExporter(sketcherInstance, 'image/jpeg');
var sketchConfig = source.val();
exporter.render(sketchConfig).then(function(result) {
assignImage(target, result);
});
});
});
},
create_download_link: function(source, link, filename) {
loadPackages().then(function(sketcherInstance) {
sketcherInstance.onReady(function() {
var exporter = createExporter(sketcherInstance, 'image/jpeg');
var sketchConfig = source.val();
exporter.render(sketchConfig).then(function(result) {
link.attr('href', result);
link.attr('download', filename);
});
});
});
},
delete_sketch: function(url, object) {
$.ajax({
url: url,
dataType: 'json',
type: 'DELETE',
success: function() {
$(object).remove();
}
});
},
team_sketches: function() {
var ps = new PerfectScrollbar(marvinJsContainer.find('.marvinjs-team-sketch')[0]);
marvinJsContainer.find('.sketch-container').remove();
$.get('/marvin_js_assets/team_sketches', function(result) {
$(result.html).appendTo(marvinJsContainer.find('.marvinjs-team-sketch'));
$.each(result.sketches, function(i, sketch) {
var sketchObj = marvinJsContainer.find('.marvinjs-team-sketch .sketch-container[data-sketch-id="' + sketch + '"]');
var src = sketchObj.find('#description');
var dest = sketchObj.find('img');
MarvinJsEditor().create_preview(src, dest);
setTimeout(() => { ps.update(); }, 500);
marvinJsContainer.find('.sketch-container').click(function() {
var sketchContainer = $(this);
loadEditor().then(function(sketcherInstance) {
sketcherInstance.importStructure('mrv', sketchContainer.find('#description').val());
});
});
});
});
}
});
});
(function() {
'use strict';
tinymce.PluginManager.requireLangPack('MarvinJsPlugin');
tinymce.create('tinymce.plugins.MarvinJsPlugin', {
MarvinJsPlugin: function(ed) {
var editor = ed;
function openMarvinJs() {
MarvinJsEditor().open({
mode: 'new-tinymce',
marvinUrl: '/marvin_js_assets',
editor: editor
});
}
// Add a button that opens a window
editor.addButton('marvinjsplugin', {
tooltip: I18n.t('marvinjs.new_button'),
icon: 'file-invoice',
onclick: openMarvinJs
});
// Adds a menu item to the tools menu
editor.addMenuItem('marvinjsplugin', {
text: I18n.t('marvinjs.new_button'),
icon: 'file-invoice',
context: 'insert',
onclick: openMarvinJs
});
}
});
tinymce.PluginManager.add(
'marvinjsplugin',
tinymce.plugins.MarvinJsPlugin
);
})();

View file

@ -374,7 +374,8 @@
}
function initCallBacks() {
applyCreateWopiFileCallback();
applyCreateWopiFileCallback()
if (typeof(MarvinJsEditor) !== 'undefined') MarvinJsEditor().initNewButton('.new-marvinjs-upload-button');
applyCheckboxCallBack();
applyStepCompletedCallBack();
applyEditCallBack();

View file

@ -1,7 +1,8 @@
/* eslint no-underscore-dangle: ["error", { "allowAfterThis": true }]*/
/* eslint no-use-before-define: ["error", { "functions": false }]*/
/* eslint-disable no-underscore-dangle */
/* global Uint8Array fabric tui animateSpinner setupAssetsLoading I18n PerfectScrollbar*/
/* global Uint8Array fabric tui animateSpinner
setupAssetsLoading I18n PerfectScrollbar MarvinJsEditor */
//= require assets
var FilePreviewModal = (function() {
@ -18,10 +19,15 @@ var FilePreviewModal = (function() {
$('.file-preview-link').off('click');
$('.file-preview-link').click(function(e) {
e.preventDefault();
name = $(this).find('p').text();
name = $(this).find('.attachment-label').text();
url = $(this).data('preview-url');
downloadUrl = $(this).attr('href');
if ($(this).data('asset-type') === 'marvin-sketch') {
openMarvinPrevieModal(name, $(this).find('#description'), this);
return true;
}
openPreviewModal(name, url, downloadUrl);
return true;
});
}
@ -388,23 +394,39 @@ var FilePreviewModal = (function() {
dataUpload.append('image', imageBlob);
animateSpinner(null, true);
$.ajax({
type: 'POST',
url: '/files/' + data.id + '/update_image',
data: dataUpload,
contentType: false,
processData: false,
success: function(res) {
$('#modal_link' + data.id).parent().html(res.html);
setupAssetsLoading();
}
}).done(function() {
function closeEditor() {
animateSpinner(null, false);
imageEditor.destroy();
imageEditor = {};
$('#tui-image-editor').html('');
$('#fileEditModal').modal('hide');
});
}
if (data.mode === 'tinymce') {
$.ajax({
type: 'PUT',
url: data.url,
data: dataUpload,
contentType: false,
processData: false,
success: function(res) {
data.image.src = res.url;
}
}).done(function() { closeEditor(); });
} else {
$.ajax({
type: 'POST',
url: '/files/' + data.id + '/update_image',
data: dataUpload,
contentType: false,
processData: false,
success: function(res) {
$('#modal_link' + data.id).parent().html(res.html);
setupAssetsLoading();
}
}).done(function() { closeEditor(); });
}
});
window.onresize = function() {
@ -421,8 +443,7 @@ var FilePreviewModal = (function() {
dataType: 'json',
success: function(data) {
var link = modal.find('.file-download-link');
modal.find('.file-preview-container').empty();
modal.find('.file-wopi-controls').empty();
clearPrevieModal();
if (Object.prototype.hasOwnProperty.call(data, 'wopi-controls')) {
modal.find('.file-wopi-controls').html(data['wopi-controls']);
}
@ -520,7 +541,52 @@ var FilePreviewModal = (function() {
});
}
function clearPrevieModal() {
var modal = $('#filePreviewModal');
modal.find('.file-preview-container').empty();
modal.find('.file-wopi-controls').empty();
modal.find('.file-edit-link').css('display', 'none');
}
function openMarvinPrevieModal(name, src, sketch) {
var modal = $('#filePreviewModal');
var link = modal.find('.file-download-link');
var target;
clearPrevieModal();
modal.modal('show');
modal.find('.file-preview-container')
.append($('<img>').attr('src', '').attr('alt', ''));
target = modal.find('.file-preview-container').find('img');
MarvinJsEditor().create_preview(src, target);
MarvinJsEditor().create_download_link(src, link, name);
modal.find('.file-name').text(name);
$.get(sketch.dataset.updateUrl, function(result) {
if (!readOnly && result.editable) {
modal.find('.file-edit-link').css('display', '');
modal.find('.file-edit-link').off().click(function(ev) {
ev.preventDefault();
ev.stopPropagation();
modal.modal('hide');
MarvinJsEditor().open({
mode: 'edit',
data: src.val(),
name: name,
marvinUrl: sketch.dataset.updateUrl,
reloadImage: {
src: src,
sketch: sketch
}
});
});
} else {
modal.find('.file-edit-link').css('display', 'none');
}
});
}
return Object.freeze({
init: initPreviewModal
init: initPreviewModal,
imageEditor: initImageEditor
});
}(window));

View file

@ -1,4 +1,4 @@
/* global _ hljs tinyMCE SmartAnnotation */
/* global _ hljs tinyMCE SmartAnnotation MarvinJsEditor FilePreviewModal */
/* eslint-disable no-unused-vars */
var TinyMCE = (function() {
@ -37,11 +37,84 @@ var TinyMCE = (function() {
}
}
function initImageToolBar(editor) {
var editorForm = $(editor.getContainer()).closest('form');
var editorContainer = $(editor.getContainer());
var menuBar = editorForm.find('.mce-menubar.mce-toolbar.mce-first .mce-flow-layout');
var editorToolbar = editorForm.find('.mce-top-part');
var editorIframe = $('#' + editor.id).prev().find('.mce-edit-area iframe');
$('<div class="tinymce-active-object-handler" style="display:none">'
+ '<a class="file-download-link tool-button" href="#" data-turbolinks="false"><i class="mce-ico mce-i-donwload"></i></a>'
+ '<span class="file-edit-link tool-button" href="#" data-turbolinks="false"><i class="mce-ico mce-i-pencil"></i></span>'
+ '<span class="file-image-editor-link tool-button" href="#" data-turbolinks="false"><i class="mce-ico mce-i-image"></i></span>'
+ '</div>').appendTo(editorToolbar.find('.mce-stack-layout'));
editorIframe.contents().click(function() {
var marvinJsEdit;
setTimeout(() => {
var image = editorIframe.contents().find('img[data-mce-selected="1"]');
var editLink;
var imageEditorLink;
if (image.length > 0) {
image.on('load', function() {
editor.fire('Dirty');
});
editorContainer.find('.tinymce-active-object-handler').css('display', 'block');
editorContainer.find('.tinymce-active-object-handler .file-download-link')
.attr('href', image[0].src)
.attr('download', 'tinymce-image');
// Edit link
editLink = editorContainer.find('.tinymce-active-object-handler .file-edit-link');
if (image[0].dataset.sourceId) {
editLink.css('display', 'inline-block');
marvinJsEdit = (image[0].dataset.sourceType === 'MarvinJsAsset' && typeof (MarvinJsEditor) !== 'undefined');
if (!marvinJsEdit) editLink.css('display', 'none');
editLink.on('click', function() {
if (marvinJsEdit) {
MarvinJsEditor().open({
mode: 'edit-tinymce',
marvinUrl: '/marvin_js_assets/' + image[0].dataset.sourceId,
image: image
});
}
});
} else {
editLink.css('display', 'none');
editLink.off('click');
}
// imaged editor Link
imageEditorLink = editorContainer.find('.tinymce-active-object-handler .file-image-editor-link');
if (image[0].dataset.mceToken && image[0].dataset.sourceId) {
imageEditorLink.css('display', 'inline-block');
imageEditorLink.on('click', function() {
FilePreviewModal.imageEditor({
'download-url': image[0].src,
filename: 'tinymce-image.jpg',
mode: 'tinymce',
url: '/tiny_mce_assets/' + image[0].dataset.mceToken,
quality: 100,
'mime-type': 'image/jpeg',
image: image[0]
});
});
} else {
imageEditorLink.css('display', 'none');
imageEditorLink.off('click');
}
} else {
editorContainer.find('.tinymce-active-object-handler').css('display', 'none');
}
}, 100);
});
}
// returns a public API for TinyMCE editor
return Object.freeze({
init: function(selector, mceConfig = {}) {
var tinyMceContainer;
var tinyMceInitSize;
var plugins;
if (typeof tinyMCE !== 'undefined') {
// Hide element containing HTML view of RTE field
tinyMceContainer = $(selector).closest('form').find('.tinymce-view');
@ -49,14 +122,14 @@ var TinyMCE = (function() {
$(selector).closest('.form-group')
.before('<div class="tinymce-placeholder" style="height:' + tinyMceInitSize + 'px"></div>');
tinyMceContainer.addClass('hidden');
plugins = 'autosave autoresize customimageuploader link advlist codesample autolink lists charmap hr anchor searchreplace wordcount visualblocks visualchars insertdatetime nonbreaking save directionality paste textcolor colorpicker textpattern';
if (typeof (MarvinJsEditor) !== 'undefined') plugins += ' marvinjsplugin';
tinyMCE.init({
cache_suffix: '?v=4.9.3', // This suffix should be changed any time library is updated
selector: selector,
menubar: 'file edit view insert format',
toolbar: 'undo redo restoredraft | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | forecolor backcolor | customimageuploader | codesample',
plugins: 'autosave autoresize customimageuploader link advlist codesample autolink lists charmap hr anchor searchreplace wordcount visualblocks visualchars insertdatetime nonbreaking save directionality paste textcolor colorpicker textpattern',
toolbar: 'undo redo restoredraft | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | forecolor backcolor | customimageuploader marvinjsplugin | codesample',
plugins: plugins,
codesample_languages: [
{ text: 'R', value: 'r' },
{ text: 'MATLAB', value: 'matlab' },
@ -129,9 +202,11 @@ var TinyMCE = (function() {
],
init_instance_callback: function(editor) {
var editorForm = $(editor.getContainer()).closest('form');
var editorContainer = $(editor.getContainer());
var menuBar = editorForm.find('.mce-menubar.mce-toolbar.mce-first .mce-flow-layout');
var editorToolbar = editorForm.find('.mce-top-part');
var editorToolbaroffset = mceConfig.toolbar_offset || 120;
var editorIframe = $('#' + editor.id).prev().find('.mce-edit-area iframe');
$('.tinymce-placeholder').css('height', $(editor.editorContainer).height() + 'px');
setTimeout(() => {
@ -149,6 +224,9 @@ var TinyMCE = (function() {
moveToolbar(editor, editorToolbar, editorToolbaroffset);
});
// Init image toolbar
initImageToolBar(editor);
// Update scroll position after exit
function updateScrollPosition() {
@ -254,6 +332,16 @@ var TinyMCE = (function() {
getContent: function() {
return tinyMCE.editors[0].getContent();
},
updateImages(editor) {
var images;
var iframe = $('#' + editor.id).prev().find('.mce-edit-area iframe').contents();
images = $.map($('img', iframe), e => {
return e.dataset.mceToken;
});
$('#' + editor.id).next()[0].value = JSON.stringify(images);
return JSON.stringify(images);
},
highlight: initHighlightjs
});
}());

View file

@ -504,6 +504,6 @@ $md-color-shadow-light: rgba(0, 0, 0, .12);
$md-color-shadow-dark: rgba(0, 0, 0, .24);
$md-shadow: 0 1px 3px $md-color-shadow-light, 0 1px 2px $md-color-shadow-dark;
$md-shadow-hover: 0 14px 28px $md-color-shadow-dark, 0 10px 10px $md-color-shadow-dark;
$md-shadow-hover: 0 12px 12px $md-color-shadow-dark, 0 10px 10px $md-color-shadow-dark;
$md-transaction: all .4s cubic-bezier(.25, .8, .25, 1);

View file

@ -0,0 +1,162 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
// scss-lint:disable SelectorFormat
// scss-lint:disable ImportantRule
// scss-lint:disable IdSelector
@import "constants";
@import "mixins";
// MarvinJs modal
.modal-marvin-js {
background: transparent;
font-size: $font-size-large;
padding: 0 !important;
.preview-close {
background: transparent;
border: 0;
color: $color-white;
display: inline-block;
float: right;
}
.modal-dialog {
height: 100%;
margin: 0;
padding: 0;
width: auto;
}
.modal-content {
background: transparent;
border: 0;
box-shadow: none;
color: $color-white;
height: 100%;
width: auto;
}
.modal-header {
background: $color-black;
border: 0;
height: 60px;
text-align: center;
.file-name {
float: left;
input {
border-radius: 5px;
box-shadow: none;
color: $color-black;
height: 40px;
outline: 0;
padding: 5px 10px;
position: relative;
top: -5px;
width: 350px;
}
}
}
.modal-body {
height: calc(100% - 60px);
padding: 0;
#marvinjs-editor {
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
#marvinjs-sketch {
border-right: 1px solid $color-gainsboro;
float: left;
min-height: 450px;
min-width: 500px;
overflow: hidden;
}
.marvinjs-team-sketch {
background: $color-white;
float: right;
height: calc(100% - 40px);
overflow-y: scroll;
position: relative;
width: 200px;
}
.marvinjs-team-sketch-header {
background: $color-white;
border-bottom: 1px solid $color-gainsboro;
color: $color-emperor;
float: right;
font-size: 16px;
height: 40px;
line-height: 39px;
text-align: center;
width: 200px;
}
}
.sketch-container {
@include md-card-style;
cursor: pointer;
margin: 10px;
overflow: hidden;
padding: 10px;
position: relative;
.sketch-image {
height: 100%;
width: 100%;
}
.sketch-name {
color: $brand-primary;
font-family: Lato;
font-size: 16px;
line-height: 18px;
margin: 10px auto;
overflow: hidden;
text-align: center;
width: 160px;
}
.sketch-object {
color: $color-emperor;
font-size: 12px;
opacity: .6;
text-align: center;
}
}
}
.file-save-link {
color: $color-white;
cursor: pointer;
display: inline-block;
float: right;
margin-right: 20px;
}
}
#new-step-sketch {
.sketch-container {
display: grid;
float: left;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
width: 100%;
}
}
.mce-i-file-invoice::before {
content: "\F570";
font-family: "Font Awesome 5 Free";
font-weight: 900;
line-height: 16px;
position: absolute;
}

View file

@ -15,7 +15,9 @@
list-style: none;
li {
& > div > span.pull-left {
margin-bottom: 10px;
> div > span.pull-left {
margin-top: 10px;
}
}
@ -92,6 +94,35 @@
.pseudo-attachment-container {
display: flex;
justify-content: center;
overflow: hidden;
.file-preview-link {
position: relative;
}
&.new {
order: 0 !important;
.file-preview-link {
transition: .5s;
}
.attachment-placeholder {
border: 1px solid $brand-primary;
&::before {
background: $brand-primary;
border-radius: 0 5px;
bottom: 16px;
color: $color-white;
content: "NEW";
left: 8px;
line-height: 20px;
position: absolute;
width: 50px;
}
}
}
}
}
@ -99,7 +130,7 @@
@include md-card-style;
color: $color-silver-chalice;
height: 280px;
margin: 8px;
margin: 4px 8px 16px;
text-align: center;
width: 220px;
@ -167,6 +198,7 @@
.remove-icon {
bottom: 15px;
cursor: pointer;
display: none;
position: relative;
right: 10px;
@ -245,8 +277,8 @@
line-height: 16px;
overflow: hidden;
padding: 2px 5px;
width: 100%;
pointer-events: none;
width: 100%;
&:focus {
outline: 0;

View file

@ -59,4 +59,50 @@
.mce-toolbar {
background: $color-white !important;
}
// scss-lint:enable ImportantRule
.mce-stack-layout {
.tinymce-active-object-handler {
border-top: 1px solid rgb(226, 228, 231);
height: 33px;
width: 100%;
.tool-button {
border: 1px solid transparent;
cursor: pointer;
display: inline-block;
line-height: 27px;
margin: 2px;
text-align: center;
width: 30px;
&:hover {
border: 1px solid rgb(226, 228, 231);
}
}
.mce-i-donwload::before {
content: "\F019";
font-family: "Font Awesome 5 Free";
font-weight: 900;
line-height: 16px;
position: absolute;
}
.mce-i-pencil::before {
content: "\F303";
font-family: "Font Awesome 5 Free";
font-weight: 900;
line-height: 16px;
position: absolute;
}
.mce-i-image::before {
content: "\F03E";
font-family: "Font Awesome 5 Free";
font-weight: 900;
line-height: 16px;
position: absolute;
}
}
}
// scss-lint:enable ImportantRule

View file

@ -0,0 +1,84 @@
# frozen_string_literal: true
class MarvinJsAssetsController < ApplicationController
def create
new_asset = MarvinJsAsset.add_sketch(marvin_params, current_team)
if new_asset.object_type == 'Step'
render json: {
html: render_to_string(
partial: 'assets/marvinjs/marvin_sketch_card.html.erb',
locals: { sketch: new_asset, i: 0, assets_count: 0, step: new_asset.object }
)
}
elsif new_asset.object_type == 'TinyMceAsset'
tiny_img = TinyMceAsset.find(new_asset.object_id)
render json: {
image: {
url: view_context.image_url(tiny_img.url(:large)),
token: Base62.encode(tiny_img.id),
source_id: new_asset.id,
source_type: new_asset.class.name
}
}, content_type: 'text/html'
elsif new_asset
render json: new_asset
else
render json: new_asset.errors, status: :unprocessable_entity
end
end
def show
sketch = current_team.marvin_js_assets.find_by_id(params[:id])
if sketch
if sketch.object_type == 'Step'
editable = can_manage_protocol_in_module?(sketch.object.protocol) ||
can_manage_protocol_in_repository?(sketch.object.protocol)
render json: {
sketch: sketch,
editable: editable
}
else
render json: sketch
end
else
render json: { error: t('marvinjs.no_sketches_found') }, status: :unprocessable_entity
end
end
def destroy
sketch = current_team.marvin_js_assets.find_by_id(params[:id])
if sketch.destroy
render json: sketch
else
render json: { error: t('marvinjs.no_sketches_found') }, status: :unprocessable_entity
end
end
def update
sketch = MarvinJsAsset.update_sketch(marvin_params, current_team)
if sketch
render json: sketch
else
render json: { error: t('marvinjs.no_sketches_found') }, status: :unprocessable_entity
end
end
def team_sketches
result = ''
sketches = current_team.marvin_js_assets.where.not(object_type: 'TinyMceAsset')
sketches.each do |sketch|
result += render_to_string(
partial: 'shared/marvinjs_modal_sketch.html.erb',
locals: { sketch: sketch }
)
end
render json: { html: result, sketches: sketches.pluck(:id) }
end
private
def marvin_params
params.permit(:id, :description, :object_id, :object_type, :name, :image)
end
end

View file

@ -595,7 +595,11 @@ class StepsController < ApplicationController
:name,
:contents,
:_destroy
]
],
marvin_js_assets_attributes: %i(
id
_destroy
)
)
end

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true
class TinyMceAssetsController < ApplicationController
def create
image = params.fetch(:file) { render_404 }
tiny_img = TinyMceAsset.new(image: image,
@ -21,4 +20,9 @@ class TinyMceAssetsController < ApplicationController
end
end
def update
image = TinyMceAsset.find_by_id(Base62.decode(params[:id]))
image.update(image: params[:image], image_file_name: image.image_file_name)
render json: { url: view_context.image_url(image.url) }
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module MyModulesHelper
def ordered_step_of(my_module)
my_module.protocol.steps.order(:position)
@ -8,11 +10,21 @@ module MyModulesHelper
end
def ordered_assets(step)
step.assets.order(:file_updated_at)
assets = []
assets += step.assets
assets += step.marvin_js_assets if MarvinJsAsset.enabled?
assets.sort! do |a, b|
a[asset_date_sort_field(a)] <=> b[asset_date_sort_field(b)]
end
end
def az_ordered_assets_index(step, asset_id)
step.assets.order('LOWER(file_file_name)').pluck(:id).index(asset_id)
assets = []
assets += step.assets
assets += step.marvin_js_assets if MarvinJsAsset.enabled?
assets.sort! do |a, b|
(a[asset_name_sort_field(a)] || '').downcase <=> (b[asset_name_sort_field(b)] || '').downcase
end.pluck(:id).index(asset_id)
end
def number_of_samples(my_module)
@ -35,11 +47,28 @@ module MyModulesHelper
end
def is_steps_page?
action_name == "steps"
action_name == 'steps'
end
def is_results_page?
action_name == "results"
action_name == 'results'
end
private
def asset_date_sort_field(element)
result = {
'Asset' => :file_updated_at,
'MarvinJsAsset' => :updated_at
}
result[element.class.name]
end
def asset_name_sort_field(element)
result = {
'Asset' => :file_file_name,
'MarvinJsAsset' => :name
}
result[element.class.name]
end
end

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
class MarvinJsAsset < ApplicationRecord
validates :name, presence: true
validates :description, presence: true
validates :object_id, presence: true
validates :object_type, presence: true
belongs_to :object, polymorphic: true,
optional: true,
inverse_of: :marvin_js_assets
belongs_to :team, inverse_of: :marvin_js_assets, optional: true
def self.url
ENV['MARVINJS_URL']
end
def self.enabled?
ENV['MARVINJS_URL'] != nil || ENV['MARVINJS_API_KEY'] != nil
end
def self.add_sketch(values, team)
if values[:object_type] == 'TinyMceAsset'
tiny_mce_img = TinyMceAsset.create(
object: nil,
team_id: team.id,
saved: false,
image: values[:image],
image_file_name: "#{name}.jpg"
)
values[:object_id] = tiny_mce_img.id
end
values[:name] = I18n.t('marvinjs.new_sketch') if values[:name].empty?
create(values.merge(team_id: team.id).except(:image))
end
def self.update_sketch(values, team)
sketch = team.marvin_js_assets.find(values[:id])
return false unless sketch
values[:name] = I18n.t('marvinjs.new_sketch') if values[:name].empty?
sketch.update(values.except(:image, :object_type, :id))
if values[:object_type] == 'TinyMceAsset'
image = TinyMceAsset.find(sketch.object_id)
image.update(image: values[:image], image_file_name: "#{name}.jpg")
return { url: image.url(:large), description: sketch.description }
end
sketch
end
end

View file

@ -33,6 +33,11 @@ class Step < ApplicationRecord
has_many :report_elements, inverse_of: :step,
dependent: :destroy
has_many :marvin_js_assets,
as: :object,
class_name: :MarvinJsAsset,
dependent: :destroy
accepts_nested_attributes_for :checklists,
reject_if: :all_blank,
allow_destroy: true
@ -44,6 +49,9 @@ class Step < ApplicationRecord
attributes['contents'].blank?
},
allow_destroy: true
accepts_nested_attributes_for :marvin_js_assets,
reject_if: :all_blank,
allow_destroy: true
after_destroy :cascade_after_destroy
before_save :set_last_modified_by

View file

@ -39,6 +39,7 @@ class Team < ApplicationRecord
has_many :repositories, dependent: :destroy
has_many :reports, inverse_of: :team, dependent: :destroy
has_many :activities, inverse_of: :team, dependent: :destroy
has_many :marvin_js_assets, inverse_of: :team, dependent: :destroy
attr_accessor :without_templates
attr_accessor :without_intro_demo

View file

@ -14,6 +14,11 @@ class TinyMceAsset < ApplicationRecord
touch: true,
optional: true
has_one :marvin_js_asset,
as: :object,
class_name: :MarvinJsAsset,
dependent: :destroy
belongs_to :object, polymorphic: true,
optional: true,
inverse_of: :tiny_mce_assets
@ -33,6 +38,10 @@ class TinyMceAsset < ApplicationRecord
}
validates :estimated_size, presence: true
def source
return marvin_js_asset if marvin_js_asset
end
def self.update_images(object, images)
images = JSON.parse(images)
current_images = object.tiny_mce_assets.pluck(:id)
@ -54,10 +63,15 @@ class TinyMceAsset < ApplicationRecord
tm_assets.each do |tm_asset|
asset_id = tm_asset.attr('data-mce-token')
new_asset_url = find_by_id(Base62.decode(asset_id))
if new_asset_url
tm_asset.attributes['src'].value = new_asset_url.url
tm_asset['class'] = 'img-responsive'
next unless new_asset_url
assets_source = new_asset_url.source
if assets_source
tm_asset.set_attribute('data-source-id', assets_source.id)
tm_asset.set_attribute('data-source-type', assets_source.class.name)
end
tm_asset.attributes['src'].value = new_asset_url.url
tm_asset['class'] = 'img-responsive'
end
description.css('body').inner_html.to_s
end

View file

@ -0,0 +1,10 @@
<span
class="btn btn-default new-marvinjs-upload-button"
data-object-id="<%= element_id %>"
data-object-type="<%= element_type %>"
data-marvin-url="<%= marvin_js_assets_path %>"
data-sketch-container="<%= sketch_container %>"
>
<span class="fas fa-file-invoice new-marvinjs-upload-icon"></span>
<%= t('marvinjs.new_button') %>
</span>

View file

@ -0,0 +1,18 @@
<div class="pseudo-attachment-container" style="order: <%= assets_count - i %>">
<%= link_to '',
class: 'file-preview-link',
id: "marvin_js_sketch_#{sketch.id}",
data: { no_turbolink: true, id: true, status: 'asset-present',
'preview-url': '',
'order-atoz': az_ordered_assets_index(step, sketch.id),
'order-ztoa': assets_count - az_ordered_assets_index(step, sketch.id),
'order-old': i,
'order-new': assets_count - i,
'asset-type': 'marvin-sketch',
'asset-id': sketch.id,
'update_url': marvin_js_asset_path(sketch.id)
} do %>
<%= render partial: 'assets/marvinjs/marvin_sketch_card_placeholder.html.erb',
locals: { edit_page: false, sketch: sketch } %>
<% end %>
</div>

View file

@ -0,0 +1,25 @@
<div class="attachment-placeholder pull-left" data-marvinjs-sketch="<%= sketch.id %>">
<div class="attachment-thumbnail">
<img src>
<%= hidden_field_tag :description, sketch.description %>
</div>
<script>
(function(){
src=$('.attachment-placeholder[data-marvinjs-sketch="<%= sketch.id %>"]').find('#description')
target=$('.attachment-placeholder[data-marvinjs-sketch="<%= sketch.id %>"]').find('img')
MarvinJsEditor().create_preview(src,target)
})()
</script>
<div class="attachment-label"><%= truncate(sketch.name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %></div>
<div class="spencer-bonnet-modif">
<%= t('protocols.steps.attachments.modified_label') %> <%= l(sketch.updated_at, format: :full_date) if sketch.updated_at %>
</div>
<% if edit_page %>
<div class="remove-icon pull-right">
<%= ff.remove_nested_fields_link do %>
<span class="fas fa-trash"></span>
<% end %>
</div>
<% end %>
</div>

View file

@ -7,6 +7,11 @@
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
<% if ENV['MARVINJS_API_KEY'] %>
<script src="https://marvinjs.chemicalize.com/v1/<%= ENV['MARVINJS_API_KEY'] %>/client-settings.js"></script>
<script src="https://marvinjs.chemicalize.com/v1/client.js"></script>
<% end %>
<%= favicon_link_tag "favicon.ico" %>
<%= favicon_link_tag "favicon-16.png", type: "image/png", size: "16x16" %>
<%= favicon_link_tag "favicon-32.png", type: "image/png", size: "32x32" %>
@ -44,6 +49,9 @@
<%= render "shared/about_modal" %>
<%= render "shared/file_preview_modal.html.erb" %>
<%= render "shared/file_edit_modal.html.erb" %>
<% if MarvinJsAsset.enabled? %>
<%= render "shared/marvinjs_modal.html.erb" %>
<% end %>
<%= render "shared/navigation" %>
<% if user_signed_in? && flash[:system_notification_modal] && current_user.show_login_system_notification? %>

View file

@ -0,0 +1,31 @@
<div id="MarvinJsModal"
class="modal modal-marvin-js"
role="dialog"
aria-labelledby="marvinJsModal"
aria-hidden="true"
data-backdrop="static"
data-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="preview-close" data-dismiss="modal"><span class="fas fa-times"></span></button>
<span class="file-name">
<%= text_field_tag :sketch_name %>
</span>
<p class="file-save-link"><span class="fas fa-save"></span> <%= t('SaveClose')%></p>
</div>
<div class="modal-body">
<div id="marvinjs-editor">
<% if ENV['MARVINJS_API_KEY'] %>
<div id="marvinjs-sketch" style="width: 600px; height: 480px"></div>
<% elsif ENV['MARVINJS_URL'] %>
<iframe id="marvinjs-sketch" src="<%= MarvinJsAsset.url %>" frameBorder="0"></iframe>
<% end %>
<div class="marvinjs-team-sketch-header"><%= t('marvinjs.team_drawings') %></div>
<div class="marvinjs-team-sketch"></div>
</div>
</div>
</div>
</div>
</div>
<%= javascript_include_tag("marvinjs_editor") %>

View file

@ -0,0 +1,11 @@
<div class="sketch-container" data-sketch-id="<%= sketch.id %>" >
<img src class="sketch-image">
<%= hidden_field_tag :description, sketch.description %>
<div class="sketch-name"><%= truncate(sketch.name, length: Constants::FILENAME_TRUNCATION_LENGTH) %></div>
<div class="sketch-object">
<% if sketch.object.protocol.my_module %>
<%= t('marvinjs.task') %>: <%= truncate(sketch.object.protocol.my_module.name,
length: Constants::FILENAME_TRUNCATION_LENGTH) %>
<% end %>
</div>
</div>

View file

@ -24,6 +24,14 @@
<%= t("protocols.steps.new.tab_tables") %>
</a>
</li>
<% if MarvinJsAsset.enabled? %>
<li role="presentation" id="new-step-sketch-tab">
<a href="#new-step-sketch" data-toggle="tab" data-no-turbolink="true">
<span class="fas fa-file-invoice"></span>
<%= t('marvinjs.checmical_drawing') %>
</a>
</li>
<% end %>
</ul>
<div class="tab-content">
<div class="tab-pane active" role="tabpanel" id="new-step-main">
@ -84,4 +92,15 @@
<%= t("protocols.steps.new.add_table") %>
<% end %>
</div>
<% if MarvinJsAsset.enabled? %>
<div class="tab-pane" role="tabpanel" id="new-step-sketch">
<div class="sketch-container">
<%= f.nested_fields_for :marvin_js_assets do |ff| %>
<% next unless ff.object.description %>
<%= render partial: 'assets/marvinjs/marvin_sketch_card_placeholder.html.erb',
locals: { sketch: ff.object, edit_page: true, ff: ff} %>
<% end %>
</div>
</div>
<% end %>
</div>

View file

@ -8,8 +8,12 @@
<div class="col-xs-12 attachments-actions">
<div class="col-md-4 drag_n_drop_label">
</div>
<div class="col-md-8">
<div class="col-xl-8 col-md-12">
<div class="attachemnts-header pull-right">
<% if MarvinJsAsset.enabled? && (can_manage_protocol_in_module?(step.protocol) || can_manage_protocol_in_repository?(step.protocol)) %>
<%= render partial: '/assets/marvinjs/create_marvin_sketch_button.html.erb',
locals: { element_id: step.id, element_type: 'Step', sketch_container: ".attacments#att-#{step.id}" } %>
<% end %>
<%= render partial: '/assets/wopi/create_wopi_file_button.html.erb',
locals: { element_id: step.id, element_type: 'Step' } %>
<div class="dropdown attachments-order" id="dd-att-step-<%= step.id %>">
@ -32,8 +36,13 @@
<div class="col-xs-12 attacments" id="att-<%= step.id %>">
<% assets.each_with_index do |asset, i| %>
<%= render partial: 'steps/attachments/item.html.erb',
<% if asset.class.name == 'Asset' %>
<%= render partial: 'steps/attachments/item.html.erb',
locals: { asset: asset, i: i, assets_count: assets.count, step: step } %>
<% elsif asset.class.name == 'MarvinJsAsset' %>
<%= render partial: 'assets/marvinjs/marvin_sketch_card.html.erb',
locals: { sketch: asset, i:i, assets_count: assets.count, step: step} %>
<% end %>
<% end %>
</div>
<hr>

View file

@ -67,6 +67,7 @@ Rails.application.config.assets.precompile +=
Rails.application.config.assets.precompile += %w(datatables.js)
Rails.application.config.assets.precompile += %w(search/index.js)
Rails.application.config.assets.precompile += %w(global_activities/side_pane.js)
Rails.application.config.assets.precompile += %w(marvinjs_editor.js)
Rails.application.config.assets.precompile += %w(navigation.js)
Rails.application.config.assets.precompile += %w(secondary_navigation.js)
Rails.application.config.assets.precompile += %w(datatables.css)

View file

@ -2144,3 +2144,10 @@ en:
new: "https://support.scinote.net/hc/en-us/articles/360004627792"
visibility: "https://support.scinote.net/hc/en-us/articles/360004627472"
manage_columns: "https://support.scinote.net/hc/en-us/articles/360004695831"
marvinjs:
new_sketch: "New sketch"
new_button: "New chemical drawing"
checmical_drawing: "Chemical drawings"
team_drawings: "Team drawings"
task: "Task"
no_sketches_found: "No sketches found"

View file

@ -450,6 +450,7 @@ Rails.application.routes.draw do
end
# tinyMCE image uploader endpoint
resources :tiny_mce_assets, only: [:update]
post '/tinymce_assets', to: 'tiny_mce_assets#create', as: :tiny_mce_assets
resources :results, only: [:update, :destroy] do
@ -676,6 +677,12 @@ Rails.application.routes.draw do
end
end
resources :marvin_js_assets, only: %i(create update destroy show) do
collection do
get :team_sketches
end
end
post 'global_activities', to: 'global_activities#index'
constraints WopiSubdomain do

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateMarvinJsAssets < ActiveRecord::Migration[5.1]
def change
create_table :marvin_js_assets do |t|
t.bigint :team_id
t.string :description
t.references :object, polymorphic: true
t.timestamps
end
change_column :marvin_js_assets, :id, :bigint
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddNameToMarvinJsAssets < ActiveRecord::Migration[5.1]
def change
add_column :marvin_js_assets, :name, :string
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190410110605) do
ActiveRecord::Schema.define(version: 20190427115413) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -180,6 +180,17 @@ ActiveRecord::Schema.define(version: 20190410110605) do
t.index ["restored_by_id"], name: "index_experiments_on_restored_by_id"
end
create_table "marvin_js_assets", force: :cascade do |t|
t.bigint "team_id"
t.string "description"
t.string "object_type"
t.bigint "object_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.index ["object_type", "object_id"], name: "index_marvin_js_assets_on_object_type_and_object_id"
end
create_table "my_module_groups", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :marvin_js_asset do
team_id 1
description 'MyString'
object ''
end
end

View file

@ -0,0 +1,120 @@
(function (win) {
function _getWrapperElement (id) {
var re = new RegExp(/^#.*/);
if (typeof id !== "string") {
return null;
}
// remove hash mark if present
return document.getElementById( (re.test(id)) ? id.substr(1) : id );
}
function _getPackage (wrapperElement) {
if (typeof wrapperElement.contentWindow.marvin != "undefined") {
return wrapperElement.contentWindow.marvin;
}
return null;
}
function _createPackage(elementId, resolve, reject) {
if(elementId == null){
reject("Element id can not be null.");
return;
}
var wrapperElement = _getWrapperElement(elementId);
if (wrapperElement == null) {
reject("Unable to get element with id: " + elementId);
return;
}
var marvinPackage = _getPackage(wrapperElement);
if (marvinPackage) {
marvinPackage.onReady(function() {
resolve(marvinPackage);
});
} else { // use listener
wrapperElement.addEventListener("load", function handleSketchLoad (e) {
var marvin = _getPackage(wrapperElement);
if (marvin) {
marvin.onReady(function() {
resolve(marvin);
});
} else {
reject("Unable to find marvin package");
}
});
}
}
function _createEditor(elementId, resolve, reject) {
if(elementId == null){
reject("Element id can not be null.");
return;
}
var wrapperElement = _getWrapperElement(elementId);
if (wrapperElement == null) {
reject("Unable to get element with id: " + elementId);
return;
}
var marvinPackage = _getPackage(wrapperElement);
if (marvinPackage) {
marvinPackage.onReady(function() {
if (typeof marvinPackage.sketcherInstance != "undefined") {
resolve(_getPackage(wrapperElement).sketcherInstance);
return;
} else {
reject("Unable to find sketcherInstance in element with id: " + elementId);
return;
}
});
} else { // use listener
wrapperElement.addEventListener("load", function handleSketchLoad (e) {
var marvin = _getPackage(wrapperElement);
if (marvin) {
marvin.onReady(function() {
if (typeof marvin.sketcherInstance != 'undefined') {
resolve(marvin.sketcherInstance);
} else {
reject("Unable to find sketcherInstance in iframe with id: " + elementId);
}
});
} else {
reject("Unable to find marvin package, cannot retrieve sketcher instance");
}
});
}
}
if (!("Promise" in win) && ("ES6Promise" in win) && ("polyfill" in win.ES6Promise)) {
win.ES6Promise.polyfill();
}
win.MarvinJSUtil = {
"getEditor": function getEditor (elementId) {
function createEditor (resolve, reject) {
_createEditor(elementId, resolve, reject);
};
return new Promise(createEditor);
}
,"getPackage": function getPackage (elementId) {
function createPackage (resolve, reject) {
_createPackage(elementId, resolve, reject);
};
return new Promise(createPackage);
}
};;
}(window));