mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-05-08 08:01:02 -04:00
Added script for checking the types.
This commit is contained in:
parent
cd98696d2b
commit
d5c011e6ed
7 changed files with 332 additions and 286 deletions
|
@ -5,17 +5,19 @@
|
||||||
* objects lack.
|
* objects lack.
|
||||||
*/
|
*/
|
||||||
class Stream {
|
class Stream {
|
||||||
|
private _iter
|
||||||
|
private _next: any
|
||||||
/**
|
/**
|
||||||
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
|
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
|
||||||
*/
|
*/
|
||||||
static range(start, end) {
|
static range(start: number, end: number) {
|
||||||
return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })());
|
return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Iterable<any>} values - Any iterable of values.
|
* @param {Iterable<any>} values - Any iterable of values.
|
||||||
*/
|
*/
|
||||||
constructor(values) {
|
constructor(values: Iterable<any>) {
|
||||||
this._iter = values[Symbol.iterator]();
|
this._iter = values[Symbol.iterator]();
|
||||||
this._next = null;
|
this._next = null;
|
||||||
}
|
}
|
||||||
|
@ -52,10 +54,11 @@ class Stream {
|
||||||
* @param {number} size - The number of values to read at a time.
|
* @param {number} size - The number of values to read at a time.
|
||||||
* @returns {Stream} A new Stream that gets its values from this Stream.
|
* @returns {Stream} A new Stream that gets its values from this Stream.
|
||||||
*/
|
*/
|
||||||
batch(size) {
|
batch(size: number) {
|
||||||
return new Stream((function* () {
|
return new Stream((function* () {
|
||||||
const b = [];
|
const b = [];
|
||||||
try {
|
try {
|
||||||
|
// @ts-ignore
|
||||||
for (const v of this) {
|
for (const v of this) {
|
||||||
Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors.
|
Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors.
|
||||||
b.push(v);
|
b.push(v);
|
||||||
|
@ -100,10 +103,11 @@ class Stream {
|
||||||
* @param {number} capacity - The number of values to keep buffered.
|
* @param {number} capacity - The number of values to keep buffered.
|
||||||
* @returns {Stream} A new Stream that gets its values from this Stream.
|
* @returns {Stream} A new Stream that gets its values from this Stream.
|
||||||
*/
|
*/
|
||||||
buffer(capacity) {
|
buffer(capacity: number) {
|
||||||
return new Stream((function* () {
|
return new Stream((function* () {
|
||||||
const b = [];
|
const b = [];
|
||||||
try {
|
try {
|
||||||
|
// @ts-ignore
|
||||||
for (const v of this) {
|
for (const v of this) {
|
||||||
Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors.
|
Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors.
|
||||||
// Note: V8 has good Array push+shift optimization.
|
// Note: V8 has good Array push+shift optimization.
|
||||||
|
@ -123,7 +127,8 @@ class Stream {
|
||||||
* @param {(v: any) => any} fn - Value transformation function.
|
* @param {(v: any) => any} fn - Value transformation function.
|
||||||
* @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`.
|
* @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`.
|
||||||
*/
|
*/
|
||||||
map(fn) { return new Stream((function* () { for (const v of this) yield fn(v); }).call(this)); }
|
map(fn:Function) { return new Stream((function* () { // @ts-ignore
|
||||||
|
for (const v of this) yield fn(v); }).call(this)); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the JavaScript iterable protocol.
|
* Implements the JavaScript iterable protocol.
|
|
@ -6,9 +6,14 @@ const headers = {
|
||||||
'User-Agent': 'Etherpad/' + settings.getEpVersion(),
|
'User-Agent': 'Etherpad/' + settings.getEpVersion(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Infos = {
|
||||||
|
latestVersion: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const updateInterval = 60 * 60 * 1000; // 1 hour
|
const updateInterval = 60 * 60 * 1000; // 1 hour
|
||||||
let infos;
|
let infos: Infos;
|
||||||
let lastLoadingTime = null;
|
let lastLoadingTime: number | null = null;
|
||||||
|
|
||||||
const loadEtherpadInformations = () => {
|
const loadEtherpadInformations = () => {
|
||||||
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
|
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
|
||||||
|
@ -16,7 +21,7 @@ const loadEtherpadInformations = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.get('https://static.etherpad.org/info.json', {headers: headers})
|
return axios.get('https://static.etherpad.org/info.json', {headers: headers})
|
||||||
.then(async resp => {
|
.then(async (resp: any) => {
|
||||||
infos = await resp.data;
|
infos = await resp.data;
|
||||||
if (infos === undefined || infos === null) {
|
if (infos === undefined || infos === null) {
|
||||||
await Promise.reject("Could not retrieve current version")
|
await Promise.reject("Could not retrieve current version")
|
||||||
|
@ -26,7 +31,7 @@ const loadEtherpadInformations = () => {
|
||||||
lastLoadingTime = Date.now();
|
lastLoadingTime = Date.now();
|
||||||
return await Promise.resolve(infos);
|
return await Promise.resolve(infos);
|
||||||
})
|
})
|
||||||
.catch(async err => {
|
.catch(async (err: Error) => {
|
||||||
return await Promise.reject(err);
|
return await Promise.reject(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -37,20 +42,20 @@ exports.getLatestVersion = () => {
|
||||||
return infos?.latestVersion;
|
return infos?.latestVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.needsUpdate = async (cb) => {
|
exports.needsUpdate = async (cb: Function) => {
|
||||||
await loadEtherpadInformations()
|
await loadEtherpadInformations()
|
||||||
.then((info) => {
|
.then((info:Infos) => {
|
||||||
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
|
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
|
||||||
if (cb) return cb(true);
|
if (cb) return cb(true);
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err: Error) => {
|
||||||
console.error(`Can not perform Etherpad update check: ${err}`);
|
console.error(`Can not perform Etherpad update check: ${err}`);
|
||||||
if (cb) return cb(false);
|
if (cb) return cb(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.check = () => {
|
exports.check = () => {
|
||||||
exports.needsUpdate((needsUpdate) => {
|
exports.needsUpdate((needsUpdate: boolean) => {
|
||||||
if (needsUpdate) {
|
if (needsUpdate) {
|
||||||
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
||||||
}
|
}
|
|
@ -5,6 +5,6 @@
|
||||||
*/
|
*/
|
||||||
const cryptoMod = require('crypto');
|
const cryptoMod = require('crypto');
|
||||||
|
|
||||||
const randomString = (len: string) => cryptoMod.randomBytes(len).toString('hex');
|
const randomString = (len: number) => cryptoMod.randomBytes(len).toString('hex');
|
||||||
|
|
||||||
module.exports = randomString;
|
module.exports = randomString;
|
||||||
|
|
|
@ -4,7 +4,7 @@ const path = require('path');
|
||||||
|
|
||||||
// Normalizes p and ensures that it is a relative path that does not reach outside. See
|
// Normalizes p and ensures that it is a relative path that does not reach outside. See
|
||||||
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
|
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
|
||||||
module.exports = (p, pathApi = path) => {
|
module.exports = (p: string, pathApi = path) => {
|
||||||
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
|
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
|
||||||
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
|
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
|
||||||
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
|
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
|
|
@ -1,270 +0,0 @@
|
||||||
'use strict';
|
|
||||||
/**
|
|
||||||
* The Toolbar Module creates and renders the toolbars and buttons
|
|
||||||
*/
|
|
||||||
const _ = require('underscore');
|
|
||||||
|
|
||||||
const removeItem = (array, what) => {
|
|
||||||
let ax;
|
|
||||||
while ((ax = array.indexOf(what)) !== -1) {
|
|
||||||
array.splice(ax, 1);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultButtonAttributes = (name, overrides) => ({
|
|
||||||
command: name,
|
|
||||||
localizationId: `pad.toolbar.${name}.title`,
|
|
||||||
class: `buttonicon buttonicon-${name}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tag = (name, attributes, contents) => {
|
|
||||||
const aStr = tagAttributes(attributes);
|
|
||||||
|
|
||||||
if (_.isString(contents) && contents.length > 0) {
|
|
||||||
return `<${name}${aStr}>${contents}</${name}>`;
|
|
||||||
} else {
|
|
||||||
return `<${name}${aStr}></${name}>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagAttributes = (attributes) => {
|
|
||||||
attributes = _.reduce(attributes || {}, (o, val, name) => {
|
|
||||||
if (!_.isUndefined(val)) {
|
|
||||||
o[name] = val;
|
|
||||||
}
|
|
||||||
return o;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ButtonsGroup = function () {
|
|
||||||
this.buttons = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
ButtonsGroup.fromArray = function (array) {
|
|
||||||
const btnGroup = new this();
|
|
||||||
_.each(array, (btnName) => {
|
|
||||||
btnGroup.addButton(Button.load(btnName));
|
|
||||||
});
|
|
||||||
return btnGroup;
|
|
||||||
};
|
|
||||||
|
|
||||||
ButtonsGroup.prototype.addButton = function (button) {
|
|
||||||
this.buttons.push(button);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
ButtonsGroup.prototype.render = function () {
|
|
||||||
if (this.buttons && this.buttons.length === 1) {
|
|
||||||
this.buttons[0].grouping = '';
|
|
||||||
} else if (this.buttons && this.buttons.length > 1) {
|
|
||||||
_.first(this.buttons).grouping = 'grouped-left';
|
|
||||||
_.last(this.buttons).grouping = 'grouped-right';
|
|
||||||
_.each(this.buttons.slice(1, -1), (btn) => {
|
|
||||||
btn.grouping = 'grouped-middle';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.map(this.buttons, (btn) => {
|
|
||||||
if (btn) return btn.render();
|
|
||||||
}).join('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const Button = function (attributes) {
|
|
||||||
this.attributes = attributes;
|
|
||||||
};
|
|
||||||
|
|
||||||
Button.load = (btnName) => {
|
|
||||||
const button = module.exports.availableButtons[btnName];
|
|
||||||
try {
|
|
||||||
if (button.constructor === Button || button.constructor === SelectButton) {
|
|
||||||
return button;
|
|
||||||
} else {
|
|
||||||
return new Button(button);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Error loading button', btnName);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_.extend(Button.prototype, {
|
|
||||||
grouping: '',
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const liAttributes = {
|
|
||||||
'data-type': 'button',
|
|
||||||
'data-key': this.attributes.command,
|
|
||||||
};
|
|
||||||
return tag('li', liAttributes,
|
|
||||||
tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId},
|
|
||||||
tag('button', {
|
|
||||||
'class': ` ${this.attributes.class}`,
|
|
||||||
'data-l10n-id': this.attributes.localizationId,
|
|
||||||
})));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const SelectButton = function (attributes) {
|
|
||||||
this.attributes = attributes;
|
|
||||||
this.options = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
_.extend(SelectButton.prototype, Button.prototype, {
|
|
||||||
addOption(value, text, attributes) {
|
|
||||||
this.options.push({
|
|
||||||
value,
|
|
||||||
text,
|
|
||||||
attributes,
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
select(attributes) {
|
|
||||||
const options = [];
|
|
||||||
|
|
||||||
_.each(this.options, (opt) => {
|
|
||||||
const a = _.extend({
|
|
||||||
value: opt.value,
|
|
||||||
}, opt.attributes);
|
|
||||||
|
|
||||||
options.push(tag('option', a, opt.text));
|
|
||||||
});
|
|
||||||
return tag('select', attributes, options.join(''));
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const attributes = {
|
|
||||||
'id': this.attributes.id,
|
|
||||||
'data-key': this.attributes.command,
|
|
||||||
'data-type': 'select',
|
|
||||||
};
|
|
||||||
return tag('li', attributes, this.select({id: this.attributes.selectId}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Separator = function () {};
|
|
||||||
Separator.prototype.render = function () {
|
|
||||||
return tag('li', {class: 'separator'});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
availableButtons: {
|
|
||||||
bold: defaultButtonAttributes('bold'),
|
|
||||||
italic: defaultButtonAttributes('italic'),
|
|
||||||
underline: defaultButtonAttributes('underline'),
|
|
||||||
strikethrough: defaultButtonAttributes('strikethrough'),
|
|
||||||
|
|
||||||
orderedlist: {
|
|
||||||
command: 'insertorderedlist',
|
|
||||||
localizationId: 'pad.toolbar.ol.title',
|
|
||||||
class: 'buttonicon buttonicon-insertorderedlist',
|
|
||||||
},
|
|
||||||
|
|
||||||
unorderedlist: {
|
|
||||||
command: 'insertunorderedlist',
|
|
||||||
localizationId: 'pad.toolbar.ul.title',
|
|
||||||
class: 'buttonicon buttonicon-insertunorderedlist',
|
|
||||||
},
|
|
||||||
|
|
||||||
indent: defaultButtonAttributes('indent'),
|
|
||||||
outdent: {
|
|
||||||
command: 'outdent',
|
|
||||||
localizationId: 'pad.toolbar.unindent.title',
|
|
||||||
class: 'buttonicon buttonicon-outdent',
|
|
||||||
},
|
|
||||||
|
|
||||||
undo: defaultButtonAttributes('undo'),
|
|
||||||
redo: defaultButtonAttributes('redo'),
|
|
||||||
|
|
||||||
clearauthorship: {
|
|
||||||
command: 'clearauthorship',
|
|
||||||
localizationId: 'pad.toolbar.clearAuthorship.title',
|
|
||||||
class: 'buttonicon buttonicon-clearauthorship',
|
|
||||||
},
|
|
||||||
|
|
||||||
importexport: {
|
|
||||||
command: 'import_export',
|
|
||||||
localizationId: 'pad.toolbar.import_export.title',
|
|
||||||
class: 'buttonicon buttonicon-import_export',
|
|
||||||
},
|
|
||||||
|
|
||||||
timeslider: {
|
|
||||||
command: 'showTimeSlider',
|
|
||||||
localizationId: 'pad.toolbar.timeslider.title',
|
|
||||||
class: 'buttonicon buttonicon-history',
|
|
||||||
},
|
|
||||||
|
|
||||||
savedrevision: defaultButtonAttributes('savedRevision'),
|
|
||||||
settings: defaultButtonAttributes('settings'),
|
|
||||||
embed: defaultButtonAttributes('embed'),
|
|
||||||
showusers: defaultButtonAttributes('showusers'),
|
|
||||||
|
|
||||||
timeslider_export: {
|
|
||||||
command: 'import_export',
|
|
||||||
localizationId: 'timeslider.toolbar.exportlink.title',
|
|
||||||
class: 'buttonicon buttonicon-import_export',
|
|
||||||
},
|
|
||||||
|
|
||||||
timeslider_settings: {
|
|
||||||
command: 'settings',
|
|
||||||
localizationId: 'pad.toolbar.settings.title',
|
|
||||||
class: 'buttonicon buttonicon-settings',
|
|
||||||
},
|
|
||||||
|
|
||||||
timeslider_returnToPad: {
|
|
||||||
command: 'timeslider_returnToPad',
|
|
||||||
localizationId: 'timeslider.toolbar.returnbutton',
|
|
||||||
class: 'buttontext',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
registerButton(buttonName, buttonInfo) {
|
|
||||||
this.availableButtons[buttonName] = buttonInfo;
|
|
||||||
},
|
|
||||||
|
|
||||||
button: (attributes) => new Button(attributes),
|
|
||||||
|
|
||||||
separator: () => (new Separator()).render(),
|
|
||||||
|
|
||||||
selectButton: (attributes) => new SelectButton(attributes),
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Valid values for whichMenu: 'left' | 'right' | 'timeslider-right'
|
|
||||||
* Valid values for page: 'pad' | 'timeslider'
|
|
||||||
*/
|
|
||||||
menu(buttons, isReadOnly, whichMenu, page) {
|
|
||||||
if (isReadOnly) {
|
|
||||||
// The best way to detect if it's the left editbar is to check for a bold button
|
|
||||||
if (buttons[0].indexOf('bold') !== -1) {
|
|
||||||
// Clear all formatting buttons
|
|
||||||
buttons = [];
|
|
||||||
} else {
|
|
||||||
// Remove Save Revision from the right menu
|
|
||||||
removeItem(buttons[0], 'savedrevision');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/*
|
|
||||||
* This pad is not read only
|
|
||||||
*
|
|
||||||
* Add back the savedrevision button (the "star") if is not already there,
|
|
||||||
* but only on the right toolbar, and only if we are showing a pad (dont't
|
|
||||||
* do it in the timeslider).
|
|
||||||
*
|
|
||||||
* This is a quick fix for #3702 (and subsequent issue #3767): it was
|
|
||||||
* sufficient to visit a single read only pad to cause the disappearence
|
|
||||||
* of the star button from all the pads.
|
|
||||||
*/
|
|
||||||
if ((buttons[0].indexOf('savedrevision') === -1) &&
|
|
||||||
(whichMenu === 'right') && (page === 'pad')) {
|
|
||||||
buttons[0].push('savedrevision');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = _.map(buttons, (group) => ButtonsGroup.fromArray(group).render());
|
|
||||||
return groups.join(this.separator());
|
|
||||||
},
|
|
||||||
};
|
|
305
src/node/utils/toolbar.ts
Normal file
305
src/node/utils/toolbar.ts
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
'use strict';
|
||||||
|
/**
|
||||||
|
* The Toolbar Module creates and renders the toolbars and buttons
|
||||||
|
*/
|
||||||
|
const _ = require('underscore');
|
||||||
|
|
||||||
|
const removeItem = (array: string[], what: string) => {
|
||||||
|
let ax;
|
||||||
|
while ((ax = array.indexOf(what)) !== -1) {
|
||||||
|
array.splice(ax, 1);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultButtonAttributes = (name: string, overrides?: boolean) => ({
|
||||||
|
command: name,
|
||||||
|
localizationId: `pad.toolbar.${name}.title`,
|
||||||
|
class: `buttonicon buttonicon-${name}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tag = (name: string, attributes: AttributeObj, contents?: string) => {
|
||||||
|
const aStr = tagAttributes(attributes);
|
||||||
|
|
||||||
|
if (_.isString(contents) && contents!.length > 0) {
|
||||||
|
return `<${name}${aStr}>${contents}</${name}>`;
|
||||||
|
} else {
|
||||||
|
return `<${name}${aStr}></${name}>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
type AttributeObj = {
|
||||||
|
[id: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagAttributes = (attributes: AttributeObj) => {
|
||||||
|
attributes = _.reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => {
|
||||||
|
if (!_.isUndefined(val)) {
|
||||||
|
o[name] = val;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return ` ${_.map(attributes, (val: string, name: string) => `${name}="${_.escape(val)}"`).join(' ')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ButtonGroupType = {
|
||||||
|
grouping: string,
|
||||||
|
render: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
class ButtonGroup {
|
||||||
|
private buttons: Button[]
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.buttons = []
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromArray = function (array: string[]) {
|
||||||
|
const btnGroup = new ButtonGroup();
|
||||||
|
_.each(array, (btnName: string) => {
|
||||||
|
const button = Button.load(btnName) as Button
|
||||||
|
btnGroup.addButton(button);
|
||||||
|
});
|
||||||
|
return btnGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addButton(button: Button) {
|
||||||
|
this.buttons.push(button);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.buttons && this.buttons.length === 1) {
|
||||||
|
this.buttons[0].grouping = '';
|
||||||
|
} else if (this.buttons && this.buttons.length > 1) {
|
||||||
|
_.first(this.buttons).grouping = 'grouped-left';
|
||||||
|
_.last(this.buttons).grouping = 'grouped-right';
|
||||||
|
_.each(this.buttons.slice(1, -1), (btn: Button) => {
|
||||||
|
btn.grouping = 'grouped-middle';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.map(this.buttons, (btn: ButtonGroup) => {
|
||||||
|
if (btn) return btn.render();
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Button {
|
||||||
|
protected attributes: AttributeObj
|
||||||
|
grouping: string
|
||||||
|
|
||||||
|
constructor(attributes: AttributeObj) {
|
||||||
|
this.attributes = attributes
|
||||||
|
this.grouping = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
public static load(btnName: string) {
|
||||||
|
const button = module.exports.availableButtons[btnName];
|
||||||
|
try {
|
||||||
|
if (button.constructor === Button || button.constructor === SelectButton) {
|
||||||
|
return button;
|
||||||
|
} else {
|
||||||
|
return new Button(button);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error loading button', btnName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const liAttributes = {
|
||||||
|
'data-type': 'button',
|
||||||
|
'data-key': this.attributes.command,
|
||||||
|
};
|
||||||
|
return tag('li', liAttributes,
|
||||||
|
tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId},
|
||||||
|
tag('button', {
|
||||||
|
'class': ` ${this.attributes.class}`,
|
||||||
|
'data-l10n-id': this.attributes.localizationId,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectButtonOptions = {
|
||||||
|
value: string,
|
||||||
|
text: string,
|
||||||
|
attributes: AttributeObj
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectButton extends Button {
|
||||||
|
private readonly options: SelectButtonOptions[];
|
||||||
|
|
||||||
|
constructor(attrs: AttributeObj) {
|
||||||
|
super(attrs);
|
||||||
|
this.options = []
|
||||||
|
}
|
||||||
|
|
||||||
|
addOption(value: string, text: string, attributes: AttributeObj) {
|
||||||
|
this.options.push({
|
||||||
|
value,
|
||||||
|
text,
|
||||||
|
attributes,
|
||||||
|
})
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
select(attributes: AttributeObj) {
|
||||||
|
const options: string[] = [];
|
||||||
|
|
||||||
|
_.each(this.options, (opt: AttributeSelect) => {
|
||||||
|
const a = _.extend({
|
||||||
|
value: opt.value,
|
||||||
|
}, opt.attributes);
|
||||||
|
|
||||||
|
options.push(tag('option', a, opt.text));
|
||||||
|
});
|
||||||
|
return tag('select', attributes, options.join(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const attributes = {
|
||||||
|
'id': this.attributes.id,
|
||||||
|
'data-key': this.attributes.command,
|
||||||
|
'data-type': 'select',
|
||||||
|
};
|
||||||
|
return tag('li', attributes, this.select({id: this.attributes.selectId}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type AttributeSelect = {
|
||||||
|
value: string,
|
||||||
|
attributes: AttributeObj,
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class Separator {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return tag('li', {class: 'separator'});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
availableButtons: {
|
||||||
|
bold: defaultButtonAttributes('bold'),
|
||||||
|
italic: defaultButtonAttributes('italic'),
|
||||||
|
underline: defaultButtonAttributes('underline'),
|
||||||
|
strikethrough: defaultButtonAttributes('strikethrough'),
|
||||||
|
|
||||||
|
orderedlist: {
|
||||||
|
command: 'insertorderedlist',
|
||||||
|
localizationId: 'pad.toolbar.ol.title',
|
||||||
|
class: 'buttonicon buttonicon-insertorderedlist',
|
||||||
|
},
|
||||||
|
|
||||||
|
unorderedlist: {
|
||||||
|
command: 'insertunorderedlist',
|
||||||
|
localizationId: 'pad.toolbar.ul.title',
|
||||||
|
class: 'buttonicon buttonicon-insertunorderedlist',
|
||||||
|
},
|
||||||
|
|
||||||
|
indent: defaultButtonAttributes('indent'),
|
||||||
|
outdent: {
|
||||||
|
command: 'outdent',
|
||||||
|
localizationId: 'pad.toolbar.unindent.title',
|
||||||
|
class: 'buttonicon buttonicon-outdent',
|
||||||
|
},
|
||||||
|
|
||||||
|
undo: defaultButtonAttributes('undo'),
|
||||||
|
redo: defaultButtonAttributes('redo'),
|
||||||
|
|
||||||
|
clearauthorship: {
|
||||||
|
command: 'clearauthorship',
|
||||||
|
localizationId: 'pad.toolbar.clearAuthorship.title',
|
||||||
|
class: 'buttonicon buttonicon-clearauthorship',
|
||||||
|
},
|
||||||
|
|
||||||
|
importexport: {
|
||||||
|
command: 'import_export',
|
||||||
|
localizationId: 'pad.toolbar.import_export.title',
|
||||||
|
class: 'buttonicon buttonicon-import_export',
|
||||||
|
},
|
||||||
|
|
||||||
|
timeslider: {
|
||||||
|
command: 'showTimeSlider',
|
||||||
|
localizationId: 'pad.toolbar.timeslider.title',
|
||||||
|
class: 'buttonicon buttonicon-history',
|
||||||
|
},
|
||||||
|
|
||||||
|
savedrevision: defaultButtonAttributes('savedRevision'),
|
||||||
|
settings: defaultButtonAttributes('settings'),
|
||||||
|
embed: defaultButtonAttributes('embed'),
|
||||||
|
showusers: defaultButtonAttributes('showusers'),
|
||||||
|
|
||||||
|
timeslider_export: {
|
||||||
|
command: 'import_export',
|
||||||
|
localizationId: 'timeslider.toolbar.exportlink.title',
|
||||||
|
class: 'buttonicon buttonicon-import_export',
|
||||||
|
},
|
||||||
|
|
||||||
|
timeslider_settings: {
|
||||||
|
command: 'settings',
|
||||||
|
localizationId: 'pad.toolbar.settings.title',
|
||||||
|
class: 'buttonicon buttonicon-settings',
|
||||||
|
},
|
||||||
|
|
||||||
|
timeslider_returnToPad: {
|
||||||
|
command: 'timeslider_returnToPad',
|
||||||
|
localizationId: 'timeslider.toolbar.returnbutton',
|
||||||
|
class: 'buttontext',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
registerButton(buttonName: string, buttonInfo: any) {
|
||||||
|
this.availableButtons[buttonName] = buttonInfo;
|
||||||
|
},
|
||||||
|
|
||||||
|
button: (attributes: AttributeObj) => new Button(attributes),
|
||||||
|
|
||||||
|
separator: () => (new Separator()).render(),
|
||||||
|
|
||||||
|
selectButton: (attributes: AttributeObj) => new SelectButton(attributes),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Valid values for whichMenu: 'left' | 'right' | 'timeslider-right'
|
||||||
|
* Valid values for page: 'pad' | 'timeslider'
|
||||||
|
*/
|
||||||
|
menu(buttons: string[][], isReadOnly: boolean, whichMenu: string, page: string) {
|
||||||
|
if (isReadOnly) {
|
||||||
|
// The best way to detect if it's the left editbar is to check for a bold button
|
||||||
|
if (buttons[0].indexOf('bold') !== -1) {
|
||||||
|
// Clear all formatting buttons
|
||||||
|
buttons = [];
|
||||||
|
} else {
|
||||||
|
// Remove Save Revision from the right menu
|
||||||
|
removeItem(buttons[0], 'savedrevision');
|
||||||
|
}
|
||||||
|
} else if ((buttons[0].indexOf('savedrevision') === -1) &&
|
||||||
|
(whichMenu === 'right') && (page === 'pad')) {
|
||||||
|
/*
|
||||||
|
* This pad is not read only
|
||||||
|
*
|
||||||
|
* Add back the savedrevision button (the "star") if is not already there,
|
||||||
|
* but only on the right toolbar, and only if we are showing a pad (dont't
|
||||||
|
* do it in the timeslider).
|
||||||
|
*
|
||||||
|
* This is a quick fix for #3702 (and subsequent issue #3767): it was
|
||||||
|
* sufficient to visit a single read only pad to cause the disappearence
|
||||||
|
* of the star button from all the pads.
|
||||||
|
*/
|
||||||
|
buttons[0].push('savedrevision');
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = _.map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render());
|
||||||
|
return groups.join(this.separator());
|
||||||
|
},
|
||||||
|
};
|
|
@ -108,7 +108,8 @@
|
||||||
"test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
"test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
||||||
"test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api",
|
"test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api",
|
||||||
"dev": "node --import tsx node/server.ts",
|
"dev": "node --import tsx node/server.ts",
|
||||||
"prod": "node --import tsx node/server.ts"
|
"prod": "node --import tsx node/server.ts",
|
||||||
|
"ts-check": "tsc --noEmit --watch"
|
||||||
},
|
},
|
||||||
"version": "1.9.6",
|
"version": "1.9.6",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue