Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migration of AngularJS Code #524

Merged
merged 4 commits into from
Sep 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@
"yargs": "^7.1.0"
},
"dependencies": {
"angular": "^1.5.5",
"angular-animate": "^1.5.5",
"angular-resource": "^1.5.5",
"angular-sanitize": "^1.5.5",
"angular-ui-sortable": "^0.17.0",
"axios": "^0.16.1",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
Expand Down
1 change: 0 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ The BookStack source is provided under the MIT License.
These are the great open-source projects used to help build BookStack:

* [Laravel](http://laravel.com/)
* [AngularJS](https://angularjs.org/)
* [jQuery](https://jquery.com/)
* [TinyMCE](https://www.tinymce.com/)
* [CodeMirror](https://codemirror.net)
Expand Down
47 changes: 47 additions & 0 deletions resources/assets/js/components/editor-toolbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class EditorToolbox {

constructor(elem) {
// Elements
this.elem = elem;
this.buttons = elem.querySelectorAll('[toolbox-tab-button]');
this.contentElements = elem.querySelectorAll('[toolbox-tab-content]');
this.toggleButton = elem.querySelector('[toolbox-toggle]');

// Toolbox toggle button click
this.toggleButton.addEventListener('click', this.toggle.bind(this));
// Tab button click
this.elem.addEventListener('click', event => {
let button = event.target.closest('[toolbox-tab-button]');
if (button === null) return;
let name = button.getAttribute('toolbox-tab-button');
this.setActiveTab(name, true);
});

// Set the first tab as active on load
this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content'));
}

toggle() {
this.elem.classList.toggle('open');
}

setActiveTab(tabName, openToolbox = false) {
// Set button visibility
for (let i = 0, len = this.buttons.length; i < len; i++) {
this.buttons[i].classList.remove('active');
let bName = this.buttons[i].getAttribute('toolbox-tab-button');
if (bName === tabName) this.buttons[i].classList.add('active');
}
// Set content visibility
for (let i = 0, len = this.contentElements.length; i < len; i++) {
this.contentElements[i].style.display = 'none';
let cName = this.contentElements[i].getAttribute('toolbox-tab-content');
if (cName === tabName) this.contentElements[i].style.display = 'block';
}

if (openToolbox) this.elem.classList.add('open');
}

}

module.exports = EditorToolbox;
3 changes: 3 additions & 0 deletions resources/assets/js/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ let componentMapping = {
'sidebar': require('./sidebar'),
'page-picker': require('./page-picker'),
'page-comments': require('./page-comments'),
'wysiwyg-editor': require('./wysiwyg-editor'),
'markdown-editor': require('./markdown-editor'),
'editor-toolbox': require('./editor-toolbox'),
};

window.components = {};
Expand Down
293 changes: 293 additions & 0 deletions resources/assets/js/components/markdown-editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('../code');

class MarkdownEditor {

constructor(elem) {
this.elem = elem;
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});

this.display = this.elem.querySelector('.markdown-display');
this.input = this.elem.querySelector('textarea');
this.htmlInput = this.elem.querySelector('input[name=html]');
this.cm = code.markdownEditor(this.input);

this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
this.init();
}

init() {

// Prevent markdown display link click redirect
this.display.addEventListener('click', event => {
let link = event.target.closest('a');
if (link === null) return;

event.preventDefault();
window.open(link.getAttribute('href'));
});

// Button actions
this.elem.addEventListener('click', event => {
let button = event.target.closest('button[data-action]');
if (button === null) return;

let action = button.getAttribute('data-action');
if (action === 'insertImage') this.actionInsertImage();
if (action === 'insertLink') this.actionShowLinkSelector();
});

window.$events.listen('editor-markdown-update', value => {
this.cm.setValue(value);
this.updateAndRender();
});

this.codeMirrorSetup();
}

// Update the input content and render the display.
updateAndRender() {
let content = this.cm.getValue();
this.input.value = content;
let html = this.markdown.render(content);
window.$events.emit('editor-html-change', html);
window.$events.emit('editor-markdown-change', content);
this.display.innerHTML = html;
this.htmlInput.value = html;
}

onMarkdownScroll(lineCount) {
let elems = this.display.children;
if (elems.length <= lineCount) return;

let topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
// TODO - Replace jQuery
$(this.display).animate({
scrollTop: topElem.offsetTop
}, {queue: false, duration: 200, easing: 'linear'});
}

codeMirrorSetup() {
let cm = this.cm;
// Custom key commands
let metaKey = code.getMetaKey();
const extraKeys = {};
// Insert Image shortcut
extraKeys[`${metaKey}-Alt-I`] = function(cm) {
let selectedText = cm.getSelection();
let newText = `![${selectedText}](http://)`;
let cursorPos = cm.getCursor('from');
cm.replaceSelection(newText);
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1);
};
// Save draft
extraKeys[`${metaKey}-S`] = cm => {window.$events.emit('editor-save-draft')};
// Show link selector
extraKeys[`Shift-${metaKey}-K`] = cm => {this.actionShowLinkSelector()};
// Insert Link
extraKeys[`${metaKey}-K`] = cm => {insertLink()};
// FormatShortcuts
extraKeys[`${metaKey}-1`] = cm => {replaceLineStart('##');};
extraKeys[`${metaKey}-2`] = cm => {replaceLineStart('###');};
extraKeys[`${metaKey}-3`] = cm => {replaceLineStart('####');};
extraKeys[`${metaKey}-4`] = cm => {replaceLineStart('#####');};
extraKeys[`${metaKey}-5`] = cm => {replaceLineStart('');};
extraKeys[`${metaKey}-d`] = cm => {replaceLineStart('');};
extraKeys[`${metaKey}-6`] = cm => {replaceLineStart('>');};
extraKeys[`${metaKey}-q`] = cm => {replaceLineStart('>');};
extraKeys[`${metaKey}-7`] = cm => {wrapSelection('\n```\n', '\n```');};
extraKeys[`${metaKey}-8`] = cm => {wrapSelection('`', '`');};
extraKeys[`Shift-${metaKey}-E`] = cm => {wrapSelection('`', '`');};
extraKeys[`${metaKey}-9`] = cm => {wrapSelection('<p class="callout info">', '</p>');};
cm.setOption('extraKeys', extraKeys);

// Update data on content change
cm.on('change', (instance, changeObj) => {
this.updateAndRender();
});

// Handle scroll to sync display view
cm.on('scroll', instance => {
// Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
let scroll = instance.getScrollInfo();
let atEnd = scroll.top + scroll.clientHeight === scroll.height;
if (atEnd) {
this.onMarkdownScroll(-1);
return;
}

let lineNum = instance.lineAtHeight(scroll.top, 'local');
let range = instance.getRange({line: 0, ch: null}, {line: lineNum, ch: null});
let parser = new DOMParser();
let doc = parser.parseFromString(this.markdown.render(range), 'text/html');
let totalLines = doc.documentElement.querySelectorAll('body > *');
this.onMarkdownScroll(totalLines.length);
});

// Handle image paste
cm.on('paste', (cm, event) => {
if (!event.clipboardData || !event.clipboardData.items) return;
for (let i = 0; i < event.clipboardData.items.length; i++) {
uploadImage(event.clipboardData.items[i].getAsFile());
}
});

// Handle images on drag-drop
cm.on('drop', (cm, event) => {
event.stopPropagation();
event.preventDefault();
let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
cm.setCursor(cursorPos);
if (!event.dataTransfer || !event.dataTransfer.files) return;
for (let i = 0; i < event.dataTransfer.files.length; i++) {
uploadImage(event.dataTransfer.files[i]);
}
});

// Helper to replace editor content
function replaceContent(search, replace) {
let text = cm.getValue();
let cursor = cm.listSelections();
cm.setValue(text.replace(search, replace));
cm.setSelections(cursor);
}

// Helper to replace the start of the line
function replaceLineStart(newStart) {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let lineStart = lineContent.split(' ')[0];

// Remove symbol if already set
if (lineStart === newStart) {
lineContent = lineContent.replace(`${newStart} `, '');
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
return;
}

let alreadySymbol = /^[#>`]/.test(lineStart);
let posDif = 0;
if (alreadySymbol) {
posDif = newStart.length - lineStart.length;
lineContent = lineContent.replace(lineStart, newStart).trim();
} else if (newStart !== '') {
posDif = newStart.length + 1;
lineContent = newStart + ' ' + lineContent;
}
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
}

function wrapLine(start, end) {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let newLineContent = lineContent;

if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
} else {
newLineContent = `${start}${lineContent}${end}`;
}

cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
}

function wrapSelection(start, end) {
let selection = cm.getSelection();
if (selection === '') return wrapLine(start, end);

let newSelection = selection;
let frontDiff = 0;
let endDiff = 0;

if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
newSelection = selection.slice(start.length, selection.length - end.length);
endDiff = -(end.length + start.length);
} else {
newSelection = `${start}${selection}${end}`;
endDiff = start.length + end.length;
}

let selections = cm.listSelections()[0];
cm.replaceSelection(newSelection);
let headFirst = selections.head.ch <= selections.anchor.ch;
selections.head.ch += headFirst ? frontDiff : endDiff;
selections.anchor.ch += headFirst ? endDiff : frontDiff;
cm.setSelections([selections]);
}

// Handle image upload and add image into markdown content
function uploadImage(file) {
if (file === null || file.type.indexOf('image') !== 0) return;
let ext = 'png';

if (file.name) {
let fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches.length > 1) ext = fileNameMatches[1];
}

// Insert image into markdown
let id = "image-" + Math.random().toString(16).slice(2);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let selectedText = cm.getSelection();
let placeHolderText = `![${selectedText}](${placeholderImage})`;
cm.replaceSelection(placeHolderText);

let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
formData.append('file', file, remoteFilename);

window.$http.post('/images/gallery/upload', formData).then(resp => {
replaceContent(placeholderImage, resp.data.thumbs.display);
}).catch(err => {
events.emit('error', trans('errors.image_upload_error'));
replaceContent(placeHolderText, selectedText);
console.log(err);
});
}

function insertLink() {
let cursorPos = cm.getCursor('from');
let selectedText = cm.getSelection() || '';
let newText = `[${selectedText}]()`;
cm.focus();
cm.replaceSelection(newText);
let cursorPosDiff = (selectedText === '') ? -3 : -1;
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
}

this.updateAndRender();
}

actionInsertImage() {
let cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
let selectedText = this.cm.getSelection();
let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")";
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}

// Show the popup link selector and insert a link when finished
actionShowLinkSelector() {
let cursorPos = this.cm.getCursor('from');
window.EntitySelectorPopup.show(entity => {
let selectedText = this.cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`;
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}

}

module.exports = MarkdownEditor ;
11 changes: 11 additions & 0 deletions resources/assets/js/components/wysiwyg-editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class WysiwygEditor {

constructor(elem) {
this.elem = elem;
this.options = require("../pages/page-form");
tinymce.init(this.options);
}

}

module.exports = WysiwygEditor;
Loading