Add composer field type
Dieser Commit ist enthalten in:
Ursprung
2859150dc2
Commit
9dd6efdaa2
19 geänderte Dateien mit 1218 neuen und 3 gelöschten Zeilen
|
@ -2,3 +2,12 @@
|
||||||
//= require discourse/lib/utilities
|
//= require discourse/lib/utilities
|
||||||
//= require discourse/lib/offset-calculator
|
//= require discourse/lib/offset-calculator
|
||||||
//= require discourse/lib/lock-on
|
//= require discourse/lib/lock-on
|
||||||
|
//= require discourse/lib/text-direction
|
||||||
|
//= require discourse/helpers/parse-html
|
||||||
|
//= require discourse/lib/to-markdown
|
||||||
|
//= require discourse/lib/load-script
|
||||||
|
|
||||||
|
//= require markdown-it-bundle
|
||||||
|
//= require pretty-text/engines/discourse-markdown-it
|
||||||
|
//= require pretty-text/engines/discourse-markdown/helpers
|
||||||
|
//= require pretty-text/pretty-text
|
||||||
|
|
|
@ -10,8 +10,13 @@
|
||||||
|
|
||||||
//= require discourse/components/user-selector
|
//= require discourse/components/user-selector
|
||||||
//= require discourse/helpers/user-avatar
|
//= require discourse/helpers/user-avatar
|
||||||
|
//= require discourse/components/conditional-loading-spinner
|
||||||
|
//= require discourse/templates/components/conditional-loading-spinner
|
||||||
|
//= require discourse/components/d-button
|
||||||
|
//= require discourse/templates/components/d-button
|
||||||
|
//= require discourse/components/d-editor-modal
|
||||||
//= require lodash.js
|
//= require lodash.js
|
||||||
|
//= require mousetrap.js
|
||||||
|
|
||||||
window.Discourse = {}
|
window.Discourse = {}
|
||||||
window.Wizard = {};
|
window.Wizard = {};
|
||||||
|
|
703
assets/javascripts/wizard/components/wizard-editor.js.es6
Normale Datei
703
assets/javascripts/wizard/components/wizard-editor.js.es6
Normale Datei
|
@ -0,0 +1,703 @@
|
||||||
|
/*global Mousetrap:true */
|
||||||
|
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
import { cookAsync } from '../lib/text-lite';
|
||||||
|
import { getRegister } from 'discourse-common/lib/get-owner';
|
||||||
|
import { siteDir } from 'discourse/lib/text-direction';
|
||||||
|
import { determinePostReplaceSelection, clipboardData } from '../lib/utilities-lite';
|
||||||
|
import toMarkdown from 'discourse/lib/to-markdown';
|
||||||
|
|
||||||
|
// Our head can be a static string or a function that returns a string
|
||||||
|
// based on input (like for numbered lists).
|
||||||
|
function getHead(head, prev) {
|
||||||
|
if (typeof head === "string") {
|
||||||
|
return [head, head.length];
|
||||||
|
} else {
|
||||||
|
return getHead(head(prev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getButtonLabel(labelKey, defaultLabel) {
|
||||||
|
// use the Font Awesome icon if the label matches the default
|
||||||
|
return I18n.t(labelKey) === defaultLabel ? null : labelKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OP = {
|
||||||
|
NONE: 0,
|
||||||
|
REMOVED: 1,
|
||||||
|
ADDED: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
const FOUR_SPACES_INDENT = '4-spaces-indent';
|
||||||
|
|
||||||
|
const _createCallbacks = [];
|
||||||
|
|
||||||
|
const isInside = (text, regex) => {
|
||||||
|
const matches = text.match(regex);
|
||||||
|
return matches && (matches.length % 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Toolbar {
|
||||||
|
|
||||||
|
constructor(site) {
|
||||||
|
this.shortcuts = {};
|
||||||
|
|
||||||
|
this.groups = [
|
||||||
|
{group: 'fontStyles', buttons: []},
|
||||||
|
{group: 'insertions', buttons: []},
|
||||||
|
{group: 'extras', buttons: []},
|
||||||
|
{group: 'mobileExtras', buttons: []}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.addButton({
|
||||||
|
trimLeading: true,
|
||||||
|
id: 'bold',
|
||||||
|
group: 'fontStyles',
|
||||||
|
icon: 'bold',
|
||||||
|
label: getButtonLabel('wizard_composer.bold_label', 'B'),
|
||||||
|
shortcut: 'B',
|
||||||
|
perform: e => e.applySurround('**', '**', 'bold_text')
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addButton({
|
||||||
|
trimLeading: true,
|
||||||
|
id: 'italic',
|
||||||
|
group: 'fontStyles',
|
||||||
|
icon: 'italic',
|
||||||
|
label: getButtonLabel('wizard_composer.italic_label', 'I'),
|
||||||
|
shortcut: 'I',
|
||||||
|
perform: e => e.applySurround('_', '_', 'italic_text')
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
|
||||||
|
|
||||||
|
this.addButton({
|
||||||
|
id: 'quote',
|
||||||
|
group: 'insertions',
|
||||||
|
icon: 'quote-right',
|
||||||
|
shortcut: 'Shift+9',
|
||||||
|
perform: e => e.applyList(
|
||||||
|
'> ',
|
||||||
|
'blockquote_text',
|
||||||
|
{ applyEmptyLines: true, multiline: true }
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addButton({id: 'code', group: 'insertions', shortcut: 'Shift+C', action: 'formatCode'});
|
||||||
|
|
||||||
|
this.addButton({
|
||||||
|
id: 'bullet',
|
||||||
|
group: 'extras',
|
||||||
|
icon: 'list-ul',
|
||||||
|
shortcut: 'Shift+8',
|
||||||
|
title: 'wizard_composer.ulist_title',
|
||||||
|
perform: e => e.applyList('* ', 'list_item')
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addButton({
|
||||||
|
id: 'list',
|
||||||
|
group: 'extras',
|
||||||
|
icon: 'list-ol',
|
||||||
|
shortcut: 'Shift+7',
|
||||||
|
title: 'wizard_composer.olist_title',
|
||||||
|
perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item')
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Wizard.SiteSettings.support_mixed_text_direction) {
|
||||||
|
this.addButton({
|
||||||
|
id: 'toggle-direction',
|
||||||
|
group: 'extras',
|
||||||
|
icon: 'exchange',
|
||||||
|
shortcut: 'Shift+6',
|
||||||
|
title: 'wizard_composer.toggle_direction',
|
||||||
|
perform: e => e.toggleDirection(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.groups[this.groups.length-1].lastGroup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addButton(button) {
|
||||||
|
const g = this.groups.findBy('group', button.group);
|
||||||
|
if (!g) {
|
||||||
|
throw `Couldn't find toolbar group ${button.group}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdButton = {
|
||||||
|
id: button.id,
|
||||||
|
className: button.className || button.id,
|
||||||
|
label: button.label,
|
||||||
|
icon: button.label ? null : button.icon || button.id,
|
||||||
|
action: button.action || 'toolbarButton',
|
||||||
|
perform: button.perform || function() { },
|
||||||
|
trimLeading: button.trimLeading,
|
||||||
|
popupMenu: button.popupMenu || false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (button.sendAction) {
|
||||||
|
createdButton.sendAction = button.sendAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = I18n.t(button.title || `wizard_composer.${button.id}_title`);
|
||||||
|
if (button.shortcut) {
|
||||||
|
const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||||
|
const mod = mac ? 'Meta' : 'Ctrl';
|
||||||
|
var shortcutTitle = `${mod}+${button.shortcut}`;
|
||||||
|
|
||||||
|
// Mac users are used to glyphs for shortcut keys
|
||||||
|
if (mac) {
|
||||||
|
shortcutTitle = shortcutTitle
|
||||||
|
.replace('Shift', "\u21E7")
|
||||||
|
.replace('Meta', "\u2318")
|
||||||
|
.replace('Alt', "\u2325")
|
||||||
|
.replace(/\+/g, '');
|
||||||
|
} else {
|
||||||
|
shortcutTitle = shortcutTitle
|
||||||
|
.replace('Shift', I18n.t('shortcut_modifier_key.shift'))
|
||||||
|
.replace('Ctrl', I18n.t('shortcut_modifier_key.ctrl'))
|
||||||
|
.replace('Alt', I18n.t('shortcut_modifier_key.alt'));
|
||||||
|
}
|
||||||
|
|
||||||
|
createdButton.title = `${title} (${shortcutTitle})`;
|
||||||
|
|
||||||
|
this.shortcuts[`${mod}+${button.shortcut}`.toLowerCase()] = createdButton;
|
||||||
|
} else {
|
||||||
|
createdButton.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button.unshift) {
|
||||||
|
g.buttons.unshift(createdButton);
|
||||||
|
} else {
|
||||||
|
g.buttons.push(createdButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ['d-editor'],
|
||||||
|
ready: false,
|
||||||
|
insertLinkHidden: true,
|
||||||
|
linkUrl: '',
|
||||||
|
linkText: '',
|
||||||
|
lastSel: null,
|
||||||
|
_mouseTrap: null,
|
||||||
|
showPreview: false,
|
||||||
|
|
||||||
|
@computed('placeholder')
|
||||||
|
placeholderTranslated(placeholder) {
|
||||||
|
if (placeholder) return I18n.t(placeholder);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_readyNow() {
|
||||||
|
this.set('ready', true);
|
||||||
|
|
||||||
|
if (this.get('autofocus')) {
|
||||||
|
this.$('textarea').focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super();
|
||||||
|
this.register = getRegister(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
Ember.run.scheduleOnce('afterRender', this, this._readyNow);
|
||||||
|
|
||||||
|
const mouseTrap = Mousetrap(this.$('.d-editor-input')[0]);
|
||||||
|
const shortcuts = this.get('toolbar.shortcuts');
|
||||||
|
|
||||||
|
// for some reason I am having trouble bubbling this so hack it in
|
||||||
|
mouseTrap.bind(['ctrl+alt+f'], (event) =>{
|
||||||
|
this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(shortcuts).forEach(sc => {
|
||||||
|
const button = shortcuts[sc];
|
||||||
|
mouseTrap.bind(sc, () => {
|
||||||
|
this.send(button.action, button);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// disable clicking on links in the preview
|
||||||
|
this.$('.d-editor-preview').on('click.preview', e => {
|
||||||
|
if ($(e.target).is("a")) {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.get('composerEvents')) {
|
||||||
|
this.appEvents.on('composer:insert-block', text => this._addBlock(this._getSelected(), text));
|
||||||
|
this.appEvents.on('composer:insert-text', (text, options) => this._addText(this._getSelected(), text, options));
|
||||||
|
this.appEvents.on('composer:replace-text', (oldVal, newVal) => this._replaceText(oldVal, newVal));
|
||||||
|
}
|
||||||
|
this._mouseTrap = mouseTrap;
|
||||||
|
},
|
||||||
|
|
||||||
|
@on('willDestroyElement')
|
||||||
|
_shutDown() {
|
||||||
|
if (this.get('composerEvents')) {
|
||||||
|
this.appEvents.off('composer:insert-block');
|
||||||
|
this.appEvents.off('composer:insert-text');
|
||||||
|
this.appEvents.off('composer:replace-text');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mouseTrap = this._mouseTrap;
|
||||||
|
Object.keys(this.get('toolbar.shortcuts')).forEach(sc => mouseTrap.unbind(sc));
|
||||||
|
mouseTrap.unbind('ctrl+/','command+/');
|
||||||
|
this.$('.d-editor-preview').off('click.preview');
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed
|
||||||
|
toolbar() {
|
||||||
|
const toolbar = new Toolbar(this.site);
|
||||||
|
_createCallbacks.forEach(cb => cb(toolbar));
|
||||||
|
this.sendAction('extraButtons', toolbar);
|
||||||
|
return toolbar;
|
||||||
|
},
|
||||||
|
|
||||||
|
_updatePreview() {
|
||||||
|
if (this._state !== "inDOM") { return; }
|
||||||
|
|
||||||
|
const value = this.get('value');
|
||||||
|
const markdownOptions = this.get('markdownOptions') || {};
|
||||||
|
|
||||||
|
cookAsync(value, markdownOptions).then(cooked => {
|
||||||
|
if (this.get('isDestroyed')) { return; }
|
||||||
|
this.set('preview', cooked);
|
||||||
|
Ember.run.scheduleOnce('afterRender', () => {
|
||||||
|
if (this._state !== "inDOM") { return; }
|
||||||
|
const $preview = this.$('.d-editor-preview');
|
||||||
|
if ($preview.length === 0) return;
|
||||||
|
this.sendAction('previewUpdated', $preview);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('ready', 'value')
|
||||||
|
_watchForChanges() {
|
||||||
|
if (!this.get('ready')) { return; }
|
||||||
|
|
||||||
|
// Debouncing in test mode is complicated
|
||||||
|
if (Ember.testing) {
|
||||||
|
this._updatePreview();
|
||||||
|
} else {
|
||||||
|
Ember.run.debounce(this, this._updatePreview, 30);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getSelected(trimLeading, opts) {
|
||||||
|
if (!this.get('ready')) { return; }
|
||||||
|
|
||||||
|
const textarea = this.$('textarea.d-editor-input')[0];
|
||||||
|
const value = textarea.value;
|
||||||
|
let start = textarea.selectionStart;
|
||||||
|
let end = textarea.selectionEnd;
|
||||||
|
|
||||||
|
// trim trailing spaces cause **test ** would be invalid
|
||||||
|
while (end > start && /\s/.test(value.charAt(end-1))) {
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimLeading) {
|
||||||
|
// trim leading spaces cause ** test** would be invalid
|
||||||
|
while(end > start && /\s/.test(value.charAt(start))) {
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selVal = value.substring(start, end);
|
||||||
|
const pre = value.slice(0, start);
|
||||||
|
const post = value.slice(end);
|
||||||
|
|
||||||
|
if (opts && opts.lineVal) {
|
||||||
|
const lineVal = value.split("\n")[value.substr(0, textarea.selectionStart).split("\n").length - 1];
|
||||||
|
return { start, end, value: selVal, pre, post, lineVal };
|
||||||
|
} else {
|
||||||
|
return { start, end, value: selVal, pre, post };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_selectText(from, length) {
|
||||||
|
Ember.run.scheduleOnce('afterRender', () => {
|
||||||
|
const $textarea = this.$('textarea.d-editor-input');
|
||||||
|
const textarea = $textarea[0];
|
||||||
|
const oldScrollPos = $textarea.scrollTop();
|
||||||
|
if (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)) {
|
||||||
|
$textarea.focus();
|
||||||
|
}
|
||||||
|
textarea.selectionStart = from;
|
||||||
|
textarea.selectionEnd = textarea.selectionStart + length;
|
||||||
|
$textarea.scrollTop(oldScrollPos);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// perform the same operation over many lines of text
|
||||||
|
_getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) {
|
||||||
|
let operation = OP.NONE;
|
||||||
|
|
||||||
|
const applyEmptyLines = opts && opts.applyEmptyLines;
|
||||||
|
|
||||||
|
return lines.map(l => {
|
||||||
|
if (!applyEmptyLines && l.length === 0) {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation !== OP.ADDED &&
|
||||||
|
(l.slice(0, hlen) === hval && tlen === 0 ||
|
||||||
|
(tail.length && l.slice(-tlen) === tail))) {
|
||||||
|
|
||||||
|
operation = OP.REMOVED;
|
||||||
|
if (tlen === 0) {
|
||||||
|
const result = l.slice(hlen);
|
||||||
|
[hval, hlen] = getHead(head, hval);
|
||||||
|
return result;
|
||||||
|
} else if (l.slice(-tlen) === tail) {
|
||||||
|
const result = l.slice(hlen, -tlen);
|
||||||
|
[hval, hlen] = getHead(head, hval);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else if (operation === OP.NONE) {
|
||||||
|
operation = OP.ADDED;
|
||||||
|
} else if (operation === OP.REMOVED) {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = `${hval}${l}${tail}`;
|
||||||
|
[hval, hlen] = getHead(head, hval);
|
||||||
|
return result;
|
||||||
|
}).join("\n");
|
||||||
|
},
|
||||||
|
|
||||||
|
_applySurround(sel, head, tail, exampleKey, opts) {
|
||||||
|
const pre = sel.pre;
|
||||||
|
const post = sel.post;
|
||||||
|
|
||||||
|
const tlen = tail.length;
|
||||||
|
if (sel.start === sel.end) {
|
||||||
|
if (tlen === 0) { return; }
|
||||||
|
|
||||||
|
const [hval, hlen] = getHead(head);
|
||||||
|
const example = I18n.t(`wizard_composer.${exampleKey}`);
|
||||||
|
this.set('value', `${pre}${hval}${example}${tail}${post}`);
|
||||||
|
this._selectText(pre.length + hlen, example.length);
|
||||||
|
} else if (opts && !opts.multiline) {
|
||||||
|
const [hval, hlen] = getHead(head);
|
||||||
|
|
||||||
|
if (pre.slice(-hlen) === hval && post.slice(0, tail.length) === tail) {
|
||||||
|
this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tail.length)}`);
|
||||||
|
this._selectText(sel.start - hlen, sel.value.length);
|
||||||
|
} else {
|
||||||
|
this.set('value', `${pre}${hval}${sel.value}${tail}${post}`);
|
||||||
|
this._selectText(sel.start + hlen, sel.value.length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const lines = sel.value.split("\n");
|
||||||
|
|
||||||
|
let [hval, hlen] = getHead(head);
|
||||||
|
if (lines.length === 1 && pre.slice(-tlen) === tail && post.slice(0, hlen) === hval) {
|
||||||
|
this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`);
|
||||||
|
this._selectText(sel.start - hlen, sel.value.length);
|
||||||
|
} else {
|
||||||
|
const contents = this._getMultilineContents(
|
||||||
|
lines,
|
||||||
|
head,
|
||||||
|
hval,
|
||||||
|
hlen,
|
||||||
|
tail,
|
||||||
|
tlen,
|
||||||
|
opts
|
||||||
|
);
|
||||||
|
|
||||||
|
this.set('value', `${pre}${contents}${post}`);
|
||||||
|
if (lines.length === 1 && tlen > 0) {
|
||||||
|
this._selectText(sel.start + hlen, sel.value.length);
|
||||||
|
} else {
|
||||||
|
this._selectText(sel.start, contents.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_applyList(sel, head, exampleKey, opts) {
|
||||||
|
if (sel.value.indexOf("\n") !== -1) {
|
||||||
|
this._applySurround(sel, head, '', exampleKey, opts);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const [hval, hlen] = getHead(head);
|
||||||
|
if (sel.start === sel.end) {
|
||||||
|
sel.value = I18n.t(`wizard_composer.${exampleKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedPre = sel.pre.trim();
|
||||||
|
const number = (sel.value.indexOf(hval) === 0) ? sel.value.slice(hlen) : `${hval}${sel.value}`;
|
||||||
|
const preLines = trimmedPre.length ? `${trimmedPre}\n\n` : "";
|
||||||
|
|
||||||
|
const trimmedPost = sel.post.trim();
|
||||||
|
const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost;
|
||||||
|
|
||||||
|
this.set('value', `${preLines}${number}${post}`);
|
||||||
|
this._selectText(preLines.length, number.length);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_replaceText(oldVal, newVal) {
|
||||||
|
const val = this.get('value');
|
||||||
|
const needleStart = val.indexOf(oldVal);
|
||||||
|
|
||||||
|
if (needleStart === -1) {
|
||||||
|
// Nothing to replace.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = this.$('textarea.d-editor-input')[0];
|
||||||
|
|
||||||
|
// Determine post-replace selection.
|
||||||
|
const newSelection = determinePostReplaceSelection({
|
||||||
|
selection: { start: textarea.selectionStart, end: textarea.selectionEnd },
|
||||||
|
needle: { start: needleStart, end: needleStart + oldVal.length },
|
||||||
|
replacement: { start: needleStart, end: needleStart + newVal.length }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace value (side effect: cursor at the end).
|
||||||
|
this.set('value', val.replace(oldVal, newVal));
|
||||||
|
|
||||||
|
// Restore cursor.
|
||||||
|
this._selectText(newSelection.start, newSelection.end - newSelection.start);
|
||||||
|
},
|
||||||
|
|
||||||
|
_addBlock(sel, text) {
|
||||||
|
text = (text || '').trim();
|
||||||
|
if (text.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pre = sel.pre;
|
||||||
|
let post = sel.value + sel.post;
|
||||||
|
|
||||||
|
if (pre.length > 0) {
|
||||||
|
pre = pre.replace(/\n*$/, "\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.length > 0) {
|
||||||
|
post = post.replace(/^\n*/, "\n\n");
|
||||||
|
} else {
|
||||||
|
post = "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = pre + text + post;
|
||||||
|
const $textarea = this.$('textarea.d-editor-input');
|
||||||
|
|
||||||
|
this.set('value', value);
|
||||||
|
|
||||||
|
$textarea.val(value);
|
||||||
|
$textarea.prop("selectionStart", (pre+text).length + 2);
|
||||||
|
$textarea.prop("selectionEnd", (pre+text).length + 2);
|
||||||
|
|
||||||
|
Ember.run.scheduleOnce("afterRender", () => $textarea.focus());
|
||||||
|
},
|
||||||
|
|
||||||
|
_addText(sel, text, options) {
|
||||||
|
const $textarea = this.$('textarea.d-editor-input');
|
||||||
|
|
||||||
|
if (options && options.ensureSpace) {
|
||||||
|
if ((sel.pre + '').length > 0) {
|
||||||
|
if (!sel.pre.match(/\s$/)) {
|
||||||
|
text = ' ' + text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((sel.post + '').length > 0) {
|
||||||
|
if (!sel.post.match(/^\s/)) {
|
||||||
|
text = text + ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const insert = `${sel.pre}${text}`;
|
||||||
|
const value = `${insert}${sel.post}`;
|
||||||
|
this.set('value', value);
|
||||||
|
$textarea.val(value);
|
||||||
|
$textarea.prop("selectionStart", insert.length);
|
||||||
|
$textarea.prop("selectionEnd", insert.length);
|
||||||
|
Ember.run.scheduleOnce("afterRender", () => $textarea.focus());
|
||||||
|
},
|
||||||
|
|
||||||
|
_extractTable(text) {
|
||||||
|
if (text.endsWith("\n")) {
|
||||||
|
text = text.substring(0, text.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = text.split("\n");
|
||||||
|
|
||||||
|
if (rows.length > 1) {
|
||||||
|
const columns = rows.map(r => r.split("\t").length);
|
||||||
|
const isTable = columns.reduce((a, b) => a && columns[0] === b && b > 1) &&
|
||||||
|
!(columns[0] === 2 && rows[0].split("\t")[0].match(/^•$|^\d+.$/)); // to skip tab delimited lists
|
||||||
|
|
||||||
|
if (isTable) {
|
||||||
|
const splitterRow = [...Array(columns[0])].map(() => "---").join("\t");
|
||||||
|
rows.splice(1, 0, splitterRow);
|
||||||
|
|
||||||
|
return "|" + rows.map(r => r.split("\t").join("|")).join("|\n|") + "|\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_toggleDirection() {
|
||||||
|
const $textArea = $(".d-editor-input");
|
||||||
|
let currentDir = $textArea.attr('dir') ? $textArea.attr('dir') : siteDir(),
|
||||||
|
newDir = currentDir === 'ltr' ? 'rtl' : 'ltr';
|
||||||
|
|
||||||
|
$textArea.attr('dir', newDir).focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
paste(e) {
|
||||||
|
if (!$(".d-editor-input").is(":focus")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isComposer = $("#reply-control .d-editor-input").is(":focus");
|
||||||
|
let { clipboard, canPasteHtml } = clipboardData(e, isComposer);
|
||||||
|
|
||||||
|
let plainText = clipboard.getData("text/plain");
|
||||||
|
let html = clipboard.getData("text/html");
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
if (plainText) {
|
||||||
|
plainText = plainText.trim().replace(/\r/g,"");
|
||||||
|
const table = this._extractTable(plainText);
|
||||||
|
if (table) {
|
||||||
|
this.appEvents.trigger('composer:insert-text', table);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pre, lineVal } = this._getSelected(null, {lineVal: true});
|
||||||
|
const isInlinePasting = pre.match(/[^\n]$/);
|
||||||
|
|
||||||
|
if (canPasteHtml && plainText) {
|
||||||
|
if (isInlinePasting) {
|
||||||
|
canPasteHtml = !(lineVal.match(/^```/) || isInside(pre, /`/g) || lineVal.match(/^ /));
|
||||||
|
} else {
|
||||||
|
canPasteHtml = !isInside(pre, /(^|\n)```/g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canPasteHtml && !handled) {
|
||||||
|
let markdown = toMarkdown(html);
|
||||||
|
|
||||||
|
if (!plainText || plainText.length < markdown.length) {
|
||||||
|
if(isInlinePasting) {
|
||||||
|
markdown = markdown.replace(/^#+/, "").trim();
|
||||||
|
markdown = pre.match(/\S$/) ? ` ${markdown}` : markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appEvents.trigger('composer:insert-text', markdown);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
keyPress(e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
const selected = this._getSelected();
|
||||||
|
this._addText(selected, '\n');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
toolbarButton(button) {
|
||||||
|
const selected = this._getSelected(button.trimLeading);
|
||||||
|
const toolbarEvent = {
|
||||||
|
selected,
|
||||||
|
selectText: (from, length) => this._selectText(from, length),
|
||||||
|
applySurround: (head, tail, exampleKey, opts) => this._applySurround(selected, head, tail, exampleKey, opts),
|
||||||
|
applyList: (head, exampleKey, opts) => this._applyList(selected, head, exampleKey, opts),
|
||||||
|
addText: text => this._addText(selected, text),
|
||||||
|
replaceText: text => this._addText({pre: '', post: ''}, text),
|
||||||
|
getText: () => this.get('value'),
|
||||||
|
toggleDirection: () => this._toggleDirection(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (button.sendAction) {
|
||||||
|
return this.sendAction(button.sendAction, toolbarEvent);
|
||||||
|
} else {
|
||||||
|
button.perform(toolbarEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showLinkModal() {
|
||||||
|
this._lastSel = this._getSelected();
|
||||||
|
|
||||||
|
if (this._lastSel) {
|
||||||
|
this.set("linkText", this._lastSel.value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('insertLinkHidden', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
formatCode() {
|
||||||
|
const sel = this._getSelected('', { lineVal: true });
|
||||||
|
const selValue = sel.value;
|
||||||
|
const hasNewLine = selValue.indexOf("\n") !== -1;
|
||||||
|
const isBlankLine = sel.lineVal.trim().length === 0;
|
||||||
|
const isFourSpacesIndent = this.siteSettings.code_formatting_style === FOUR_SPACES_INDENT;
|
||||||
|
|
||||||
|
if (!hasNewLine) {
|
||||||
|
if (selValue.length === 0 && isBlankLine) {
|
||||||
|
if (isFourSpacesIndent) {
|
||||||
|
const example = I18n.t(`wizard_composer.code_text`);
|
||||||
|
this.set('value', `${sel.pre} ${example}${sel.post}`);
|
||||||
|
return this._selectText(sel.pre.length + 4, example.length);
|
||||||
|
} else {
|
||||||
|
return this._applySurround(sel, "```\n", "\n```", 'paste_code_text');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this._applySurround(sel, '`', '`', 'code_title');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isFourSpacesIndent) {
|
||||||
|
return this._applySurround(sel, ' ', '', 'code_text');
|
||||||
|
} else {
|
||||||
|
const preNewline = (sel.pre[-1] !== "\n" && sel.pre !== "") ? "\n" : "";
|
||||||
|
const postNewline = sel.post[0] !== "\n" ? "\n" : "";
|
||||||
|
return this._addText(sel, `${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
insertLink() {
|
||||||
|
const origLink = this.get('linkUrl');
|
||||||
|
const linkUrl = (origLink.indexOf('://') === -1) ? `http://${origLink}` : origLink;
|
||||||
|
const sel = this._lastSel;
|
||||||
|
|
||||||
|
if (Ember.isEmpty(linkUrl)) { return; }
|
||||||
|
|
||||||
|
const linkText = this.get('linkText') || '';
|
||||||
|
if (linkText.length) {
|
||||||
|
this._addText(sel, `[${linkText}](${linkUrl})`);
|
||||||
|
} else {
|
||||||
|
if (sel.value) {
|
||||||
|
this._addText(sel, `[${sel.value}](${linkUrl})`);
|
||||||
|
} else {
|
||||||
|
this._addText(sel, `[${origLink}](${linkUrl})`);
|
||||||
|
this._selectText(sel.start + 1, origLink.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('linkUrl', '');
|
||||||
|
this.set('linkText', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
14
assets/javascripts/wizard/components/wizard-field-composer.js.es6
Normale Datei
14
assets/javascripts/wizard/components/wizard-field-composer.js.es6
Normale Datei
|
@ -0,0 +1,14 @@
|
||||||
|
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
@computed('showPreview')
|
||||||
|
togglePreviewLabel(showPreview) {
|
||||||
|
return showPreview ? 'wizard_composer.hide_preview' : 'wizard_composer.show_preview';
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
togglePreview() {
|
||||||
|
this.toggleProperty('showPreview');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
36
assets/javascripts/wizard/components/wizard-text-field.js.es6
Normale Datei
36
assets/javascripts/wizard/components/wizard-text-field.js.es6
Normale Datei
|
@ -0,0 +1,36 @@
|
||||||
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
|
import { siteDir, isRTL, isLTR } from "discourse/lib/text-direction";
|
||||||
|
|
||||||
|
export default Ember.TextField.extend({
|
||||||
|
attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength', 'dir'],
|
||||||
|
|
||||||
|
@computed
|
||||||
|
dir() {
|
||||||
|
if (Wizard.SiteSettings.support_mixed_text_direction) {
|
||||||
|
let val = this.value;
|
||||||
|
if (val) {
|
||||||
|
return isRTL(val) ? 'rtl' : 'ltr';
|
||||||
|
} else {
|
||||||
|
return siteDir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
keyUp() {
|
||||||
|
if (Wizard.SiteSettings.support_mixed_text_direction) {
|
||||||
|
let val = this.value;
|
||||||
|
if (isRTL(val)) {
|
||||||
|
this.set('dir', 'rtl');
|
||||||
|
} else if (isLTR(val)) {
|
||||||
|
this.set('dir', 'ltr');
|
||||||
|
} else {
|
||||||
|
this.set('dir', siteDir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("placeholderKey")
|
||||||
|
placeholder(placeholderKey) {
|
||||||
|
return placeholderKey ? I18n.t(placeholderKey) : "";
|
||||||
|
}
|
||||||
|
});
|
|
@ -159,7 +159,7 @@ export default {
|
||||||
}.property('field.type', 'field.id')
|
}.property('field.type', 'field.id')
|
||||||
});
|
});
|
||||||
|
|
||||||
const StandardFields = ['text', 'textarea', 'dropdown', 'image', 'checkbox', 'user-selector', 'text-only'];
|
const StandardFields = ['text', 'textarea', 'dropdown', 'image', 'checkbox', 'user-selector', 'text-only', 'composer'];
|
||||||
|
|
||||||
FieldModel.reopen({
|
FieldModel.reopen({
|
||||||
hasCustomCheck: false,
|
hasCustomCheck: false,
|
||||||
|
|
92
assets/javascripts/wizard/lib/load-script.js.es6
Normale Datei
92
assets/javascripts/wizard/lib/load-script.js.es6
Normale Datei
|
@ -0,0 +1,92 @@
|
||||||
|
import { ajax } from 'wizard/lib/ajax';
|
||||||
|
const _loaded = {};
|
||||||
|
const _loading = {};
|
||||||
|
|
||||||
|
function loadWithTag(path, cb) {
|
||||||
|
const head = document.getElementsByTagName('head')[0];
|
||||||
|
|
||||||
|
let finished = false;
|
||||||
|
let s = document.createElement('script');
|
||||||
|
s.src = path;
|
||||||
|
if (Ember.Test) {
|
||||||
|
Ember.Test.registerWaiter(() => finished);
|
||||||
|
}
|
||||||
|
head.appendChild(s);
|
||||||
|
|
||||||
|
s.onload = s.onreadystatechange = function(_, abort) {
|
||||||
|
finished = true;
|
||||||
|
if (abort || !s.readyState || s.readyState === "loaded" || s.readyState === "complete") {
|
||||||
|
s = s.onload = s.onreadystatechange = null;
|
||||||
|
if (!abort) {
|
||||||
|
Ember.run(null, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadCSS(url) {
|
||||||
|
return loadScript(url, { css: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function loadScript(url, opts) {
|
||||||
|
|
||||||
|
// TODO: Remove this once plugins have been updated not to use it:
|
||||||
|
if (url === "defer/html-sanitizer-bundle") { return Ember.RSVP.Promise.resolve(); }
|
||||||
|
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
$('script').each((i, tag) => {
|
||||||
|
const src = tag.getAttribute('src');
|
||||||
|
|
||||||
|
if (src && (opts.scriptTag || src !== url)) {
|
||||||
|
_loaded[tag.getAttribute('src')] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return new Ember.RSVP.Promise(function(resolve) {
|
||||||
|
url = Discourse.getURL(url);
|
||||||
|
|
||||||
|
// If we already loaded this url
|
||||||
|
if (_loaded[url]) { return resolve(); }
|
||||||
|
if (_loading[url]) { return _loading[url].then(resolve);}
|
||||||
|
|
||||||
|
let done;
|
||||||
|
_loading[url] = new Ember.RSVP.Promise(function(_done){
|
||||||
|
done = _done;
|
||||||
|
});
|
||||||
|
|
||||||
|
_loading[url].then(function(){
|
||||||
|
delete _loading[url];
|
||||||
|
});
|
||||||
|
|
||||||
|
const cb = function(data) {
|
||||||
|
if (opts && opts.css) {
|
||||||
|
$("head").append("<style>" + data + "</style>");
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
resolve();
|
||||||
|
_loaded[url] = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let cdnUrl = url;
|
||||||
|
|
||||||
|
// Scripts should always load from CDN
|
||||||
|
// CSS is type text, to accept it from a CDN we would need to handle CORS
|
||||||
|
if (!opts.css && Discourse.CDN && url[0] === "/" && url[1] !== "/") {
|
||||||
|
cdnUrl = Discourse.CDN.replace(/\/$/,"") + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some javascript depends on the path of where it is loaded (ace editor)
|
||||||
|
// to dynamically load more JS. In that case, add the `scriptTag: true`
|
||||||
|
// option.
|
||||||
|
if (opts.scriptTag) {
|
||||||
|
if (Ember.testing) {
|
||||||
|
throw `In test mode scripts cannot be loaded async ${cdnUrl}`;
|
||||||
|
}
|
||||||
|
loadWithTag(cdnUrl, cb);
|
||||||
|
} else {
|
||||||
|
ajax({url: cdnUrl, dataType: opts.css ? "text": "script", cache: true}).then(cb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
18
assets/javascripts/wizard/lib/text-lite.js.es6
Normale Datei
18
assets/javascripts/wizard/lib/text-lite.js.es6
Normale Datei
|
@ -0,0 +1,18 @@
|
||||||
|
import loadScript from './load-script';
|
||||||
|
import { default as PrettyText } from 'pretty-text/pretty-text';
|
||||||
|
|
||||||
|
export function cook(text, options) {
|
||||||
|
return new Handlebars.SafeString(new PrettyText(options).cook(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything should eventually move to async API and this should be renamed
|
||||||
|
// cook
|
||||||
|
export function cookAsync(text, options) {
|
||||||
|
if (Discourse.MarkdownItURL) {
|
||||||
|
return loadScript(Discourse.MarkdownItURL)
|
||||||
|
.then(()=>cook(text, options))
|
||||||
|
.catch(e => Ember.Logger.error(e));
|
||||||
|
} else {
|
||||||
|
return Ember.RSVP.Promise.resolve(cook(text));
|
||||||
|
}
|
||||||
|
}
|
60
assets/javascripts/wizard/lib/utilities-lite.js.es6
Normale Datei
60
assets/javascripts/wizard/lib/utilities-lite.js.es6
Normale Datei
|
@ -0,0 +1,60 @@
|
||||||
|
// lite version of discourse/lib/utilities
|
||||||
|
|
||||||
|
export function determinePostReplaceSelection({ selection, needle, replacement }) {
|
||||||
|
const diff = (replacement.end - replacement.start) - (needle.end - needle.start);
|
||||||
|
|
||||||
|
if (selection.end <= needle.start) {
|
||||||
|
// Selection ends (and starts) before needle.
|
||||||
|
return { start: selection.start, end: selection.end };
|
||||||
|
} else if (selection.start <= needle.start) {
|
||||||
|
// Selection starts before needle...
|
||||||
|
if (selection.end < needle.end) {
|
||||||
|
// ... and ends inside needle.
|
||||||
|
return { start: selection.start, end: needle.start };
|
||||||
|
} else {
|
||||||
|
// ... and spans needle completely.
|
||||||
|
return { start: selection.start, end: selection.end + diff };
|
||||||
|
}
|
||||||
|
} else if (selection.start < needle.end) {
|
||||||
|
// Selection starts inside needle...
|
||||||
|
if (selection.end <= needle.end) {
|
||||||
|
// ... and ends inside needle.
|
||||||
|
return { start: replacement.end, end: replacement.end };
|
||||||
|
} else {
|
||||||
|
// ... and spans end of needle.
|
||||||
|
return { start: replacement.end, end: selection.end + diff };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Selection starts (and ends) behind needle.
|
||||||
|
return { start: selection.start + diff, end: selection.end + diff };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toArray = items => {
|
||||||
|
items = items || [];
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
return Array.from(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function clipboardData(e, canUpload) {
|
||||||
|
const clipboard = e.clipboardData ||
|
||||||
|
e.originalEvent.clipboardData ||
|
||||||
|
e.delegatedEvent.originalEvent.clipboardData;
|
||||||
|
|
||||||
|
const types = toArray(clipboard.types);
|
||||||
|
let files = toArray(clipboard.files);
|
||||||
|
|
||||||
|
if (types.includes("Files") && files.length === 0) { // for IE
|
||||||
|
files = toArray(clipboard.items).filter(i => i.kind === "file");
|
||||||
|
}
|
||||||
|
|
||||||
|
canUpload = files && canUpload && !types.includes("text/plain");
|
||||||
|
const canUploadImage = canUpload && files.filter(f => f.type.match('^image/'))[0];
|
||||||
|
const canPasteHtml = Discourse.SiteSettings.enable_rich_text_paste && types.includes("text/html") && !canUploadImage;
|
||||||
|
|
||||||
|
return { clipboard, types, canUpload, canPasteHtml };
|
||||||
|
}
|
53
assets/javascripts/wizard/templates/components/wizard-editor.hbs
Normale Datei
53
assets/javascripts/wizard/templates/components/wizard-editor.hbs
Normale Datei
|
@ -0,0 +1,53 @@
|
||||||
|
<div class='d-editor-overlay hidden'></div>
|
||||||
|
|
||||||
|
<div class='d-editor-modals'>
|
||||||
|
{{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction="insertLink"}}
|
||||||
|
<h3>{{i18n "wizard_composer.link_dialog_title"}}</h3>
|
||||||
|
{{wizard-text-field value=linkUrl placeholderKey="wizard_composer.link_url_placeholder" class="link-url"}}
|
||||||
|
{{wizard-text-field value=linkText placeholderKey="wizard_composer.link_optional_text" class="link-text"}}
|
||||||
|
{{/d-editor-modal}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='d-editor-container'>
|
||||||
|
{{#if showPreview}}
|
||||||
|
<div class="d-editor-preview-wrapper {{if forcePreview 'force-preview'}}">
|
||||||
|
<div class="d-editor-preview">
|
||||||
|
{{{preview}}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="d-editor-textarea-wrapper">
|
||||||
|
<div class='d-editor-button-bar'>
|
||||||
|
{{#each toolbar.groups as |group|}}
|
||||||
|
{{#each group.buttons as |b|}}
|
||||||
|
{{#if b.popupMenu}}
|
||||||
|
{{toolbar-popup-menu-options
|
||||||
|
onPopupMenuAction=onPopupMenuAction
|
||||||
|
onExpand=(action b.action b)
|
||||||
|
title=b.title
|
||||||
|
headerIcon=b.icon
|
||||||
|
class=b.className
|
||||||
|
content=popupMenuOptions}}
|
||||||
|
{{else}}
|
||||||
|
<div>{{d.icon}}</div>
|
||||||
|
{{d-button
|
||||||
|
action=b.action
|
||||||
|
actionParam=b
|
||||||
|
translatedTitle=b.title
|
||||||
|
label=b.label
|
||||||
|
icon=b.icon
|
||||||
|
class=b.className}}
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#unless group.lastGroup}}
|
||||||
|
<div class='d-editor-spacer'></div>
|
||||||
|
{{/unless}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{conditional-loading-spinner condition=loading}}
|
||||||
|
{{textarea tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{d-button class='wizard-btn primary' action='togglePreview' label=togglePreviewLabel}}
|
||||||
|
{{wizard-editor showPreview=showPreview value=field.value}}
|
168
assets/stylesheets/wizard/wizard_composer.scss
Normale Datei
168
assets/stylesheets/wizard/wizard_composer.scss
Normale Datei
|
@ -0,0 +1,168 @@
|
||||||
|
@import 'wizard_variables';
|
||||||
|
|
||||||
|
.d-editor-container {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-overlay {
|
||||||
|
position: absolute;
|
||||||
|
background-color: black;
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: z("modal","overlay");
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-modals {
|
||||||
|
position: absolute;
|
||||||
|
z-index: z("modal","content");
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor .d-editor-modal {
|
||||||
|
min-width: 400px;
|
||||||
|
@media screen and (max-width: 424px) {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
position: absolute;
|
||||||
|
background-color: $secondary;
|
||||||
|
border: 1px solid $primary;
|
||||||
|
padding: 1em;
|
||||||
|
top: 25px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-textarea-wrapper,
|
||||||
|
.d-editor-preview-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-textarea-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: $secondary;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #919191;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-preview-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-button-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: none;
|
||||||
|
min-height: 30px;
|
||||||
|
padding-left: 3px;
|
||||||
|
border-bottom: 1px solid #e9e9e9;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:not(.no-text) {
|
||||||
|
font-size: 1.1487em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.bold {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-spacer {
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0 5px;
|
||||||
|
background-color: $primary;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-preview-wrapper {
|
||||||
|
overflow: auto;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-input,
|
||||||
|
.d-editor-preview {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
min-height: auto;
|
||||||
|
word-wrap: break-word;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border-radius: 0;
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-input {
|
||||||
|
border: 0;
|
||||||
|
padding: 10px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-preview {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-plugin {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composing-whisper .d-editor-preview {
|
||||||
|
font-style: italic;
|
||||||
|
color: $primary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-preview > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-preview .d-editor-preview-wrapper {
|
||||||
|
display: none;
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
////
|
||||||
|
|
||||||
|
.d-editor {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-modal.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-editor-button-bar .btn {
|
||||||
|
border: none;
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import 'wizard_variables';
|
||||||
|
|
||||||
.custom-wizard {
|
.custom-wizard {
|
||||||
background-color: initial;
|
background-color: initial;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import 'wizard_variables';
|
||||||
|
|
||||||
.custom-wizard {
|
.custom-wizard {
|
||||||
.wizard-step-form {
|
.wizard-step-form {
|
||||||
.wizard-btn {
|
.wizard-btn {
|
||||||
|
|
10
assets/stylesheets/wizard/wizard_variables.scss
Normale Datei
10
assets/stylesheets/wizard/wizard_variables.scss
Normale Datei
|
@ -0,0 +1,10 @@
|
||||||
|
$primary: #222222 !default;
|
||||||
|
$secondary: #ffffff !default;
|
||||||
|
$tertiary: #0088cc !default;
|
||||||
|
$quaternary: #e45735 !default;
|
||||||
|
$header_background: #ffffff !default;
|
||||||
|
$header_primary: #333333 !default;
|
||||||
|
$highlight: #ffff4d !default;
|
||||||
|
$danger: #e45735 !default;
|
||||||
|
$success: #009900 !default;
|
||||||
|
$love: #fa6c8d !default;
|
|
@ -157,3 +157,40 @@ en:
|
||||||
completed: "You have completed this wizard."
|
completed: "You have completed this wizard."
|
||||||
not_permitted: "You need to be trust level {{level}} or higher to access this wizard."
|
not_permitted: "You need to be trust level {{level}} or higher to access this wizard."
|
||||||
none: "There is no wizard here."
|
none: "There is no wizard here."
|
||||||
|
|
||||||
|
wizard_composer:
|
||||||
|
show_preview: "Show Preview"
|
||||||
|
hide_preview: "Hide Preview"
|
||||||
|
quote_post_title: "Quote whole post"
|
||||||
|
bold_label: "B"
|
||||||
|
bold_title: "Strong"
|
||||||
|
bold_text: "strong text"
|
||||||
|
italic_label: "I"
|
||||||
|
italic_title: "Emphasis"
|
||||||
|
italic_text: "emphasized text"
|
||||||
|
link_title: "Hyperlink"
|
||||||
|
link_description: "enter link description here"
|
||||||
|
link_dialog_title: "Insert Hyperlink"
|
||||||
|
link_optional_text: "optional title"
|
||||||
|
link_url_placeholder: "http://example.com"
|
||||||
|
quote_title: "Blockquote"
|
||||||
|
quote_text: "Blockquote"
|
||||||
|
blockquote_text: "Blockquote"
|
||||||
|
code_title: "Preformatted text"
|
||||||
|
code_text: "indent preformatted text by 4 spaces"
|
||||||
|
paste_code_text: "type or paste code here"
|
||||||
|
upload_title: "Upload"
|
||||||
|
upload_description: "enter upload description here"
|
||||||
|
olist_title: "Numbered List"
|
||||||
|
ulist_title: "Bulleted List"
|
||||||
|
list_item: "List item"
|
||||||
|
toggle_direction: "Toggle Direction"
|
||||||
|
help: "Markdown Editing Help"
|
||||||
|
collapse: "minimize the composer panel"
|
||||||
|
abandon: "close composer and discard draft"
|
||||||
|
modal_ok: "OK"
|
||||||
|
modal_cancel: "Cancel"
|
||||||
|
cant_send_pm: "Sorry, you can't send a message to %{username}."
|
||||||
|
yourself_confirm:
|
||||||
|
title: "Did you forget to add recipients?"
|
||||||
|
body: "Right now this message is only being sent to yourself!"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
class CustomWizard::Field
|
class CustomWizard::Field
|
||||||
def self.types
|
def self.types
|
||||||
@types ||= ['text', 'textarea', 'dropdown', 'image', 'checkbox', 'user-selector', 'text-only']
|
@types ||= ['text', 'textarea', 'dropdown', 'image', 'checkbox', 'user-selector', 'text-only', 'composer']
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.require_assets
|
def self.require_assets
|
||||||
|
|
|
@ -18,6 +18,8 @@ if Rails.env.production?
|
||||||
wizard-custom.js
|
wizard-custom.js
|
||||||
wizard-plugin.js
|
wizard-plugin.js
|
||||||
stylesheets/wizard/wizard_custom.scss
|
stylesheets/wizard/wizard_custom.scss
|
||||||
|
stylesheets/wizard/wizard_composer.scss
|
||||||
|
stylesheets/wizard/wizard_variables.scss
|
||||||
stylesheets/wizard/wizard_custom_mobile.scss
|
stylesheets/wizard/wizard_custom_mobile.scss
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
<head>
|
<head>
|
||||||
<%= discourse_stylesheet_link_tag :wizard, theme_key: nil %>
|
<%= discourse_stylesheet_link_tag :wizard, theme_key: nil %>
|
||||||
<%= stylesheet_link_tag "wizard_custom", media: "all", "data-turbolinks-track" => "reload" %>
|
<%= stylesheet_link_tag "wizard_custom", media: "all", "data-turbolinks-track" => "reload" %>
|
||||||
|
<%= stylesheet_link_tag "wizard_composer", media: "all", "data-turbolinks-track" => "reload" %>
|
||||||
|
<%= stylesheet_link_tag "wizard_variables", media: "all", "data-turbolinks-track" => "reload" %>
|
||||||
<%= stylesheet_link_tag "wizard_custom_mobile", media: "all", "data-turbolinks-track" => "reload" if mobile_view?%>
|
<%= stylesheet_link_tag "wizard_custom_mobile", media: "all", "data-turbolinks-track" => "reload" if mobile_view?%>
|
||||||
<%- if theme_key %>
|
<%- if theme_key %>
|
||||||
<%= discourse_stylesheet_link_tag (mobile_view? ? :mobile_theme : :desktop_theme) %>
|
<%= discourse_stylesheet_link_tag (mobile_view? ? :mobile_theme : :desktop_theme) %>
|
||||||
|
|
Laden …
In neuem Issue referenzieren