diff --git a/.eslintrc.yml b/.eslintrc.yml index d7aa1db..b38725b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -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}] diff --git a/rollup.config.js b/rollup.config.js index 0a87215..bfbcf63 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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()], } diff --git a/src-js/godo-additional-commands.mjs b/src-js/godo-additional-commands.mjs index 6a4a681..f3448ea 100644 --- a/src-js/godo-additional-commands.mjs +++ b/src-js/godo-additional-commands.mjs @@ -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 } diff --git a/src-js/godo-additional-keymap.mjs b/src-js/godo-additional-keymap.mjs index c44d4b3..56d5224 100644 --- a/src-js/godo-additional-keymap.mjs +++ b/src-js/godo-additional-keymap.mjs @@ -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 }) diff --git a/src-js/godo-menus-language.mjs b/src-js/godo-menus-language.mjs index 08074c5..1696f05 100644 --- a/src-js/godo-menus-language.mjs +++ b/src-js/godo-menus-language.mjs @@ -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] } diff --git a/src-js/godo-menus.mjs b/src-js/godo-menus.mjs index bf1e310..0b9ec02 100644 --- a/src-js/godo-menus.mjs +++ b/src-js/godo-menus.mjs @@ -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 } diff --git a/src-js/godo-schemas.mjs b/src-js/godo-schemas.mjs index b8e192f..2238d9f 100644 --- a/src-js/godo-schemas.mjs +++ b/src-js/godo-schemas.mjs @@ -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 `

` 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 `
`. - 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 `` element. // Has parse rules that also match `` 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 ``, parse rules // also match `` and `font-weight: bold`. strong: { - parseDOM: [{tag: "strong"}, - // This works around a Google Docs misbehavior where - // pasted content will be inexplicably wrapped in `` - // 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 `` + // 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 `

` to // `
` 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 `
    `. - 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 (`
  • `) 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 } diff --git a/src-js/godo.js b/src-js/godo.js index 41cd4e5..d6901ae 100644 --- a/src-js/godo.js +++ b/src-js/godo.js @@ -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 } }