misc: configure and fix eslint formatting (#66719)

This commit is contained in:
Corentin Sechet 2022-09-22 12:58:44 +02:00
parent 7ee0085317
commit 253620ecd0
8 changed files with 471 additions and 426 deletions

View File

@ -6,7 +6,8 @@ parserOptions:
ecmaVersion: 13
sourceType: module
rules:
# https://standardjs.com/rules.html
# Follow Standard JS guidelines : https://standardjs.com/rules.html, except rules
# annotated with a 'custom' comment
# Linting
array-callback-return: error
@ -78,3 +79,51 @@ rules:
no-with: error
use-isnan: error
valid-typeof: error
# Style / Formatting
accessor-pairs: error
block-spacing: error
brace-style: [error, 1tbs, {allowSingleLine: true}]
camelcase: error
comma-dangle: [error, always-multiline] # custom : Adding a dangling comma make patches shorter
comma-spacing: error
comma-style: error
curly: [error, multi-line]
dot-location: [error, property]
eol-last: [error, always]
func-call-spacing: error
indent: [error, 2]
key-spacing: error
keyword-spacing: error
max-len: [error, {code: 110}] # custom: configured like this on python projects
new-cap: [error, { newIsCap: true, capIsNew: false}]
new-parens: error
no-extra-parens: [error, functions]
no-floating-decimal: error
no-irregular-whitespace: error
no-lone-blocks: error
no-mixed-spaces-and-tabs: error
no-multi-spaces: error
no-multi-str: error
no-multiple-empty-lines: error
no-tabs: error
no-trailing-spaces: error
no-undef-init: error
no-useless-rename: error
no-whitespace-before-property: error
object-property-newline: [error, { allowMultiplePropertiesPerLine: true }]
one-var: [error, never]
operator-linebreak: [error, before]
padded-blocks: [error, never]
quotes: [error, single]
rest-spread-spacing: error
semi-spacing: error
semi: [error, never]
space-before-function-paren: error
space-in-parens: error
space-infix-ops: error
space-unary-ops: error
spaced-comment: error
template-curly-spacing: error
wrap-iife: [error, any]
yield-star-spacing: [error, {after: true, before: true}]

View File

@ -1,6 +1,6 @@
import buble from '@rollup/plugin-buble';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { terser } from "rollup-plugin-terser";
import buble from '@rollup/plugin-buble'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
export default {
input: './src-js/godo.js',
@ -8,14 +8,14 @@ export default {
{
file: 'dist/js/godo.js',
format: 'es',
sourcemap: true
sourcemap: true,
},
{
file: 'dist/js/godo.min.js',
format: 'es',
sourcemap: true,
plugins: [terser()],
}
},
],
plugins: [nodeResolve(), buble()]
plugins: [nodeResolve(), buble()],
}

View File

@ -1,5 +1,5 @@
import {findWrapping, liftTarget, canSplit, ReplaceAroundStep} from "prosemirror-transform"
import {Slice, Fragment, NodeRange} from "prosemirror-model"
import {findWrapping, liftTarget, canSplit, ReplaceAroundStep} from 'prosemirror-transform'
import {Slice, Fragment, NodeRange} from 'prosemirror-model'
// wrapInList, doWrapInList, splitListItem, liftToOuterList, liftOutOfList, sinkListItem
@ -10,19 +10,21 @@ import {Slice, Fragment, NodeRange} from "prosemirror-model"
// the given type an attributes. If `dispatch` is null, only return a
// value to indicate whether this is possible, but don't actually
// perform the change.
export function wrapInList(listType, attrs) {
return function(state, dispatch) {
export function wrapInList (listType, attrs) {
return function (state, dispatch) {
let {$from, $to} = state.selection
let range = $from.blockRange($to), doJoin = false, outerRange = range
let range = $from.blockRange($to); let doJoin = false; let outerRange = range
if (!range) return false
// This is at the top of an existing list item
if (range.depth >= 2 && $from.node(range.depth - 1).type.compatibleContent(listType) && range.startIndex === 0) {
if (range.depth >= 2 && $from.node(range.depth - 1).type.compatibleContent(listType)
&& range.startIndex === 0) {
// Don't do anything if this is the top of the list
if ($from.index(range.depth - 1) === 0) return false
let $insert = state.doc.resolve(range.start - 2)
outerRange = new NodeRange($insert, $insert, range.depth)
if (range.endIndex < range.parent.childCount)
if (range.endIndex < range.parent.childCount) {
range = new NodeRange($from, state.doc.resolve($to.end(range.depth)), range.depth)
}
doJoin = true
}
let wrap = findWrapping(outerRange, listType, attrs, range)
@ -32,19 +34,20 @@ export function wrapInList(listType, attrs) {
}
}
function doWrapInList(tr, range, wrappers, joinBefore, listType) {
function doWrapInList (tr, range, wrappers, joinBefore, listType) {
let content = Fragment.empty
for (let i = wrappers.length - 1; i >= 0; i--)
for (let i = wrappers.length - 1; i >= 0; i--) {
content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content))
}
tr.step(new ReplaceAroundStep(range.start - (joinBefore ? 2 : 0), range.end, range.start, range.end,
new Slice(content, 0, 0), wrappers.length, true))
new Slice(content, 0, 0), wrappers.length, true))
let found = 0
for (let i = 0; i < wrappers.length; i++) if (wrappers[i].type === listType) found = i + 1
let splitDepth = wrappers.length - found
let splitPos = range.start + wrappers.length - (joinBefore ? 2 : 0), parent = range.parent
let splitPos = range.start + wrappers.length - (joinBefore ? 2 : 0); let parent = range.parent
for (let i = range.startIndex, e = range.endIndex, first = true; i < e; i++, first = false) {
if (!first && canSplit(tr.doc, splitPos, splitDepth)) {
tr.split(splitPos, splitDepth)
@ -58,8 +61,8 @@ function doWrapInList(tr, range, wrappers, joinBefore, listType) {
// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
// Build a command that splits a non-empty textblock at the top level
// of a list item by also splitting that list item.
export function splitListItem(itemType) {
return function(state, dispatch) {
export function splitListItem (itemType) {
return function (state, dispatch) {
let {$from, $to, node} = state.selection
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) return false
let grandParent = $from.node(-1)
@ -68,17 +71,18 @@ export function splitListItem(itemType) {
// In an empty block. If this is a nested list, the wrapping
// list item should be split. Otherwise, bail out and let next
// command handle lifting.
if ($from.depth === 2 || $from.node(-3).type !== itemType ||
$from.index(-2) !== $from.node(-2).childCount - 1) return false
if ($from.depth === 2 || $from.node(-3).type !== itemType
|| $from.index(-2) !== $from.node(-2).childCount - 1) return false
if (dispatch) {
let wrap = Fragment.empty
let depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3
// Build a fragment containing empty versions of the structure
// from the outer list item to the parent node of the cursor
for (let d = $from.depth - depthBefore; d >= $from.depth - 3; d--)
for (let d = $from.depth - depthBefore; d >= $from.depth - 3; d--) {
wrap = Fragment.from($from.node(d).copy(wrap))
}
let depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1
: $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3
: $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3
// Add a second list item with an empty default start node
wrap = wrap.append(Fragment.from(itemType.createAndFill()))
let start = $from.before($from.depth - (depthBefore - 1))
@ -105,54 +109,54 @@ export function splitListItem(itemType) {
// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
// Create a command to lift the list item around the selection up into
// a wrapping list.
export function liftListItem(itemType) {
return function(state, dispatch) {
export function liftListItem (itemType) {
return function (state, dispatch) {
let {$from, $to} = state.selection
let range = $from.blockRange($to, node => node.childCount && node.firstChild.type === itemType)
if (!range) return false
if (!dispatch) return true
if ($from.node(range.depth - 1).type === itemType) // Inside a parent list
if ($from.node(range.depth - 1).type === itemType) { // Inside a parent list
return liftToOuterList(state, dispatch, itemType, range)
else // Outer list node
} else { // Outer list node
return liftOutOfList(state, dispatch, range)
}
}
}
function liftToOuterList(state, dispatch, itemType, range) {
let tr = state.tr, end = range.end, endOfList = range.$to.end(range.depth)
function liftToOuterList (state, dispatch, itemType, range) {
let tr = state.tr; let end = range.end; let endOfList = range.$to.end(range.depth)
if (end < endOfList) {
// There are siblings after the lifted items, which must become
// children of the last item
tr.step(new ReplaceAroundStep(end - 1, endOfList, end, endOfList,
new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true))
new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true))
range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth)
}
dispatch(tr.lift(range, liftTarget(range)).scrollIntoView())
return true
}
function liftOutOfList(state, dispatch, range) {
let tr = state.tr, list = range.parent
function liftOutOfList (state, dispatch, range) {
let tr = state.tr; let list = range.parent
// Merge the list items into a single big item
for (let pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
pos -= list.child(i).nodeSize
tr.delete(pos - 1, pos + 1)
}
let $start = tr.doc.resolve(range.start), item = $start.nodeAfter
let $start = tr.doc.resolve(range.start); let item = $start.nodeAfter
if (tr.mapping.map(range.end) !== range.start + $start.nodeAfter.nodeSize) return false
let atStart = range.startIndex === 0, atEnd = range.endIndex === list.childCount
let parent = $start.node(-1), indexBefore = $start.index(-1)
let atStart = range.startIndex === 0; let atEnd = range.endIndex === list.childCount
let parent = $start.node(-1); let indexBefore = $start.index(-1)
if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
item.content.append(atEnd ? Fragment.empty : Fragment.from(list))))
return false
let start = $start.pos, end = start + item.nodeSize
item.content.append(atEnd ? Fragment.empty : Fragment.from(list)))) { return false }
let start = $start.pos; let end = start + item.nodeSize
// Strip off the surrounding list. At the sides where we're not at
// the end of the list, the existing list is closed. At sides where
// this is the end, it is overwritten to its end.
tr.step(new ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
new Slice((atStart ? Fragment.empty : Fragment.from(list.copy(Fragment.empty)))
.append(atEnd ? Fragment.empty : Fragment.from(list.copy(Fragment.empty))),
atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1))
new Slice((atStart ? Fragment.empty : Fragment.from(list.copy(Fragment.empty)))
.append(atEnd ? Fragment.empty : Fragment.from(list.copy(Fragment.empty))),
atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1))
dispatch(tr.scrollIntoView())
return true
}
@ -160,25 +164,26 @@ function liftOutOfList(state, dispatch, range) {
// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
// Create a command to sink the list item around the selection down
// into an inner list.
export function sinkListItem(itemType) {
return function(state, dispatch) {
export function sinkListItem (itemType) {
return function (state, dispatch) {
let {$from, $to} = state.selection
let range = $from.blockRange($to, node => node.childCount && node.firstChild.type === itemType)
if (!range) return false
let startIndex = range.startIndex
if (startIndex === 0) return false
let parent = range.parent, nodeBefore = parent.child(startIndex - 1)
let parent = range.parent; let nodeBefore = parent.child(startIndex - 1)
if (nodeBefore.type !== itemType) return false
if (dispatch) {
let nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type === parent.type
let inner = Fragment.from(nestedBefore ? itemType.create() : null)
let slice = new Slice(Fragment.from(itemType.create(null, Fragment.from(parent.type.create(null, inner)))),
nestedBefore ? 3 : 1, 0)
let before = range.start, after = range.end
let slice = new Slice(Fragment.from(itemType.create(null,
Fragment.from(parent.type.create(null, inner)))),
nestedBefore ? 3 : 1, 0)
let before = range.start; let after = range.end
dispatch(state.tr.step(new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
before, after, slice, 1, true))
.scrollIntoView())
before, after, slice, 1, true))
.scrollIntoView())
}
return true
}

View File

@ -1,9 +1,9 @@
import {wrapIn, setBlockType, chainCommands, toggleMark, exitCode,
joinUp, joinDown, lift, selectParentNode} from "prosemirror-commands"
import {splitListItem, liftListItem} from "./godo-additional-commands.mjs"
import {undo, redo} from "prosemirror-history"
joinUp, joinDown, lift, selectParentNode} from 'prosemirror-commands'
import {splitListItem, liftListItem} from './godo-additional-commands.mjs'
import {undo, redo} from 'prosemirror-history'
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false
const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false
// :: (Schema, ?Object) → Object
// Inspect the given schema looking for marks and nodes from the
@ -31,9 +31,9 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) :
// You can suppress or map these bindings by passing a `mapKeys`
// argument, which maps key names (say `"Mod-B"` to either `false`, to
// remove the binding, or a new key name string.
export function buildKeymap(schema, mapKeys, options) {
let keys = {}, type;
function bind(key, cmd) {
export function buildKeymap (schema, mapKeys, options) {
let keys = {}; let type
function bind (key, cmd) {
if (mapKeys) {
let mapped = mapKeys[key]
if (mapped === false) return
@ -42,59 +42,53 @@ export function buildKeymap(schema, mapKeys, options) {
keys[key] = cmd
}
bind("Mod-z", undo)
bind("Shift-Mod-z", redo)
if (!mac) bind("Mod-y", redo)
bind('Mod-z', undo)
bind('Shift-Mod-z', redo)
if (!mac) bind('Mod-y', redo)
bind("Alt-ArrowUp", joinUp)
bind("Alt-ArrowDown", joinDown)
bind("Mod-BracketLeft", lift)
bind("Escape", selectParentNode)
bind('Alt-ArrowUp', joinUp)
bind('Alt-ArrowDown', joinDown)
bind('Mod-BracketLeft', lift)
bind('Escape', selectParentNode)
if ((type = schema.marks.strong)) {
bind("Mod-b", toggleMark(type))
bind("Mod-B", toggleMark(type))
bind('Mod-b', toggleMark(type))
bind('Mod-B', toggleMark(type))
}
if ((type = schema.marks.em)) {
bind("Mod-i", toggleMark(type))
bind("Mod-I", toggleMark(type))
bind('Mod-i', toggleMark(type))
bind('Mod-I', toggleMark(type))
}
if ((type = schema.marks.code))
bind("Mod-`", toggleMark(type))
if ((type = schema.nodes.blockquote))
bind("Ctrl->", wrapIn(type))
if ((type = schema.marks.code)) bind('Mod-`', toggleMark(type))
if ((type = schema.nodes.blockquote)) bind('Ctrl->', wrapIn(type))
if ((type = schema.nodes.list_item)) {
bind("Enter", splitListItem(type))
bind("Mod-[", liftListItem(type))
bind('Enter', splitListItem(type))
bind('Mod-[', liftListItem(type))
}
if ((type = schema.nodes.hard_break)) {
let br = type, cmd = chainCommands(exitCode, (state, dispatch) => {
let br = type; let cmd = chainCommands(exitCode, (state, dispatch) => {
dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView())
return true
})
bind("Mod-Enter", cmd)
bind("Shift-Enter", cmd)
if (mac) bind("Ctrl-Enter", cmd)
bind('Mod-Enter', cmd)
bind('Shift-Enter', cmd)
if (mac) bind('Ctrl-Enter', cmd)
}
if ((type = schema.nodes.paragraph))
bind("Shift-Ctrl-0", setBlockType(type))
if ((type = schema.nodes.paragraph)) bind('Shift-Ctrl-0', setBlockType(type))
if ((type = schema.nodes.code_block))
bind("Shift-Ctrl-\\", setBlockType(type))
if ((type = schema.nodes.code_block)) bind('Shift-Ctrl-\\', setBlockType(type))
if ((type = schema.nodes.heading)) {
options.heading_levels.forEach( (level, index) => {
bind(`Shift-Ctrl-${index+1}`, setBlockType(type, {level: level}))
});
options.heading_levels.forEach((level, index) => {
bind(`Shift-Ctrl-${index + 1}`, setBlockType(type, {level: level}))
})
}
if ((type = schema.nodes.horizontal_rule)) {
let hr = type
bind("Mod-_", (state, dispatch) => {
bind('Mod-_', (state, dispatch) => {
dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView())
return true
})

View File

@ -1,92 +1,92 @@
// menus language
const dLanguage = document.documentElement.lang.split('-')[0];
const nLanguage = navigator.languages ? navigator.languages[0] : navigator.language || navigator.userLanguage;
const language = (dLanguage) ? dLanguage : nLanguage;
const godoLanguage = (language === "fr") ? language : "en";
const dLanguage = document.documentElement.lang.split('-')[0]
const nLanguage = navigator.languages ? navigator.languages[0] : navigator.language || navigator.userLanguage
const language = (dLanguage) ? dLanguage : nLanguage
const godoLanguage = (language === 'fr') ? language : 'en'
const languageContent = {
"a": {
en: {
icon: "a",
text: "link"
},
fr: {
icon: "a",
text: 'lien'
}
},
"b": {
en: {
icon: "B",
text: "bold"
},
fr: {
icon: "G",
text: "gras"
}
},
"h": {
en: {
icon: "H",
text: "Headline"
},
fr: {
icon: "T",
text: "Titre"
}
},
"hh": {
en: {
icon: "h",
text: "Subhead"
},
fr: {
icon: "t",
text: "Sous-titre"
}
},
"hhh": {
en: {
icon: "hh",
text: 'Crosshead'
},
fr: {
icon: "tt",
text: "Intertitre"
}
},
"i": {
en: {
icon: "i",
text: "italic",
},
fr: {
icon: "i",
text: "italique"
}
},
"p": {
en: {
icon: "p",
text: "paragraph"
},
fr: {
icon: "p",
text: "paragraphe"
}
},
"ul": {
en: {
icon: "•",
text: "bullet list"
},
fr: {
icon: "•",
text: "Liste à puce"
}
}
'a': {
en: {
icon: 'a',
text: 'link',
},
fr: {
icon: 'a',
text: 'lien',
},
},
'b': {
en: {
icon: 'B',
text: 'bold',
},
fr: {
icon: 'G',
text: 'gras',
},
},
'h': {
en: {
icon: 'H',
text: 'Headline',
},
fr: {
icon: 'T',
text: 'Titre',
},
},
'hh': {
en: {
icon: 'h',
text: 'Subhead',
},
fr: {
icon: 't',
text: 'Sous-titre',
},
},
'hhh': {
en: {
icon: 'hh',
text: 'Crosshead',
},
fr: {
icon: 'tt',
text: 'Intertitre',
},
},
'i': {
en: {
icon: 'i',
text: 'italic',
},
fr: {
icon: 'i',
text: 'italique',
},
},
'p': {
en: {
icon: 'p',
text: 'paragraph',
},
fr: {
icon: 'p',
text: 'paragraphe',
},
},
'ul': {
en: {
icon: '•',
text: 'bullet list',
},
fr: {
icon: '•',
text: 'Liste à puce',
},
},
}
export default function getLanguage(content) {
return languageContent[content][godoLanguage];
export default function getLanguage (content) {
return languageContent[content][godoLanguage]
}

View File

@ -1,201 +1,198 @@
import {toggleMark, setBlockType, chainCommands} from "prosemirror-commands";
import {wrapInList, liftListItem} from "./godo-additional-commands.mjs";
import getLanguage from "./godo-menus-language.mjs";
import {toggleMark, setBlockType, chainCommands} from 'prosemirror-commands'
import {wrapInList, liftListItem} from './godo-additional-commands.mjs'
import getLanguage from './godo-menus-language.mjs'
class Menu {
constructor(menu, editorView) {
this.items = menu.items;
this.editorView = editorView;
this.name = menu.name;
constructor (menu, editorView) {
this.items = menu.items
this.editorView = editorView
this.name = menu.name
this.el = document.createElement("div");
this.el.className = `godo-${this.name}-menu`;
this.el.wrapper = document.createElement("div");
this.el.wrapper.className = "menuicons";
this.el = document.createElement('div')
this.el.className = `godo-${this.name}-menu`
this.el.wrapper = document.createElement('div')
this.el.wrapper.className = 'menuicons'
for (const item in this.items) {
this.el.wrapper.appendChild(this.items[item].dom);
this.el.wrapper.appendChild(this.items[item].dom)
}
this.el.appendChild(this.el.wrapper);
this.el.appendChild(this.el.wrapper)
this.update(editorView, null);
this.update(editorView, null)
this.el.addEventListener("mousedown", e => {
e.preventDefault();
this.el.addEventListener('mousedown', e => {
e.preventDefault()
for (const item in this.items) {
const {dom, run} = this.items[item];
const {dom, run} = this.items[item]
if (dom.contains(e.target)) {
run(editorView.state, editorView.dispatch, editorView);
run(editorView.state, editorView.dispatch, editorView)
}
}
});
})
}
update( view, lastState ) {
let state = view.state;
update (view, lastState) {
let state = view.state
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
return;
return
}
if (this.name === "marks") {
this.update_marks(view, state);
if (this.name === 'marks') {
this.updateMarks(view, state)
}
if (this.name === "blocks") {
this.update_blocks(view, state);
if (this.name === 'blocks') {
this.udpateBlocks(view, state)
}
}
set_menu_item( menuItem, is_active, is_disabled = true ) {
if (is_active) {
menuItem.classList.add('active');
setMenuItem (menuItem, isActive, isDisabled = true) {
if (isActive) {
menuItem.classList.add('active')
} else {
menuItem.classList.remove('active');
menuItem.classList.remove('active')
}
menuItem.disabled = is_disabled;
menuItem.disabled = isDisabled
}
update_blocks(view, state) {
udpateBlocks (view, state) {
for (const item in this.items) {
const {dom, run, type, active, options} = this.items[item];
const active_default = () => {
let {$from, to} = state.selection;
const {dom, run, type, active, options} = this.items[item]
const activeDefault = () => {
let {$from, to} = state.selection
return to <= $from.end() && $from.parent.hasMarkup(type, options)
};
const is_active = (active) ? active(state) : active_default();
const is_disabled = !run(state, null, view);
this.set_menu_item(dom, is_active, is_disabled);
}
const isActive = (active) ? active(state) : activeDefault()
const isDisabled = !run(state, null, view)
this.setMenuItem(dom, isActive, isDisabled)
}
}
update_marks( view, state ) {
let $sel = state.selection;
const $cursor = $sel.$cursor;
const ranges = $sel.ranges;
let get_is_active;
updateMarks (view, state) {
let $sel = state.selection
const $cursor = $sel.$cursor
const ranges = $sel.ranges
let getIsActive
// if single cursor
if ($cursor && $cursor.marks()) {
const activeMarks = $cursor.marks().map( m => m.type.name);
get_is_active = (type) => activeMarks.includes(type.name);
this.el.classList.add("fade");
const activeMarks = $cursor.marks().map(m => m.type.name)
getIsActive = (type) => activeMarks.includes(type.name)
this.el.classList.add('fade')
// else = select range
} else {
const range = ranges[0];
const range = ranges[0]
if (range && ranges.length === 1){
get_is_active = (type) => state.doc.rangeHasMark(range.$from.pos, range.$to.pos, type);
this.el.classList.remove("fade");
getIsActive = (type) => state.doc.rangeHasMark(range.$from.pos, range.$to.pos, type)
this.el.classList.remove('fade')
}
}
for (const item in this.items) {
const {dom, type, run} = this.items[item];
const is_active = get_is_active(type);
let is_disabled = !run(state, null, view);
this.set_menu_item(dom, is_active, is_disabled);
const {dom, type, run} = this.items[item]
const isActive = getIsActive(type)
let isDisabled = !run(state, null, view)
this.setMenuItem(dom, isActive, isDisabled)
}
// move marks nav above selection
let {from, to} = $sel;
let start = view.coordsAtPos(from);
let end = view.coordsAtPos(to);
let left = Math.max((start.left + end.left) / 2, start.left + 3);
this.el.style.transform = `translate(${left}px, ${end.bottom}px)`;
let {from, to} = $sel
let start = view.coordsAtPos(from)
let end = view.coordsAtPos(to)
let left = Math.max((start.left + end.left) / 2, start.left + 3)
this.el.style.transform = `translate(${left}px, ${end.bottom}px)`
// if (view.hasFocus()) this.el.hidden = false;
this.el.hidden = view.hasFocus();
this.el.hidden = view.hasFocus()
}
destroy() { this.el.remove(); }
destroy () { this.el.remove() }
}
function linkItem( type ) {
function linkItem (type) {
return {
dom: icon("a"),
run( state, dispatch ) {
let chref = prompt("href");
toggleMark(type, {href: chref})(state, dispatch);
dom: icon('a'),
run (state, dispatch) {
let chref = prompt('href')
toggleMark(type, {href: chref})(state, dispatch)
},
type
type,
}
}
function setHeader(type, level, icon_id) {
function setHeader (type, level, iconId) {
return {
run: setBlockType(type, {level}),
dom: icon(icon_id),
run: setBlockType(type, {level}),
dom: icon(iconId),
type,
options: {level}
options: {level},
}
}
// Helper function to create menu icons
// id: defined in languageContent
function icon(id) {
let menuicon = document.createElement("button")
menuicon.className = "menuicon menuicon-" + id;
menuicon.setAttribute("type", "button");
menuicon.title = getLanguage(id).text;
menuicon.textContent = getLanguage(id).icon;
return menuicon;
function icon (id) {
let menuicon = document.createElement('button')
menuicon.className = 'menuicon menuicon-' + id
menuicon.setAttribute('type', 'button')
menuicon.title = getLanguage(id).text
menuicon.textContent = getLanguage(id).icon
return menuicon
}
function blocks( schema, h_levels ) {
function blocks (schema, hLevels) {
let menu = {
name : "blocks",
h_levels
};
let type, i = {};
name: 'blocks',
hLevels,
}
let type; let i = {}
if ((type = schema.nodes.heading)) {
let icon_id = 'h';
h_levels.forEach( (level) => {
i["setHeader"+level] = setHeader(type, level, icon_id);
icon_id = icon_id + "h";
});
let iconId = 'h'
hLevels.forEach((level) => {
i['setHeader' + level] = setHeader(type, level, iconId)
iconId = iconId + 'h'
})
}
if ((type = schema.nodes.paragraph)) {
i.setP = {run: setBlockType(type), dom: icon("p"), type};
i.setP = {run: setBlockType(type), dom: icon('p'), type}
}
if ((type = schema.nodes.bullet_list)) {
i.setList = {
run: chainCommands(wrapInList(type), liftListItem(schema.nodes.list_item)),
dom: icon("ul"),
run: chainCommands(wrapInList(type), liftListItem(schema.nodes.list_item)),
dom: icon('ul'),
type,
active(state) {
active (state) {
let {$from, $to} = state.selection
let range = $from.blockRange($to)
switch (range.parent.type) {
case type:
case schema.nodes.list_item:
return true;
default:
return false;
case type:
case schema.nodes.list_item:
return true
default:
return false
}
}
};
},
}
}
menu.items = i;
return menu;
menu.items = i
return menu
}
function marks( schema ) {
function marks (schema) {
let menu = {
name : "marks",
};
let type, i = {};
name: 'marks',
}
let type; let i = {}
if ((type = schema.marks.strong))
i.toggleStrong = {run: toggleMark(type), dom: icon("b"), type};
if ((type = schema.marks.em))
i.toggleEm = {run: toggleMark(type), dom: icon("i"), type};
if ((type = schema.marks.link))
i.toggleLink = linkItem(type);
if ((type = schema.marks.strong)) i.toggleStrong = {run: toggleMark(type), dom: icon('b'), type}
if ((type = schema.marks.em)) i.toggleEm = {run: toggleMark(type), dom: icon('i'), type}
if ((type = schema.marks.link)) i.toggleLink = linkItem(type)
menu.items = i;
return menu
menu.items = i
return menu
}
export { Menu, blocks, marks };
export { Menu, blocks, marks }

View File

@ -1,4 +1,4 @@
import {Schema} from "prosemirror-model"
import {Schema} from 'prosemirror-model'
// :: Schema
// This schema roughly corresponds to the document schema used by
@ -10,124 +10,125 @@ import {Schema} from "prosemirror-model"
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
// BASIC
const basicSchema = {
nodes : {
const basicSchemaContent = {
nodes: {
// :: NodeSpec The top level document node.
doc: {
content: "block+"
content: 'block+',
},
// :: NodeSpec A plain paragraph textblock. Represented in the DOM
// as a `<p>` element.
paragraph: {
content: "inline*",
group: "block",
parseDOM: [{tag: "p"}],
toDOM() { return ["p", 0] }
content: 'inline*',
group: 'block',
parseDOM: [{tag: 'p'}],
toDOM () { return ['p', 0] },
},
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
hard_break: {
hardBreak: {
inline: true,
group: "inline",
group: 'inline',
selectable: false,
parseDOM: [{tag: "br"}],
toDOM() { return ["br"] }
parseDOM: [{tag: 'br'}],
toDOM () { return ['br'] },
},
// :: NodeSpec The text node.
text: {
group: "inline"
group: 'inline',
},
},
marks : {
marks: {
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`.
em: {
parseDOM: [{tag: "i"}, {tag: "em"}, {style: "font-style=italic"}],
toDOM() { return ["em", 0] }
parseDOM: [{tag: 'i'}, {tag: 'em'}, {style: 'font-style=italic'}],
toDOM () { return ['em', 0] },
},
// :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
// also match `<b>` and `font-weight: bold`.
strong: {
parseDOM: [{tag: "strong"},
// This works around a Google Docs misbehavior where
// pasted content will be inexplicably wrapped in `<b>`
// tags with a font-weight normal.
{tag: "b", getAttrs: node => node.style.fontWeight !== "normal" && null},
{style: "font-weight", getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null}],
toDOM() { return ["strong", 0] }
}
}
parseDOM: [{tag: 'strong'},
// This works around a Google Docs misbehavior where
// pasted content will be inexplicably wrapped in `<b>`
// tags with a font-weight normal.
{tag: 'b', getAttrs: node => node.style.fontWeight !== 'normal' && null},
{style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null}],
toDOM () { return ['strong', 0] },
},
},
}
// HEADERS
const headersSchema = {
const headersSchemaContent = {
nodes: {
// :: NodeSpec A heading textblock, with a `level` attribute that
// should hold the number 1 to 6. Parsed and serialized as `<h1>` to
// `<h6>` elements.
heading: {
attrs: {level: {default: 1}},
content: "inline*",
marks: "em",
group: "block",
content: 'inline*',
marks: 'em',
group: 'block',
defining: true,
parseDOM: [{tag: "h1", attrs: {level: 1}},
{tag: "h2", attrs: {level: 2}},
{tag: "h3", attrs: {level: 3}},
{tag: "h4", attrs: {level: 4}},
{tag: "h5", attrs: {level: 5}},
{tag: "h6", attrs: {level: 6}}],
toDOM(node) { return ["h" + node.attrs.level, 0] }
}
}
parseDOM: [{tag: 'h1', attrs: {level: 1}},
{tag: 'h2', attrs: {level: 2}},
{tag: 'h3', attrs: {level: 3}},
{tag: 'h4', attrs: {level: 4}},
{tag: 'h5', attrs: {level: 5}},
{tag: 'h6', attrs: {level: 6}}],
toDOM (node) { return ['h' + node.attrs.level, 0] },
},
},
}
// LISTS
const listSchema = {
const listSchemaContent = {
nodes: {
// A bullet list node spec, represented in the DOM as `<ul>`.
bullet_list: {
content: "list_item+",
group: "block",
parseDOM: [{tag: "ul"}],
toDOM() { return ["ul", 0] }
bulletList: {
content: 'listItem+',
group: 'block',
parseDOM: [{tag: 'ul'}],
toDOM () { return ['ul', 0] },
},
// A list item (`<li>`) spec.
list_item: {
content: "paragraph+",
parseDOM: [{tag: "li"}],
toDOM() { return ["li", 0] },
defining: true
}
}
listItem: {
content: 'paragraph+',
parseDOM: [{tag: 'li'}],
toDOM () { return ['li', 0] },
defining: true,
},
},
}
const linkSchema = { // eslint-disable-line no-unused-vars
const linkSchemaContent = { // eslint-disable-line no-unused-vars
marks: {
link: {
attrs: {
href: {},
title: {default: null}
title: {default: null},
},
inclusive: false,
parseDOM: [{tag: "a[href]", getAttrs(dom) {
return {href: dom.getAttribute("href"), title: dom.getAttribute("title")}
}}],
toDOM(node) { let {href, title} = node.attrs; return ["a", {href, title}, 0] }
parseDOM: [{tag: 'a[href]',
getAttrs (dom) {
return {href: dom.getAttribute('href'), title: dom.getAttribute('title')}
}}],
toDOM (node) { let {href, title} = node.attrs; return ['a', {href, title}, 0] },
},
}
},
}
const basic_schema = new Schema(basicSchema);
const basicSchema = new Schema(basicSchemaContent)
const headers_schema = new Schema({
nodes: basic_schema.spec.nodes.append(headersSchema.nodes),
marks: basic_schema.spec.marks
});
const headersSchema = new Schema({
nodes: basicSchema.spec.nodes.append(headersSchemaContent.nodes),
marks: basicSchema.spec.marks,
})
const list_schema = new Schema({
nodes: headers_schema.spec.nodes.append(listSchema.nodes),
marks: headers_schema.spec.marks
});
const listSchema = new Schema({
nodes: headersSchema.spec.nodes.append(listSchemaContent.nodes),
marks: headersSchema.spec.marks,
})
const full_schema = list_schema;
const fullSchema = listSchema
export { basic_schema, headers_schema, list_schema, full_schema };
export { basicSchema, headersSchema, listSchema, fullSchema }

View File

@ -1,101 +1,100 @@
import {EditorState, Plugin} from "prosemirror-state";
import {EditorView} from "prosemirror-view";
import {history} from "prosemirror-history";
import {keymap} from "prosemirror-keymap";
import {baseKeymap} from "prosemirror-commands";
import {DOMParser, DOMSerializer} from "prosemirror-model";
import {EditorState, Plugin} from 'prosemirror-state'
import {EditorView} from 'prosemirror-view'
import {history} from 'prosemirror-history'
import {keymap} from 'prosemirror-keymap'
import {baseKeymap} from 'prosemirror-commands'
import {DOMParser, DOMSerializer} from 'prosemirror-model'
import {buildKeymap} from "./godo-additional-keymap.mjs";
import {basic_schema, headers_schema, list_schema, full_schema} from "./godo-schemas.mjs";
import {Menu, blocks, marks} from "./godo-menus.mjs";
import {buildKeymap} from './godo-additional-keymap.mjs'
import {basicSchema, headersSchema, listSchema, fullSchema} from './godo-schemas.mjs'
import {Menu, blocks, marks} from './godo-menus.mjs'
function menuPlugin(menu) {
function menuPlugin (menu) {
return new Plugin({
view(editorView) {
let menuView = new Menu(menu, editorView);
editorView.dom.parentNode.insertBefore(menuView.el, editorView.dom);
return menuView;
}
});
view (editorView) {
let menuView = new Menu(menu, editorView)
editorView.dom.parentNode.insertBefore(menuView.el, editorView.dom)
return menuView
},
})
}
export default class Godo {
constructor (textarea, options) {
this.textarea = textarea;
this.textarea.hidden = true;
this.textarea.style.display = "none";
let displayBlocksMenu = true;
this.textarea = textarea
this.textarea.hidden = true
this.textarea.style.display = 'none'
let displayBlocksMenu = true
this.default_options = {
schema: "full",
heading_levels: [3, 4]
this.defaultOptions = {
schema: 'full',
headingLevels: [3, 4],
}
this.options = Object.assign(this.default_options, options);
this.options.heading_levels = this.options.heading_levels.slice(0, 3);
this.options = Object.assign(this.defaultOptions, options)
this.options.headingLevels = this.options.headingLevels.slice(0, 3)
this.schema = (() => {
switch(this.options.schema) {
case 'basic':
displayBlocksMenu = false;
return basic_schema;
case 'headers':
return headers_schema;
case 'list':
return list_schema
case 'full':
default:
return full_schema
switch (this.options.schema) {
case 'basic':
displayBlocksMenu = false
return basicSchema
case 'headers':
return headersSchema
case 'list':
return listSchema
case 'full':
default:
return fullSchema
}
})();
})()
this.editor_wrapper = document.createElement('div');
this.editor_wrapper.className = "godo";
textarea.insertAdjacentElement('afterend', this.editor_wrapper);
this.editorWrapper = document.createElement('div')
this.editorWrapper.className = 'godo'
textarea.insertAdjacentElement('afterend', this.editorWrapper)
this.marksMenu = menuPlugin(marks(this.schema));
this.blocksMenu = menuPlugin(blocks(this.schema, this.options.heading_levels));
this.marksMenu = menuPlugin(marks(this.schema))
this.blocksMenu = menuPlugin(blocks(this.schema, this.options.headingLevels))
let plugins_list = [
history(),
keymap(buildKeymap(this.schema, null, this.options)),
keymap(baseKeymap),
this.marksMenu
];
let pluginsList = [
history(),
keymap(buildKeymap(this.schema, null, this.options)),
keymap(baseKeymap),
this.marksMenu,
]
if (displayBlocksMenu && !textarea.readOnly) plugins_list.push(this.blocksMenu);
if (displayBlocksMenu && !textarea.readOnly) pluginsList.push(this.blocksMenu)
this.state = EditorState.create({
schema: this.schema,
doc: DOMParser.fromSchema(this.schema).parse(this.get_textarea_content()),
plugins: plugins_list
});
doc: DOMParser.fromSchema(this.schema).parse(this.getTextAreaContent()),
plugins: pluginsList,
})
this.view = new EditorView(this.editor_wrapper, {
this.view = new EditorView(this.editorWrapper, {
state: this.state,
editable: () => !textarea.readOnly,
attributes: {
class: "godo--editor"
}
});
class: 'godo--editor',
},
})
this.textarea.form.addEventListener('submit', () => {
textarea.value = this.getHTML();
});
textarea.value = this.getHTML()
})
}
get_textarea_content() {
const w = document.createElement('div');
w.innerHTML = this.textarea.value;
return w;
getTextAreaContent () {
const w = document.createElement('div')
w.innerHTML = this.textarea.value
return w
}
getHTML() {
const div = document.createElement('div');
getHTML () {
const div = document.createElement('div')
const fragment = DOMSerializer
.fromSchema(this.schema)
.serializeFragment(this.view.state.doc.content);
div.appendChild(fragment);
return div.innerHTML;
.serializeFragment(this.view.state.doc.content)
div.appendChild(fragment)
return div.innerHTML
}
}