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 ecmaVersion: 13
sourceType: module sourceType: module
rules: rules:
# https://standardjs.com/rules.html # Follow Standard JS guidelines : https://standardjs.com/rules.html, except rules
# annotated with a 'custom' comment
# Linting # Linting
array-callback-return: error array-callback-return: error
@ -78,3 +79,51 @@ rules:
no-with: error no-with: error
use-isnan: error use-isnan: error
valid-typeof: 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 buble from '@rollup/plugin-buble'
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { nodeResolve } from '@rollup/plugin-node-resolve'
import { terser } from "rollup-plugin-terser"; import { terser } from 'rollup-plugin-terser'
export default { export default {
input: './src-js/godo.js', input: './src-js/godo.js',
@ -8,14 +8,14 @@ export default {
{ {
file: 'dist/js/godo.js', file: 'dist/js/godo.js',
format: 'es', format: 'es',
sourcemap: true sourcemap: true,
}, },
{ {
file: 'dist/js/godo.min.js', file: 'dist/js/godo.min.js',
format: 'es', format: 'es',
sourcemap: true, sourcemap: true,
plugins: [terser()], plugins: [terser()],
} },
], ],
plugins: [nodeResolve(), buble()] plugins: [nodeResolve(), buble()],
} }

View File

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

View File

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

View File

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

View File

@ -1,201 +1,198 @@
import {toggleMark, setBlockType, chainCommands} from "prosemirror-commands"; import {toggleMark, setBlockType, chainCommands} from 'prosemirror-commands'
import {wrapInList, liftListItem} from "./godo-additional-commands.mjs"; import {wrapInList, liftListItem} from './godo-additional-commands.mjs'
import getLanguage from "./godo-menus-language.mjs"; import getLanguage from './godo-menus-language.mjs'
class Menu { class Menu {
constructor(menu, editorView) { constructor (menu, editorView) {
this.items = menu.items; this.items = menu.items
this.editorView = editorView; this.editorView = editorView
this.name = menu.name; this.name = menu.name
this.el = document.createElement("div"); this.el = document.createElement('div')
this.el.className = `godo-${this.name}-menu`; this.el.className = `godo-${this.name}-menu`
this.el.wrapper = document.createElement("div"); this.el.wrapper = document.createElement('div')
this.el.wrapper.className = "menuicons"; this.el.wrapper.className = 'menuicons'
for (const item in this.items) { 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 => { this.el.addEventListener('mousedown', e => {
e.preventDefault(); e.preventDefault()
for (const item in this.items) { for (const item in this.items) {
const {dom, run} = this.items[item]; const {dom, run} = this.items[item]
if (dom.contains(e.target)) { if (dom.contains(e.target)) {
run(editorView.state, editorView.dispatch, editorView); run(editorView.state, editorView.dispatch, editorView)
} }
} }
}); })
} }
update( view, lastState ) { update (view, lastState) {
let state = view.state; let state = view.state
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) { if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
return; return
} }
if (this.name === "marks") { if (this.name === 'marks') {
this.update_marks(view, state); this.updateMarks(view, state)
} }
if (this.name === "blocks") { if (this.name === 'blocks') {
this.update_blocks(view, state); this.udpateBlocks(view, state)
} }
} }
set_menu_item( menuItem, is_active, is_disabled = true ) { setMenuItem (menuItem, isActive, isDisabled = true) {
if (is_active) { if (isActive) {
menuItem.classList.add('active'); menuItem.classList.add('active')
} else { } 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) { for (const item in this.items) {
const {dom, run, type, active, options} = this.items[item]; const {dom, run, type, active, options} = this.items[item]
const active_default = () => { const activeDefault = () => {
let {$from, to} = state.selection; let {$from, to} = state.selection
return to <= $from.end() && $from.parent.hasMarkup(type, options) return to <= $from.end() && $from.parent.hasMarkup(type, options)
}; }
const is_active = (active) ? active(state) : active_default(); const isActive = (active) ? active(state) : activeDefault()
const is_disabled = !run(state, null, view); const isDisabled = !run(state, null, view)
this.set_menu_item(dom, is_active, is_disabled); this.setMenuItem(dom, isActive, isDisabled)
} }
} }
update_marks( view, state ) { updateMarks (view, state) {
let $sel = state.selection; let $sel = state.selection
const $cursor = $sel.$cursor; const $cursor = $sel.$cursor
const ranges = $sel.ranges; const ranges = $sel.ranges
let get_is_active; let getIsActive
// if single cursor // if single cursor
if ($cursor && $cursor.marks()) { if ($cursor && $cursor.marks()) {
const activeMarks = $cursor.marks().map( m => m.type.name); const activeMarks = $cursor.marks().map(m => m.type.name)
get_is_active = (type) => activeMarks.includes(type.name); getIsActive = (type) => activeMarks.includes(type.name)
this.el.classList.add("fade"); this.el.classList.add('fade')
// else = select range // else = select range
} else { } else {
const range = ranges[0]; const range = ranges[0]
if (range && ranges.length === 1){ if (range && ranges.length === 1){
get_is_active = (type) => state.doc.rangeHasMark(range.$from.pos, range.$to.pos, type); getIsActive = (type) => state.doc.rangeHasMark(range.$from.pos, range.$to.pos, type)
this.el.classList.remove("fade"); this.el.classList.remove('fade')
} }
} }
for (const item in this.items) { for (const item in this.items) {
const {dom, type, run} = this.items[item]; const {dom, type, run} = this.items[item]
const is_active = get_is_active(type); const isActive = getIsActive(type)
let is_disabled = !run(state, null, view); let isDisabled = !run(state, null, view)
this.set_menu_item(dom, is_active, is_disabled); this.setMenuItem(dom, isActive, isDisabled)
} }
// move marks nav above selection // move marks nav above selection
let {from, to} = $sel; let {from, to} = $sel
let start = view.coordsAtPos(from); let start = view.coordsAtPos(from)
let end = view.coordsAtPos(to); let end = view.coordsAtPos(to)
let left = Math.max((start.left + end.left) / 2, start.left + 3); let left = Math.max((start.left + end.left) / 2, start.left + 3)
this.el.style.transform = `translate(${left}px, ${end.bottom}px)`; this.el.style.transform = `translate(${left}px, ${end.bottom}px)`
// if (view.hasFocus()) this.el.hidden = false; // 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 { return {
dom: icon("a"), dom: icon('a'),
run( state, dispatch ) { run (state, dispatch) {
let chref = prompt("href"); let chref = prompt('href')
toggleMark(type, {href: chref})(state, dispatch); toggleMark(type, {href: chref})(state, dispatch)
}, },
type type,
} }
} }
function setHeader(type, level, icon_id) { function setHeader (type, level, iconId) {
return { return {
run: setBlockType(type, {level}), run: setBlockType(type, {level}),
dom: icon(icon_id), dom: icon(iconId),
type, type,
options: {level} options: {level},
} }
} }
// Helper function to create menu icons // Helper function to create menu icons
// id: defined in languageContent // id: defined in languageContent
function icon(id) { function icon (id) {
let menuicon = document.createElement("button") let menuicon = document.createElement('button')
menuicon.className = "menuicon menuicon-" + id; menuicon.className = 'menuicon menuicon-' + id
menuicon.setAttribute("type", "button"); menuicon.setAttribute('type', 'button')
menuicon.title = getLanguage(id).text; menuicon.title = getLanguage(id).text
menuicon.textContent = getLanguage(id).icon; menuicon.textContent = getLanguage(id).icon
return menuicon; return menuicon
} }
function blocks( schema, h_levels ) { function blocks (schema, hLevels) {
let menu = { let menu = {
name : "blocks", name: 'blocks',
h_levels hLevels,
}; }
let type, i = {}; let type; let i = {}
if ((type = schema.nodes.heading)) { if ((type = schema.nodes.heading)) {
let icon_id = 'h'; let iconId = 'h'
h_levels.forEach( (level) => { hLevels.forEach((level) => {
i["setHeader"+level] = setHeader(type, level, icon_id); i['setHeader' + level] = setHeader(type, level, iconId)
icon_id = icon_id + "h"; iconId = iconId + 'h'
}); })
} }
if ((type = schema.nodes.paragraph)) { 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)) { if ((type = schema.nodes.bullet_list)) {
i.setList = { i.setList = {
run: chainCommands(wrapInList(type), liftListItem(schema.nodes.list_item)), run: chainCommands(wrapInList(type), liftListItem(schema.nodes.list_item)),
dom: icon("ul"), dom: icon('ul'),
type, type,
active(state) { active (state) {
let {$from, $to} = state.selection let {$from, $to} = state.selection
let range = $from.blockRange($to) let range = $from.blockRange($to)
switch (range.parent.type) { switch (range.parent.type) {
case type: case type:
case schema.nodes.list_item: case schema.nodes.list_item:
return true; return true
default: default:
return false; return false
} }
} },
}; }
} }
menu.items = i; menu.items = i
return menu; return menu
} }
function marks( schema ) { function marks (schema) {
let menu = { let menu = {
name : "marks", name: 'marks',
}; }
let type, i = {}; let type; let i = {}
if ((type = schema.marks.strong)) if ((type = schema.marks.strong)) i.toggleStrong = {run: toggleMark(type), dom: icon('b'), type}
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.em)) if ((type = schema.marks.link)) i.toggleLink = linkItem(type)
i.toggleEm = {run: toggleMark(type), dom: icon("i"), type};
if ((type = schema.marks.link))
i.toggleLink = linkItem(type);
menu.items = i; menu.items = i
return menu 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 // :: Schema
// This schema roughly corresponds to the document schema used by // 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). // `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
// BASIC // BASIC
const basicSchema = { const basicSchemaContent = {
nodes : { nodes: {
// :: NodeSpec The top level document node. // :: NodeSpec The top level document node.
doc: { doc: {
content: "block+" content: 'block+',
}, },
// :: NodeSpec A plain paragraph textblock. Represented in the DOM // :: NodeSpec A plain paragraph textblock. Represented in the DOM
// as a `<p>` element. // as a `<p>` element.
paragraph: { paragraph: {
content: "inline*", content: 'inline*',
group: "block", group: 'block',
parseDOM: [{tag: "p"}], parseDOM: [{tag: 'p'}],
toDOM() { return ["p", 0] } toDOM () { return ['p', 0] },
}, },
// :: NodeSpec A hard line break, represented in the DOM as `<br>`. // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
hard_break: { hardBreak: {
inline: true, inline: true,
group: "inline", group: 'inline',
selectable: false, selectable: false,
parseDOM: [{tag: "br"}], parseDOM: [{tag: 'br'}],
toDOM() { return ["br"] } toDOM () { return ['br'] },
}, },
// :: NodeSpec The text node. // :: NodeSpec The text node.
text: { text: {
group: "inline" group: 'inline',
}, },
}, },
marks : { marks: {
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element. // :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`. // Has parse rules that also match `<i>` and `font-style: italic`.
em: { em: {
parseDOM: [{tag: "i"}, {tag: "em"}, {style: "font-style=italic"}], parseDOM: [{tag: 'i'}, {tag: 'em'}, {style: 'font-style=italic'}],
toDOM() { return ["em", 0] } toDOM () { return ['em', 0] },
}, },
// :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
// also match `<b>` and `font-weight: bold`. // also match `<b>` and `font-weight: bold`.
strong: { strong: {
parseDOM: [{tag: "strong"}, parseDOM: [{tag: 'strong'},
// This works around a Google Docs misbehavior where // This works around a Google Docs misbehavior where
// pasted content will be inexplicably wrapped in `<b>` // pasted content will be inexplicably wrapped in `<b>`
// tags with a font-weight normal. // tags with a font-weight normal.
{tag: "b", getAttrs: node => node.style.fontWeight !== "normal" && null}, {tag: 'b', getAttrs: node => node.style.fontWeight !== 'normal' && null},
{style: "font-weight", getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null}], {style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null}],
toDOM() { return ["strong", 0] } toDOM () { return ['strong', 0] },
} },
} },
} }
// HEADERS // HEADERS
const headersSchema = { const headersSchemaContent = {
nodes: { nodes: {
// :: NodeSpec A heading textblock, with a `level` attribute that // :: NodeSpec A heading textblock, with a `level` attribute that
// should hold the number 1 to 6. Parsed and serialized as `<h1>` to // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
// `<h6>` elements. // `<h6>` elements.
heading: { heading: {
attrs: {level: {default: 1}}, attrs: {level: {default: 1}},
content: "inline*", content: 'inline*',
marks: "em", marks: 'em',
group: "block", group: 'block',
defining: true, defining: true,
parseDOM: [{tag: "h1", attrs: {level: 1}}, parseDOM: [{tag: 'h1', attrs: {level: 1}},
{tag: "h2", attrs: {level: 2}}, {tag: 'h2', attrs: {level: 2}},
{tag: "h3", attrs: {level: 3}}, {tag: 'h3', attrs: {level: 3}},
{tag: "h4", attrs: {level: 4}}, {tag: 'h4', attrs: {level: 4}},
{tag: "h5", attrs: {level: 5}}, {tag: 'h5', attrs: {level: 5}},
{tag: "h6", attrs: {level: 6}}], {tag: 'h6', attrs: {level: 6}}],
toDOM(node) { return ["h" + node.attrs.level, 0] } toDOM (node) { return ['h' + node.attrs.level, 0] },
} },
} },
} }
// LISTS // LISTS
const listSchema = { const listSchemaContent = {
nodes: { nodes: {
// A bullet list node spec, represented in the DOM as `<ul>`. // A bullet list node spec, represented in the DOM as `<ul>`.
bullet_list: { bulletList: {
content: "list_item+", content: 'listItem+',
group: "block", group: 'block',
parseDOM: [{tag: "ul"}], parseDOM: [{tag: 'ul'}],
toDOM() { return ["ul", 0] } toDOM () { return ['ul', 0] },
}, },
// A list item (`<li>`) spec. // A list item (`<li>`) spec.
list_item: { listItem: {
content: "paragraph+", content: 'paragraph+',
parseDOM: [{tag: "li"}], parseDOM: [{tag: 'li'}],
toDOM() { return ["li", 0] }, toDOM () { return ['li', 0] },
defining: true defining: true,
} },
} },
} }
const linkSchema = { // eslint-disable-line no-unused-vars const linkSchemaContent = { // eslint-disable-line no-unused-vars
marks: { marks: {
link: { link: {
attrs: { attrs: {
href: {}, href: {},
title: {default: null} title: {default: null},
}, },
inclusive: false, inclusive: false,
parseDOM: [{tag: "a[href]", getAttrs(dom) { parseDOM: [{tag: 'a[href]',
return {href: dom.getAttribute("href"), title: dom.getAttribute("title")} getAttrs (dom) {
}}], return {href: dom.getAttribute('href'), title: dom.getAttribute('title')}
toDOM(node) { let {href, title} = node.attrs; return ["a", {href, title}, 0] } }}],
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({ const headersSchema = new Schema({
nodes: basic_schema.spec.nodes.append(headersSchema.nodes), nodes: basicSchema.spec.nodes.append(headersSchemaContent.nodes),
marks: basic_schema.spec.marks marks: basicSchema.spec.marks,
}); })
const list_schema = new Schema({ const listSchema = new Schema({
nodes: headers_schema.spec.nodes.append(listSchema.nodes), nodes: headersSchema.spec.nodes.append(listSchemaContent.nodes),
marks: headers_schema.spec.marks 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 {EditorState, Plugin} from 'prosemirror-state'
import {EditorView} from "prosemirror-view"; import {EditorView} from 'prosemirror-view'
import {history} from "prosemirror-history"; import {history} from 'prosemirror-history'
import {keymap} from "prosemirror-keymap"; import {keymap} from 'prosemirror-keymap'
import {baseKeymap} from "prosemirror-commands"; import {baseKeymap} from 'prosemirror-commands'
import {DOMParser, DOMSerializer} from "prosemirror-model"; import {DOMParser, DOMSerializer} from 'prosemirror-model'
import {buildKeymap} from "./godo-additional-keymap.mjs"; import {buildKeymap} from './godo-additional-keymap.mjs'
import {basic_schema, headers_schema, list_schema, full_schema} from "./godo-schemas.mjs"; import {basicSchema, headersSchema, listSchema, fullSchema} from './godo-schemas.mjs'
import {Menu, blocks, marks} from "./godo-menus.mjs"; import {Menu, blocks, marks} from './godo-menus.mjs'
function menuPlugin(menu) { function menuPlugin (menu) {
return new Plugin({ return new Plugin({
view(editorView) { view (editorView) {
let menuView = new Menu(menu, editorView); let menuView = new Menu(menu, editorView)
editorView.dom.parentNode.insertBefore(menuView.el, editorView.dom); editorView.dom.parentNode.insertBefore(menuView.el, editorView.dom)
return menuView; return menuView
} },
}); })
} }
export default class Godo { export default class Godo {
constructor (textarea, options) { constructor (textarea, options) {
this.textarea = textarea; this.textarea = textarea
this.textarea.hidden = true; this.textarea.hidden = true
this.textarea.style.display = "none"; this.textarea.style.display = 'none'
let displayBlocksMenu = true; let displayBlocksMenu = true
this.default_options = { this.defaultOptions = {
schema: "full", schema: 'full',
heading_levels: [3, 4] headingLevels: [3, 4],
} }
this.options = Object.assign(this.default_options, options); this.options = Object.assign(this.defaultOptions, options)
this.options.heading_levels = this.options.heading_levels.slice(0, 3); this.options.headingLevels = this.options.headingLevels.slice(0, 3)
this.schema = (() => { this.schema = (() => {
switch(this.options.schema) { switch (this.options.schema) {
case 'basic': case 'basic':
displayBlocksMenu = false; displayBlocksMenu = false
return basic_schema; return basicSchema
case 'headers': case 'headers':
return headers_schema; return headersSchema
case 'list': case 'list':
return list_schema return listSchema
case 'full': case 'full':
default: default:
return full_schema return fullSchema
} }
})(); })()
this.editor_wrapper = document.createElement('div'); this.editorWrapper = document.createElement('div')
this.editor_wrapper.className = "godo"; this.editorWrapper.className = 'godo'
textarea.insertAdjacentElement('afterend', this.editor_wrapper); textarea.insertAdjacentElement('afterend', this.editorWrapper)
this.marksMenu = menuPlugin(marks(this.schema)); this.marksMenu = menuPlugin(marks(this.schema))
this.blocksMenu = menuPlugin(blocks(this.schema, this.options.heading_levels)); this.blocksMenu = menuPlugin(blocks(this.schema, this.options.headingLevels))
let plugins_list = [ let pluginsList = [
history(), history(),
keymap(buildKeymap(this.schema, null, this.options)), keymap(buildKeymap(this.schema, null, this.options)),
keymap(baseKeymap), keymap(baseKeymap),
this.marksMenu this.marksMenu,
]; ]
if (displayBlocksMenu && !textarea.readOnly) plugins_list.push(this.blocksMenu); if (displayBlocksMenu && !textarea.readOnly) pluginsList.push(this.blocksMenu)
this.state = EditorState.create({ this.state = EditorState.create({
schema: this.schema, schema: this.schema,
doc: DOMParser.fromSchema(this.schema).parse(this.get_textarea_content()), doc: DOMParser.fromSchema(this.schema).parse(this.getTextAreaContent()),
plugins: plugins_list plugins: pluginsList,
}); })
this.view = new EditorView(this.editor_wrapper, { this.view = new EditorView(this.editorWrapper, {
state: this.state, state: this.state,
editable: () => !textarea.readOnly, editable: () => !textarea.readOnly,
attributes: { attributes: {
class: "godo--editor" class: 'godo--editor',
} },
}); })
this.textarea.form.addEventListener('submit', () => { this.textarea.form.addEventListener('submit', () => {
textarea.value = this.getHTML(); textarea.value = this.getHTML()
}); })
} }
get_textarea_content() { getTextAreaContent () {
const w = document.createElement('div'); const w = document.createElement('div')
w.innerHTML = this.textarea.value; w.innerHTML = this.textarea.value
return w; return w
} }
getHTML() { getHTML () {
const div = document.createElement('div'); const div = document.createElement('div')
const fragment = DOMSerializer const fragment = DOMSerializer
.fromSchema(this.schema) .fromSchema(this.schema)
.serializeFragment(this.view.state.doc.content); .serializeFragment(this.view.state.doc.content)
div.appendChild(fragment); div.appendChild(fragment)
return div.innerHTML; return div.innerHTML
} }
} }