├── .stylelintignore ├── _config.yml ├── .gitattribute ├── src ├── ui │ ├── czi-frag.css │ ├── isOffline.js │ ├── uuid.js │ ├── czi-custom-menu.css │ ├── czi-bookmark-view.css │ ├── Frag.js │ ├── isReactClass.js │ ├── CustomMenu.js │ ├── KeyCodes.js │ ├── icon-font.css │ ├── htmlElementToRect.js │ ├── czi-table-cell-menu.css │ ├── listType.css │ ├── czi-body-layout-editor.css │ ├── czi-inline-editor.css │ ├── czi-custom-menu-button.css │ ├── czi-custom-scrollbar.css │ ├── isElementFullyVisible.js │ ├── toHexColor.js │ ├── LoadingIndicator.js │ ├── czi-heading.css │ ├── czi-selection-placeholder.css │ ├── handleEditorDrop.js │ ├── czi-custom-menu-item.css │ ├── CustomEditorView.js │ ├── handleEditorPaste.js │ ├── czi-table-grid-size-editor.css │ ├── czi-image-url-editor.css │ ├── czi-cursor-placeholder.css │ ├── handleEditorKeyDown.js │ ├── CustomMenuItem.js │ ├── czi-image-upload-placeholder.css │ ├── findActiveFontType.js │ ├── TableNodeView.js │ ├── PasteMenu.js │ ├── bindScrollHandler.js │ ├── injectStyleSheet.js │ ├── toCSSColor.js │ ├── czi-math-view.css │ ├── czi-loading-indicator.css │ ├── TableCellMenu.js │ ├── czi-image-upload-editor.css │ ├── canUseCSSFont.js │ ├── LinkTooltip.js │ ├── toCSSLineSpacing.js │ ├── ImageInlineEditor.js │ ├── AlertInfo.js │ ├── FontTypeCommandMenuButton.js │ ├── findActiveFontSize.js │ ├── CustomRadioButton.js │ ├── czi-icon.css │ ├── FontSizeCommandMenuButton.js │ ├── czi-editor-frameset.css │ ├── czi-custom-radio-button.css │ ├── czi-vars.css │ ├── BookmarkNodeView.js │ ├── CommandButton.js │ ├── czi-table.css │ ├── EditorFrameset.js │ ├── ListTypeMenu.js │ └── ListTypeCommandButton.js ├── fonts │ ├── acme │ │ └── Acme.woff2 │ ├── times │ │ ├── Times-L.woff2 │ │ └── Times-LE.woff2 │ ├── tahoma │ │ ├── Tahoma-C.woff2 │ │ ├── Tahoma-G.woff2 │ │ ├── Tahoma-L.woff2 │ │ ├── Tahoma-CE.woff2 │ │ ├── Tahoma-GE.woff2 │ │ └── Tahoma-LE.woff2 │ ├── aclonica │ │ └── Aclonica.woff2 │ ├── georgia │ │ ├── Georgia-C.woff2 │ │ ├── Georgia-CE.woff2 │ │ ├── Georgia-G.woff2 │ │ ├── Georgia-GE.woff2 │ │ ├── Georgia-L.woff2 │ │ └── Georgia-LE.woff2 │ ├── verdana │ │ ├── Verdana-C.woff2 │ │ ├── Verdana-CE.woff2 │ │ ├── Verdana-G.woff2 │ │ ├── Verdana-GE.woff2 │ │ ├── Verdana-L.woff2 │ │ ├── Verdana-LE.woff2 │ │ └── Verdana-V.woff2 │ ├── arial-black │ │ ├── ArialBlack-G.woff2 │ │ ├── ArialBlack-L.woff2 │ │ └── ArialBlack-LE.woff2 │ ├── courier-new │ │ └── CourierNew-L.woff2 │ ├── alegreya │ │ ├── Alegreya-Regular-C.woff2 │ │ ├── Alegreya-Regular-CE.woff2 │ │ ├── Alegreya-Regular-G.woff2 │ │ ├── Alegreya-Regular-GE.woff2 │ │ ├── Alegreya-Regular-L.woff2 │ │ ├── Alegreya-Regular-LE.woff2 │ │ └── Alegreya-Regular-V.woff2 │ ├── times-new-roman │ │ ├── TimesNewRoman-C.woff2 │ │ ├── TimesNewRoman-CE.woff2 │ │ ├── TimesNewRoman-G.woff2 │ │ ├── TimesNewRoman-GE.woff2 │ │ ├── TimesNewRoman-L.woff2 │ │ ├── TimesNewRoman-LE.woff2 │ │ └── TimesNewRoman-V.woff2 │ └── material-icons │ │ └── MaterialIcons-Regular.ttf ├── browser.js ├── TextNodeSpec.js ├── EditorState.js ├── client │ ├── licit.css │ ├── throttle.js │ ├── Reporter.js │ ├── http.js │ └── SimpleConnector.js ├── convertToJSON.js ├── uuid.js ├── EditorPlugins.js ├── CodeMarkSpec.js ├── sanitizeURL.js ├── HardBreakNodeSpec.js ├── lookUpElement.js ├── EditorSchema.js ├── TextNoWrapMarkSpec.js ├── nodeAt.js ├── TablePlugins.js ├── isTableNode.js ├── toSafeHTMLDocument.js ├── hyphenize.js ├── StyleView.js ├── index.js ├── convertFromHTML.js ├── WebFontLoader.js ├── TextSelectionMarkSpec.js ├── patchParagraphElements.js ├── convertToCSSPTValue.js ├── CodeBlockNodeSpec.js ├── TextUnderlineMarkSpec.js ├── TextSuperMarkSpec.js ├── StrikeMarkSpec.js ├── findActiveMark.js ├── TextSubMarkSpec.js ├── createEmptyEditorState.js ├── joinDown.js ├── patchBreakElements.js ├── BlockquoteNodeSpec.js ├── HangingIndentMarkSpec.js ├── MarkNames.js ├── NodeNames.js ├── HorizontalRuleNodeSpec.js ├── toClosestFontPtSize.js ├── isEditorStateEmpty.js ├── EMMarkSpec.js ├── StrongMarkSpec.js ├── blockQuoteInputRule.js ├── BookmarkNodeSpec.js ├── LinkMarkSpec.js ├── TextColorMarkSpec.js ├── convertFromDOMElement.js ├── joinUp.js ├── rebaseDocWithSteps.js ├── SpacerMarkSpec.js ├── HistoryRedoCommand.js ├── HistoryUndoCommand.js ├── HeadingNodeSpec.js ├── PrintCommand.js ├── patchAnchorElements.js ├── TextHighlightMarkSpec.js ├── ListSplitCommand.js ├── convertFromJSON.js ├── joinListNode.js ├── ListItemNodeSpec.js ├── insertTable.js ├── FontSizeMarkSpec.js ├── BlockquoteToggleCommand.js ├── createCommand.js ├── BulletListNodeSpec.js ├── styles.css ├── OverrideMarkSpec.js ├── DocNodeSpec.js ├── HTMLMutator.js ├── Types.js ├── MarksClearCommand.js ├── EditorNodes.js ├── CodeBlockCommand.js ├── HorizontalRuleCommand.js ├── BlockquoteInsertNewLineCommand.js ├── buildEditorPlugins.js ├── patchMathElements.js ├── EditorPageLayoutPlugin.js ├── findActionableCell.js ├── TableCellColorCommand.js └── ListItemInsertNewLineCommand.js ├── lint.sh ├── .prettierignore ├── flow-typed ├── docs-editor.js ├── draft-js.js ├── draft-convert.js ├── uuid.js ├── flatted.js ├── create-emotion.js ├── prosemirror-collab.js ├── prosemirror-keymap.js ├── prosemirror-model.js ├── prosemirror-state.js ├── prosemirror-tables.js ├── prosemirror-utils.js ├── prosemirror-view.js ├── prosemirror-commands.js ├── prosemirror-history.js ├── prosemirror-dropcursor.js ├── prosemirror-gapcursor.js ├── prosemirror-inputrules.js ├── prosemirror-transform.js ├── resize-observer-polyfill.js ├── @modusoperandilicit-customstyles.js ├── @modusoperandilicit-ui-commands.js ├── @modusoperandilicit-doc-attrs-step.js └── katex.js ├── .dockerignore ├── .prettierrc ├── jest.setup.js ├── .gitignore ├── licit ├── index.html └── server │ └── collab │ ├── start.js │ └── route.js ├── utils ├── build_bin.js ├── env.js └── build_web_server.js ├── scripts ├── env.js ├── build_bin.js ├── ci_check_dist.sh └── webserver.js ├── .github ├── release.yml ├── dependabot.yml └── workflows │ ├── dependencycheck.yml │ ├── version-bump.yml │ └── publish.yml ├── .stylelintrc.json ├── sonar-project.properties ├── .travis.yml ├── run_image_server.py ├── run_collab_server.py ├── run_web_server.py ├── tsconfig.json ├── style-service.Dockerfile ├── .flowconfig ├── LICENSE ├── .babelrc ├── babel.config.json ├── CODEOWNERS └── eslint.config.mjs /.stylelintignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /dist -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /.gitattribute: -------------------------------------------------------------------------------- 1 | dist/** linguist-generated=true -------------------------------------------------------------------------------- /src/ui/czi-frag.css: -------------------------------------------------------------------------------- 1 | .czi-frag { 2 | display: contents; 3 | } 4 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node node_modules/eslint/bin/eslint.js --fix src/ 4 | 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /package.json 2 | /bin 3 | /dist 4 | /.eslintrc 5 | /.vscode 6 | /.stylelintrc.json -------------------------------------------------------------------------------- /flow-typed/docs-editor.js: -------------------------------------------------------------------------------- 1 | declare module 'czi-rte' { 2 | declare module.exports: any; 3 | } 4 | -------------------------------------------------------------------------------- /flow-typed/draft-js.js: -------------------------------------------------------------------------------- 1 | declare module 'draft-js' { 2 | declare module.exports: any; 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in the project 2 | ** 3 | # Except servers folder 4 | !servers 5 | -------------------------------------------------------------------------------- /src/fonts/acme/Acme.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/acme/Acme.woff2 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } -------------------------------------------------------------------------------- /flow-typed/draft-convert.js: -------------------------------------------------------------------------------- 1 | declare module 'draft-convert' { 2 | declare module.exports: any; 3 | } 4 | -------------------------------------------------------------------------------- /flow-typed/uuid.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'uuid' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/fonts/times/Times-L.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times/Times-L.woff2 -------------------------------------------------------------------------------- /flow-typed/flatted.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'flatted' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/fonts/tahoma/Tahoma-C.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/tahoma/Tahoma-C.woff2 -------------------------------------------------------------------------------- /src/fonts/tahoma/Tahoma-G.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/tahoma/Tahoma-G.woff2 -------------------------------------------------------------------------------- /src/fonts/tahoma/Tahoma-L.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/tahoma/Tahoma-L.woff2 -------------------------------------------------------------------------------- /src/fonts/times/Times-LE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times/Times-LE.woff2 -------------------------------------------------------------------------------- /src/fonts/aclonica/Aclonica.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/aclonica/Aclonica.woff2 -------------------------------------------------------------------------------- /src/fonts/georgia/Georgia-C.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/georgia/Georgia-C.woff2 -------------------------------------------------------------------------------- /src/fonts/georgia/Georgia-CE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/georgia/Georgia-CE.woff2 -------------------------------------------------------------------------------- /src/fonts/georgia/Georgia-G.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/georgia/Georgia-G.woff2 -------------------------------------------------------------------------------- /src/fonts/georgia/Georgia-GE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/georgia/Georgia-GE.woff2 -------------------------------------------------------------------------------- /src/fonts/georgia/Georgia-L.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/georgia/Georgia-L.woff2 -------------------------------------------------------------------------------- /src/fonts/georgia/Georgia-LE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/georgia/Georgia-LE.woff2 -------------------------------------------------------------------------------- /src/fonts/tahoma/Tahoma-CE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/tahoma/Tahoma-CE.woff2 -------------------------------------------------------------------------------- /src/fonts/tahoma/Tahoma-GE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/tahoma/Tahoma-GE.woff2 -------------------------------------------------------------------------------- /src/fonts/tahoma/Tahoma-LE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/tahoma/Tahoma-LE.woff2 -------------------------------------------------------------------------------- /src/fonts/verdana/Verdana-C.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/verdana/Verdana-C.woff2 -------------------------------------------------------------------------------- /src/fonts/verdana/Verdana-CE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/verdana/Verdana-CE.woff2 -------------------------------------------------------------------------------- /src/fonts/verdana/Verdana-G.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/verdana/Verdana-G.woff2 -------------------------------------------------------------------------------- /src/fonts/verdana/Verdana-GE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/verdana/Verdana-GE.woff2 -------------------------------------------------------------------------------- /src/fonts/verdana/Verdana-L.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/verdana/Verdana-L.woff2 -------------------------------------------------------------------------------- /src/fonts/verdana/Verdana-LE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/verdana/Verdana-LE.woff2 -------------------------------------------------------------------------------- /src/fonts/verdana/Verdana-V.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/verdana/Verdana-V.woff2 -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const browser = { 4 | isMac: () => true, 5 | }; 6 | 7 | export default browser; 8 | -------------------------------------------------------------------------------- /flow-typed/create-emotion.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'create-emotion' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-collab.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-collab' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-keymap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-keymap' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-model.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-model' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-state.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-state' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-tables.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-tables' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-utils' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-view.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-view' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/TextNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const TextNodeSpec = { 4 | group: 'inline', 5 | }; 6 | 7 | export default TextNodeSpec; 8 | -------------------------------------------------------------------------------- /src/fonts/arial-black/ArialBlack-G.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/arial-black/ArialBlack-G.woff2 -------------------------------------------------------------------------------- /src/fonts/arial-black/ArialBlack-L.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/arial-black/ArialBlack-L.woff2 -------------------------------------------------------------------------------- /src/fonts/arial-black/ArialBlack-LE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/arial-black/ArialBlack-LE.woff2 -------------------------------------------------------------------------------- /src/fonts/courier-new/CourierNew-L.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/courier-new/CourierNew-L.woff2 -------------------------------------------------------------------------------- /flow-typed/prosemirror-commands.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-commands' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-history.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-history' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/fonts/alegreya/Alegreya-Regular-C.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/alegreya/Alegreya-Regular-C.woff2 -------------------------------------------------------------------------------- /src/fonts/alegreya/Alegreya-Regular-CE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/alegreya/Alegreya-Regular-CE.woff2 -------------------------------------------------------------------------------- /src/fonts/alegreya/Alegreya-Regular-G.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/alegreya/Alegreya-Regular-G.woff2 -------------------------------------------------------------------------------- /src/fonts/alegreya/Alegreya-Regular-GE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/alegreya/Alegreya-Regular-GE.woff2 -------------------------------------------------------------------------------- /src/fonts/alegreya/Alegreya-Regular-L.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/alegreya/Alegreya-Regular-L.woff2 -------------------------------------------------------------------------------- /src/fonts/alegreya/Alegreya-Regular-LE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/alegreya/Alegreya-Regular-LE.woff2 -------------------------------------------------------------------------------- /src/fonts/alegreya/Alegreya-Regular-V.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/alegreya/Alegreya-Regular-V.woff2 -------------------------------------------------------------------------------- /flow-typed/prosemirror-dropcursor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-dropcursor' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-gapcursor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-gapcursor' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-inputrules.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-inputrules' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/prosemirror-transform.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'prosemirror-transform' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/resize-observer-polyfill.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'resize-observer-polyfill' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/fonts/times-new-roman/TimesNewRoman-C.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times-new-roman/TimesNewRoman-C.woff2 -------------------------------------------------------------------------------- /src/fonts/times-new-roman/TimesNewRoman-CE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times-new-roman/TimesNewRoman-CE.woff2 -------------------------------------------------------------------------------- /src/fonts/times-new-roman/TimesNewRoman-G.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times-new-roman/TimesNewRoman-G.woff2 -------------------------------------------------------------------------------- /src/fonts/times-new-roman/TimesNewRoman-GE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times-new-roman/TimesNewRoman-GE.woff2 -------------------------------------------------------------------------------- /src/fonts/times-new-roman/TimesNewRoman-L.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times-new-roman/TimesNewRoman-L.woff2 -------------------------------------------------------------------------------- /src/fonts/times-new-roman/TimesNewRoman-LE.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times-new-roman/TimesNewRoman-LE.woff2 -------------------------------------------------------------------------------- /src/fonts/times-new-roman/TimesNewRoman-V.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/times-new-roman/TimesNewRoman-V.woff2 -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // needed to mock this due to execute during loading 2 | document.execCommand = document.execCommand || function execCommandMock() {}; 3 | -------------------------------------------------------------------------------- /src/fonts/material-icons/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MO-Movia/licit/HEAD/src/fonts/material-icons/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /flow-typed/@modusoperandilicit-customstyles.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module '@modusoperandi/licit-customstyles' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/@modusoperandilicit-ui-commands.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module '@modusoperandi/licit-ui-commands' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bin 3 | node_modules 4 | servers 5 | *.pyc 6 | *.code-workspace 7 | .vscode/ 8 | dist 9 | src/coverage 10 | /coverage 11 | *.tgz 12 | -------------------------------------------------------------------------------- /flow-typed/@modusoperandilicit-doc-attrs-step.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module '@modusoperandi/licit-doc-attrs-step' { 4 | declare module.exports: any; 5 | } 6 | -------------------------------------------------------------------------------- /flow-typed/katex.js: -------------------------------------------------------------------------------- 1 | declare module 'katex' { 2 | declare module.exports: any; 3 | } 4 | 5 | declare module 'katex/dist/katex.min.css' { 6 | declare module.exports: any; 7 | } -------------------------------------------------------------------------------- /src/EditorState.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | 5 | const ProseMirrorEditorState = EditorState; 6 | 7 | export default ProseMirrorEditorState; 8 | -------------------------------------------------------------------------------- /licit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LICIT 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/client/licit.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: #fff; 3 | border: 0; 4 | border: none; 5 | margin: 0; 6 | } 7 | 8 | body { 9 | background: #fff; 10 | border: none; 11 | margin: 0; 12 | } 13 | -------------------------------------------------------------------------------- /src/ui/isOffline.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default function isOffline(): boolean { 4 | if (window.navigator.hasOwnProperty('onLine')) { 5 | return !window.navigator.onLine; 6 | } 7 | return false; 8 | } 9 | -------------------------------------------------------------------------------- /src/convertToJSON.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | 5 | export default function convertToJSON(editorState: EditorState): Object { 6 | return editorState.doc.toJSON(); 7 | } 8 | -------------------------------------------------------------------------------- /src/uuid.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // [FS] IRAD-1005 2020-07-07 4 | // Upgrade outdated packages. 5 | import { v1 as uuidv1 } from 'uuid'; 6 | 7 | export default function uuid(): string { 8 | return uuidv1(); 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/uuid.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // [FS] IRAD-1005 2020-07-07 4 | // Upgrade outdated packages. 5 | import { v1 as uuidv1 } from 'uuid'; 6 | 7 | export default function uuid(): string { 8 | return uuidv1(); 9 | } 10 | -------------------------------------------------------------------------------- /utils/build_bin.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import config from '../webpack.config.js'; 3 | 4 | delete config.chromeExtensionBoilerplate; 5 | 6 | webpack( 7 | config, 8 | function (err) { if (err) throw err; } 9 | ); 10 | -------------------------------------------------------------------------------- /src/ui/czi-custom-menu.css: -------------------------------------------------------------------------------- 1 | .czi-custom-menu { 2 | background: #fff; 3 | box-shadow: var(--czi-overlay-shadow); 4 | font-family: var(--czi-font-family); 5 | font-size: var(--czi-font-size); 6 | max-height: 98vh; 7 | overflow: auto; 8 | } 9 | -------------------------------------------------------------------------------- /scripts/env.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | // tiny wrapper with default env vars 3 | 4 | const NODE_ENV = process.env.NODE_ENV || 'development'; 5 | const PORT = process.env.PORT || 3001; 6 | 7 | export default { 8 | NODE_ENV, 9 | PORT, 10 | }; 11 | -------------------------------------------------------------------------------- /scripts/build_bin.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | 3 | import webpack from 'webpack'; 4 | import config from '../webpack.config'; 5 | 6 | delete config.chromeExtensionBoilerplate; 7 | 8 | webpack(config, function(err) { 9 | if (err) throw err; 10 | }); 11 | -------------------------------------------------------------------------------- /src/ui/czi-bookmark-view.css: -------------------------------------------------------------------------------- 1 | .czi-bookmark-view { 2 | color: #5e8adb; 3 | display: inline-block; 4 | margin-bottom: 0; 5 | margin-left: 4px; 6 | margin-right: 4px; 7 | margin-top: 0; 8 | vertical-align: middle; 9 | width: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /src/EditorPlugins.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import EditorSchema from './EditorSchema.js'; 4 | import DefaultEditorPlugins from './buildEditorPlugins.js'; 5 | 6 | // Plugin 7 | const EditorPlugins = new DefaultEditorPlugins(EditorSchema); 8 | export default EditorPlugins; 9 | -------------------------------------------------------------------------------- /src/ui/Frag.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react'; 4 | 5 | class Frag extends React.PureComponent { 6 | render(): React.Element { 7 | return
{this.props.children}
; 8 | } 9 | } 10 | 11 | export default Frag; 12 | -------------------------------------------------------------------------------- /utils/env.js: -------------------------------------------------------------------------------- 1 | // tiny wrapper with default env vars 2 | 3 | const NODE_ENV = process.env.NODE_ENV || 'production'; 4 | const PORT = process.env.PORT || '3001'; 5 | const IP = process.env.IP || '127.0.0.1'; 6 | 7 | export default { 8 | NODE_ENV, 9 | PORT, 10 | IP 11 | }; 12 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - dependabot[bot] 7 | categories: 8 | - title: Updates 9 | labels: 10 | - '*' 11 | exclude: 12 | labels: 13 | - dependencies -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-prettier", 3 | "rules": { 4 | "selector-type-no-unknown": [ 5 | true, 6 | { 7 | "ignoreTypes": [ 8 | "nobr" 9 | ] 10 | } 11 | ], 12 | "no-descending-specificity": null 13 | } 14 | } -------------------------------------------------------------------------------- /src/CodeMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { MarkSpec } from './Types.js'; 4 | 5 | const CODE_DOM = ['code', 0]; 6 | 7 | const CodeMarkSpec: MarkSpec = { 8 | parseDOM: [{ tag: 'code' }], 9 | toDOM() { 10 | return CODE_DOM; 11 | }, 12 | }; 13 | 14 | export default CodeMarkSpec; 15 | -------------------------------------------------------------------------------- /src/sanitizeURL.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const HTTP_PREFIX = /^http(s?):*\/\//i; 4 | 5 | export default function sanitizeURL(url: ?string): string { 6 | if (!url) { 7 | return 'http://'; 8 | } 9 | if (HTTP_PREFIX.test(url)) { 10 | return url; 11 | } 12 | return 'https://' + url; 13 | } 14 | -------------------------------------------------------------------------------- /src/HardBreakNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const BR_DOM = ['br']; 4 | 5 | const HardBreakNodeSpec = { 6 | inline: true, 7 | group: 'inline', 8 | selectable: false, 9 | parseDOM: [{ tag: 'br' }], 10 | toDOM() { 11 | return BR_DOM; 12 | }, 13 | }; 14 | 15 | export default HardBreakNodeSpec; 16 | -------------------------------------------------------------------------------- /src/ui/isReactClass.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default function isReactClass(maybe: any): boolean { 4 | if (typeof maybe !== 'function') { 5 | return false; 6 | } 7 | const proto = maybe.prototype; 8 | if (!proto) { 9 | return false; 10 | } 11 | return !!proto.isReactComponent; 12 | } 13 | -------------------------------------------------------------------------------- /src/lookUpElement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default function lookUpElement( 4 | el: ?Element, 5 | predict: (el: Element) => boolean 6 | ): ?Element { 7 | while (el?.nodeName) { 8 | if (predict(el)) { 9 | return el; 10 | } 11 | el = el.parentElement; 12 | } 13 | return null; 14 | } 15 | -------------------------------------------------------------------------------- /src/EditorSchema.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Schema } from 'prosemirror-model'; 4 | 5 | import EditorMarks from './EditorMarks.js'; 6 | import EditorNodes from './EditorNodes.js'; 7 | 8 | const EditorSchema = new Schema({ 9 | nodes: EditorNodes, 10 | marks: EditorMarks, 11 | }); 12 | export default EditorSchema; 13 | -------------------------------------------------------------------------------- /src/TextNoWrapMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { MarkSpec } from './Types.js'; 4 | 5 | const NO_WRAP_DOM = ['nobr', 0]; 6 | 7 | const TextNoWrapMarkSpec: MarkSpec = { 8 | parseDOM: [{ tag: 'nobr' }], 9 | toDOM() { 10 | return NO_WRAP_DOM; 11 | }, 12 | }; 13 | 14 | export default TextNoWrapMarkSpec; 15 | -------------------------------------------------------------------------------- /src/nodeAt.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | export default function nodeAt(doc: Node, pos: number): ?Node { 6 | if (pos < 0 || pos > doc.content.size) { 7 | // Exit here or error will be thrown: 8 | // e.g. RangeError: Position outside of fragment. 9 | return null; 10 | } 11 | return doc.nodeAt(pos); 12 | } 13 | -------------------------------------------------------------------------------- /scripts/ci_check_dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build:dist > /dev/null 4 | 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | NO_COLOR='\033[0m' 8 | if git diff --quiet --exit-code dist/; then 9 | echo -e "${GREEN}dist/ check passed${NO_COLOR}" 10 | exit 0 11 | else 12 | echo -e "${RED}dist/ check failed${NO_COLOR} - run \"npm run build:dist\"" 13 | exit 1 14 | fi -------------------------------------------------------------------------------- /src/ui/CustomMenu.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react'; 4 | 5 | class CustomMenu extends React.Component { 6 | render(): React.Element { 7 | const { children } = this.props; 8 | return ( 9 |
{children}
10 | ); 11 | } 12 | } 13 | 14 | export default CustomMenu; 15 | -------------------------------------------------------------------------------- /src/ui/KeyCodes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes 4 | 5 | export const BACKSPACE = 8; 6 | export const DELETE = 46; 7 | export const DOWN_ARROW = 40; 8 | export const ENTER = 13; 9 | export const LEFT_ARROW = 37; 10 | export const RIGHT_ARROW = 39; 11 | export const TAB = 9; 12 | export const UP_ARROW = 38; 13 | -------------------------------------------------------------------------------- /src/ui/icon-font.css: -------------------------------------------------------------------------------- 1 | /* [FS] IRAD-1061 2020-09-19 2 | Now loaded locally, so that it work in closed network as well. */ 3 | @font-face { 4 | font-family: 'Material Icons'; 5 | font-style: normal; 6 | font-weight: 400; 7 | src: local('Material Icons'), local('MaterialIcons-Regular'), 8 | url('../fonts/material-icons/MaterialIcons-Regular.ttf') format('truetype'); 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/htmlElementToRect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type Rect = { 4 | h: number, 5 | w: number, 6 | x: number, 7 | y: number, 8 | }; 9 | 10 | export default function htmlElementToRect(el: HTMLElement): Rect { 11 | const rect = el.getBoundingClientRect(); 12 | return { 13 | x: rect.left, 14 | y: rect.top, 15 | w: rect.width, 16 | h: rect.height, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /licit/server/collab/start.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createServer} from 'http'; 4 | import handleCollabRequest from './server'; 5 | 6 | const PORT = process.env.PORT || 3002; 7 | 8 | // The collaborative editing document server. 9 | createServer((req, resp) => { 10 | handleCollabRequest(req, resp); 11 | }).listen(PORT); 12 | 13 | console.log('Licit Collab v0.13.17 server listening on ' + PORT); 14 | -------------------------------------------------------------------------------- /src/ui/czi-table-cell-menu.css: -------------------------------------------------------------------------------- 1 | .czi-table-cell-menu.use-icon { 2 | position: fixed; 3 | margin-left: -26px; 4 | margin-top: 5px; 5 | box-shadow: 0 0 0 0.3px rgba(0, 0, 0, 0.35); 6 | font-size: 12px; 7 | font-weight: normal; 8 | height: 20px; 9 | line-height: 22px; 10 | text-align: center; 11 | width: 20px; 12 | } 13 | 14 | .czi-table-cell-menu .czi-icon { 15 | font-size: 12px; 16 | } 17 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=licit 2 | sonar.sourceEncoding=UTF-8 3 | # only analyzing actual library source 4 | sonar.sources=src 5 | sonar.tests=src 6 | sonar.test.inclusions=**/*.test.ts,**/*.test.tsx 7 | # exclude unit tests from coverage and analysis 8 | sonar.exclusions=**/*.test.ts,**/*.test.tsx 9 | # tell sonar where to find coverage results 10 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 11 | -------------------------------------------------------------------------------- /src/ui/listType.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background: #fffefe; 3 | border: 1px solid #bdb5b5; 4 | display: grid; 5 | grid-gap: 10px; 6 | grid-template-columns: auto auto auto; 7 | padding: 5px; 8 | } 9 | 10 | .buttonsize { 11 | height: 50px; 12 | width: 50px; 13 | } 14 | 15 | /* [FS] IRAD-1076 2020-10-15 16 | Style for paste popup menu */ 17 | 18 | .pastemenu { 19 | height: 35px; 20 | width: 72px; 21 | } 22 | -------------------------------------------------------------------------------- /src/TablePlugins.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { tableEditing } from 'prosemirror-tables'; 4 | 5 | import TableCellMenuPlugin from './TableCellMenuPlugin.js'; 6 | import TableResizePlugin from './TableResizePlugin.js'; 7 | 8 | // Tables 9 | // https://github.com/ProseMirror/prosemirror-tables/blob/master/demo.js 10 | export default [ 11 | new TableCellMenuPlugin(), 12 | new TableResizePlugin(), 13 | tableEditing(), 14 | ]; 15 | -------------------------------------------------------------------------------- /src/ui/czi-body-layout-editor.css: -------------------------------------------------------------------------------- 1 | @import './czi-vars.css'; 2 | 3 | .czi-body-layout-editor { 4 | background: #fff; 5 | box-shadow: var(--czi-overlay-shadow); 6 | font-family: var(--czi-font-family); 7 | font-size: var(--czi-font-size); 8 | padding-bottom: 4px; 9 | padding-left: 10px; 10 | padding-right: 10px; 11 | padding-top: 4px; 12 | } 13 | 14 | .czi-body-layout-editor .czi-form { 15 | width: 300px; 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/czi-inline-editor.css: -------------------------------------------------------------------------------- 1 | @import './czi-vars.css'; 2 | 3 | .czi-inline-editor { 4 | background: #fff; 5 | box-shadow: var(--czi-overlay-shadow); 6 | display: flex; 7 | flex-direction: row; 8 | margin-top: 6px; 9 | padding: 4px 10px; 10 | } 11 | 12 | .czi-inline-editor .czi-custom-button { 13 | border: none; 14 | } 15 | 16 | @media only print { 17 | .czi-inline-editor { 18 | display: none; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/isTableNode.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | import { TABLE, TABLE_CELL, TABLE_HEADER, TABLE_ROW } from './NodeNames'; 6 | 7 | export default function isTableNode(node: Node): boolean { 8 | const name = node instanceof Node ? node.type.name : null; 9 | return ( 10 | name === TABLE || 11 | name === TABLE_ROW || 12 | name === TABLE_HEADER || 13 | name === TABLE_CELL 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/toSafeHTMLDocument.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Parses HTML in a detached document to help with avoiding XSS 4 | // https://developer.mozilla.org/en-US/docs/Archive/Add-ons/Code_snippets/HTML_to_DOM 5 | // https://github.com/ProseMirror/prosemirror/issues/473#issuecomment-255727531 6 | export default function toSafeHTMLDocument(html: string): ?Document { 7 | const parser = new window.DOMParser(); 8 | return parser.parseFromString(html, 'text/html'); 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/czi-custom-menu-button.css: -------------------------------------------------------------------------------- 1 | .czi-custom-menu-button.expanded.expanded.expanded { 2 | border-bottom-left-radius: 0; 3 | border-bottom-right-radius: 0; 4 | border-color: transparent; 5 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); 6 | } 7 | 8 | .czi-custom-menu-button.width-100 { 9 | width: 100px; 10 | } 11 | 12 | .czi-custom-menu-button.width-30 { 13 | width: 30px; 14 | } 15 | 16 | .czi-custom-menu-button.width-60 { 17 | width: 60px; 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | cache: npm 5 | jobs: 6 | include: 7 | - name: "Lint JS Files" 8 | script: npm run lint:js 9 | stage: test 10 | - name: "Lint CSS Files" 11 | script: npm run lint:css 12 | stage: test 13 | - name: "Type Checking" 14 | script: npx flow check 15 | stage: test 16 | - name: "Check Dist" 17 | script: scripts/ci_check_dist.sh 18 | stage: test 19 | -------------------------------------------------------------------------------- /src/hyphenize.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | function hadnleMatch(matched: string): string { 4 | return matched[0] + '-' + matched[1].toLowerCase(); 5 | } 6 | 7 | const cached = {}; 8 | 9 | // converts `fooBar` to `foo-bar`. 10 | export default function hyphenize(str: string): string { 11 | if (cached.hasOwnProperty(str)) { 12 | return cached[str]; 13 | } 14 | const result = str.replace(/[a-z][A-Z]/g, hadnleMatch); 15 | cached[str] = result; 16 | return result; 17 | } 18 | -------------------------------------------------------------------------------- /src/StyleView.js: -------------------------------------------------------------------------------- 1 | // temp view for custom style have to check this view is needed? 2 | 3 | import { EditorView } from 'prosemirror-view'; 4 | import { EditorState } from 'prosemirror-state'; 5 | 6 | export class StyleView { 7 | constructor(editorView: EditorView) { 8 | this.update(editorView, null); 9 | } 10 | 11 | update(view: EditorView, lastState: EditorState): void { 12 | if (view.readOnly) { 13 | this.destroy(); 14 | } 15 | } 16 | 17 | destroy() {} 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export { EditorState } from 'prosemirror-state'; 4 | export { default as isEditorStateEmpty } from './isEditorStateEmpty'; 5 | export { default as uuid } from './ui/uuid'; 6 | // [FS] IRAD-978 2020-06-05 7 | // Export Licit as a component 8 | export { default as Licit, DataType } from './client/Licit.js'; 9 | // export { ImageLike, EditorRuntime } from './Types'; //Flow garbles these types beyond use for now 10 | export { GET, POST, DELETE, PATCH } from './client/http'; 11 | -------------------------------------------------------------------------------- /src/convertFromHTML.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Schema } from 'prosemirror-model'; 4 | import { EditorState, Plugin } from 'prosemirror-state'; 5 | import convertFromDOMElement from './convertFromDOMElement.js'; 6 | 7 | export default function convertFromHTML( 8 | html: string, 9 | schema: Schema, 10 | plugins: Array 11 | ): EditorState { 12 | const root = document.createElement('html'); 13 | root.innerHTML = html || ' '; 14 | return convertFromDOMElement(root, schema, plugins); 15 | } 16 | -------------------------------------------------------------------------------- /src/ui/czi-custom-scrollbar.css: -------------------------------------------------------------------------------- 1 | /* width */ 2 | .czi-custom-scrollbar::-webkit-scrollbar { 3 | width: 10px; 4 | } 5 | 6 | /* Track */ 7 | .czi-custom-scrollbar::-webkit-scrollbar-track { 8 | background: #fff; 9 | } 10 | 11 | /* Handle */ 12 | .czi-custom-scrollbar::-webkit-scrollbar-thumb { 13 | background: #ccc; 14 | border-radius: 5px; 15 | box-shadow: inset 0 0 0 2px #fff; 16 | } 17 | 18 | /* Handle on hover */ 19 | .czi-custom-scrollbar::-webkit-scrollbar-thumb:hover { 20 | background: #aaa; 21 | } 22 | -------------------------------------------------------------------------------- /src/WebFontLoader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | class WebFontLoader { 4 | _implementation = null; 5 | 6 | setImplementation(impl: any): void { 7 | this._implementation = impl; 8 | } 9 | 10 | load(params: Object): void { 11 | const impl = this._implementation; 12 | if (impl) { 13 | impl.load(params); 14 | } else { 15 | console.warn('Method WebFontLoader.load does not have an implementation'); 16 | } 17 | } 18 | } 19 | 20 | const loader = new WebFontLoader(); 21 | 22 | export default loader; 23 | -------------------------------------------------------------------------------- /src/TextSelectionMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | import type { MarkSpec } from './Types.js'; 6 | 7 | const TextSelectionMarkSpec: MarkSpec = { 8 | attrs: { 9 | id: '', 10 | }, 11 | inline: true, 12 | group: 'inline', 13 | parseDOM: [ 14 | { 15 | tag: 'czi-text-selection', 16 | }, 17 | ], 18 | 19 | toDOM(node: Node) { 20 | return ['czi-text-selection', { class: 'czi-text-selection' }, 0]; 21 | }, 22 | }; 23 | 24 | export default TextSelectionMarkSpec; 25 | -------------------------------------------------------------------------------- /src/ui/isElementFullyVisible.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { fromHTMlElement } from '@modusoperandi/licit-ui-commands'; 4 | 5 | export default function isElementFullyVisible(el: HTMLElement): boolean { 6 | const { x, y, w, h } = fromHTMlElement(el); 7 | // to handle the rounded border scenario. 8 | const factor = '10px' === el.style.borderRadius ? 3 : 1; 9 | // Only checks the top-left point. 10 | const nwEl = 11 | w && h ? el.ownerDocument.elementFromPoint(x + factor, y + factor) : null; 12 | 13 | return !nwEl ? false : nwEl === el || el.contains(nwEl); 14 | } 15 | -------------------------------------------------------------------------------- /src/ui/toHexColor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Color from 'color'; 4 | 5 | const ColorMaping = { 6 | transparent: '', 7 | inherit: '', 8 | }; 9 | 10 | export default function toHexColor(source: any): string { 11 | if (!source) { 12 | return ''; 13 | } 14 | if (source in ColorMaping) { 15 | return ColorMaping[source]; 16 | } 17 | let hex = ''; 18 | try { 19 | hex = Color(source).hex().toLowerCase(); 20 | ColorMaping[source] = hex; 21 | } catch (ex) { 22 | console.warn('unable to convert to hex', source, ex); 23 | ColorMaping[source] = ''; 24 | } 25 | return hex; 26 | } 27 | -------------------------------------------------------------------------------- /run_image_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import platform 6 | import socket 7 | 8 | # [FS] IRAD-991 2020-07-17 9 | # gracefully handling commands WRT OS. 10 | hostname = socket.gethostname() 11 | IPAddr = socket.gethostbyname(hostname) 12 | port = '3004' 13 | 14 | if platform.system()=="Windows": cmd = 'node servers/image/run_image_server.bundle.js ' + ' PORT=' + port + ' IP='+ IPAddr 15 | else: cmd = 'PORT=' + port + ' IP=' + IPAddr + ' node servers/image/run_image_server.bundle.js' 16 | 17 | print('=' * 80) 18 | print(cmd) 19 | print('=' * 80) 20 | os.system(cmd) 21 | -------------------------------------------------------------------------------- /src/ui/LoadingIndicator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react'; 4 | 5 | // https://loading.io/css/ 6 | class LoadingIndicator extends React.PureComponent { 7 | render(): React.Element { 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 |
15 | ); 16 | } 17 | } 18 | 19 | export default LoadingIndicator; 20 | -------------------------------------------------------------------------------- /src/patchParagraphElements.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | ATTRIBUTE_INDENT, 5 | convertMarginLeftToIndentValue, 6 | } from './ParagraphNodeSpec.js'; 7 | 8 | export default function patchParagraphElements(doc: Document): void { 9 | Array.from(doc.querySelectorAll('p')).forEach(patchParagraphElement); 10 | } 11 | 12 | function patchParagraphElement(pElement: HTMLElement): void { 13 | const { marginLeft } = pElement.style; 14 | if (marginLeft) { 15 | const indent = convertMarginLeftToIndentValue(marginLeft); 16 | if (indent) { 17 | pElement.setAttribute(ATTRIBUTE_INDENT, String(indent)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/convertToCSSPTValue.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const PX_TO_PT_RATIO = 0.75292857; 4 | export const PT_TO_PX_RATIO = 1 / PX_TO_PT_RATIO; 5 | 6 | export default function convertToCSSPTValue(styleValue: string): number { 7 | const unit = styleValue.slice(-2).toLowerCase(); // Extract the last two characters for the unit 8 | const value = Number(styleValue.slice(0, -2)); // Extract and convert the number part 9 | 10 | if (!value || (unit !== 'px' && unit !== 'pt')) { 11 | return 0; 12 | } 13 | 14 | if (unit === 'px') { 15 | return PX_TO_PT_RATIO * value; 16 | } 17 | 18 | return value; // If 'pt', return the value unchanged 19 | } 20 | -------------------------------------------------------------------------------- /run_collab_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import platform 6 | import socket 7 | 8 | # [FS] IRAD-892 2020-03-03 9 | # Correct licit windows build 10 | # gracefully handling commands WRT OS. 11 | hostname = socket.gethostname() 12 | IPAddr = socket.gethostbyname(hostname) 13 | port = '3002' 14 | 15 | if platform.system()=="Windows": cmd = 'node servers/collab/run_licit_collab_server.bundle.js ' + ' PORT=' + port + ' IP='+ IPAddr 16 | else: cmd = 'PORT=' + port + ' IP=' + IPAddr + ' node servers/collab/run_licit_collab_server.bundle.js' 17 | 18 | print('=' * 80) 19 | print(cmd) 20 | print('=' * 80) 21 | os.system(cmd) 22 | -------------------------------------------------------------------------------- /src/client/throttle.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default function throttle( 4 | fn: Function, 5 | threshhold: number, 6 | context: any 7 | ): Function { 8 | let last; 9 | let deferTimer: window.TimeoutID; 10 | const boundFn = fn.bind(context); 11 | 12 | return function () { 13 | const now = Date.now(); 14 | const args = Array.prototype.slice.call(arguments); 15 | if (last && now < last + threshhold) { 16 | // hold on to it 17 | clearTimeout(deferTimer); 18 | deferTimer = setTimeout(() => { 19 | last = now; 20 | boundFn(...args); 21 | }, threshhold); 22 | } else { 23 | last = now; 24 | boundFn(...args); 25 | } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/CodeBlockNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const PRE_DOM = ['pre', ['code', 0]]; 4 | 5 | // https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js 6 | // :: NodeSpec A code listing. Disallows marks or non-text inline 7 | // nodes by default. Represented as a `
` element with a
 8 | // `` element inside of it.
 9 | const CodeBlockNodeSpec = {
10 |   attrs: {
11 |     id: { default: null },
12 |   },
13 |   content: 'inline*',
14 |   group: 'block',
15 |   marks: '_',
16 |   code: true,
17 |   defining: true,
18 |   parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
19 |   toDOM() {
20 |     return PRE_DOM;
21 |   },
22 | };
23 | 
24 | export default CodeBlockNodeSpec;
25 | 


--------------------------------------------------------------------------------
/src/TextUnderlineMarkSpec.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | import type { MarkSpec } from './Types.js';
 4 | 
 5 | // https://bitbucket.org/atlassian/atlaskit/src/34facee3f461/packages/editor-core/src/schema/nodes/?at=master
 6 | const TextUnderlineMarkSpec: MarkSpec = {
 7 |   attrs: {
 8 |     overridden: { default: false },
 9 |   },
10 |   parseDOM: [
11 |     {
12 |       tag: 'u',
13 |       getAttrs: (dom: HTMLElement) => {
14 |         const _overridden = dom.getAttribute('overridden');
15 |         return { overridden: _overridden === 'true' };
16 |       }
17 |     },
18 |   ],
19 |   toDOM(mark) {
20 |     return ['u', { overridden: mark.attrs.overridden }, 0];
21 |   },
22 | };
23 | 
24 | export default TextUnderlineMarkSpec;
25 | 


--------------------------------------------------------------------------------
/src/TextSuperMarkSpec.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | import type { MarkSpec } from './Types.js';
 4 | 
 5 | const TextSuperMarkSpec: MarkSpec = {
 6 |   attrs: {
 7 |     overridden: { default: false },
 8 |   },
 9 |   parseDOM: [
10 |     {
11 |       tag: 'sup',
12 |       getAttrs: (dom: HTMLElement) => {
13 |         const _overridden = dom.getAttribute('overridden');
14 |         return { overridden: _overridden === 'true' };
15 |       }
16 |     },
17 | 
18 |     {
19 |       style: 'vertical-align',
20 |       getAttrs: (value) => (value === 'sup' ? { overridden: true } : null),
21 |     },
22 |   ],
23 |   toDOM(mark) {
24 |     return ['sup', { overridden: mark.attrs.overridden }, 0];
25 |   },
26 | };
27 | 
28 | export default TextSuperMarkSpec;
29 | 


--------------------------------------------------------------------------------
/src/StrikeMarkSpec.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | import type { MarkSpec } from './Types.js';
 4 | 
 5 | // https://bitbucket.org/atlassian/atlaskit/src/34facee3f461/packages/editor-core/src/schema/nodes/?at=master
 6 | const StrikeMarkSpec: MarkSpec = {
 7 |   attrs: {
 8 |     overridden: { default: false }, // Optional attribute for additional logic
 9 |   },
10 |   parseDOM: [
11 |     {
12 |       tag: 'strike',
13 |       getAttrs: (dom: HTMLElement) => {
14 |         const _overridden = dom.getAttribute('overridden');
15 |         return { overridden: _overridden === 'true' };
16 |       }
17 |     },
18 |   ],
19 |   toDOM(mark) {
20 |     return ['strike', { overridden: mark.attrs.overridden }, 0];
21 |   },
22 | };
23 | 
24 | export default StrikeMarkSpec;
25 | 


--------------------------------------------------------------------------------
/src/findActiveMark.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | import { Mark, MarkType, Node } from 'prosemirror-model';
 4 | 
 5 | export default function findActiveMark(
 6 |   doc: Node,
 7 |   from: number,
 8 |   to: number,
 9 |   markType: MarkType
10 | ): ?Mark {
11 |   let ii = from;
12 |   if (doc.nodeSize <= 2) {
13 |     return null;
14 |   }
15 |   const finder = (mark) => mark.type === markType;
16 |   from = Math.max(2, from);
17 |   to = Math.min(to, doc.nodeSize - 2);
18 | 
19 |   while (ii <= to) {
20 |     const node = doc.nodeAt(ii);
21 |     if (!node?.marks) {
22 |       ii++;
23 |       continue;
24 |     }
25 |     const mark = node.marks.find(finder);
26 |     if (mark) {
27 |       return mark;
28 |     }
29 |     ii++;
30 |   }
31 |   return null;
32 | }
33 | 


--------------------------------------------------------------------------------
/src/TextSubMarkSpec.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | import type { MarkSpec } from './Types.js';
 4 | 
 5 | const TextSubMarkSpec: MarkSpec = {
 6 |   attrs: {
 7 |     overridden: { default: false },
 8 |   },
 9 |   parseDOM: [
10 |     {
11 |       tag: 'sub',
12 |       priority: 150,
13 |       getAttrs: (dom: HTMLElement) => {
14 |         const _overridden = dom.getAttribute('overridden');
15 |         return { overridden: _overridden === 'true' };
16 |       }
17 |     },
18 |     {
19 |       style: 'vertical-align',
20 |       getAttrs: (value) => (value === 'sub' ? { overridden: true } : null),
21 |     },
22 |   ],
23 |   toDOM(mark) {
24 |     return ['sub', { overridden: mark.attrs.overridden }, 0];
25 |   },
26 | };
27 | 
28 | export default TextSubMarkSpec;
29 | 


--------------------------------------------------------------------------------
/src/ui/czi-heading.css:
--------------------------------------------------------------------------------
 1 | .ProseMirror h1,
 2 | .ProseMirror h2,
 3 | .ProseMirror h3,
 4 | .ProseMirror h4,
 5 | .ProseMirror h5,
 6 | .ProseMirror h6 {
 7 |   font-weight: normal;
 8 |   line-height: var(--czi-content-line-height);
 9 |   orphans: 2;
10 |   widows: 2;
11 | }
12 | 
13 | .ProseMirror h1 {
14 |   font-size: 20pt;
15 | }
16 | 
17 | .ProseMirror h2 {
18 |   font-size: 18pt;
19 | }
20 | 
21 | .ProseMirror h3 {
22 |   font-size: 16pt;
23 | }
24 | 
25 | .ProseMirror h4 {
26 |   color: rgb(67, 67, 67);
27 |   font-size: 14pt;
28 | }
29 | 
30 | .ProseMirror h5 {
31 |   color: rgb(102, 102, 102);
32 |   font-size: 11pt;
33 |   font-weight: 400;
34 | }
35 | 
36 | .ProseMirror h6 {
37 |   color: rgb(102, 102, 102);
38 |   font-size: 11pt;
39 |   font-weight: 400;
40 | }
41 | 


--------------------------------------------------------------------------------
/src/ui/czi-selection-placeholder.css:
--------------------------------------------------------------------------------
 1 | @import 'czi-vars.css';
 2 | 
 3 | .czi-selection-placeholder {
 4 |   -webkit-animation-direction: alternate;
 5 |   animation-direction: alternate;
 6 |   -webkit-animation-duration: 0.5s;
 7 |   animation-duration: 0.5s;
 8 |   -webkit-animation-iteration-count: infinite;
 9 |   animation-iteration-count: infinite;
10 |   -webkit-animation-name: czi_selection_blink;
11 |   animation-name: czi_selection_blink;
12 |   -webkit-animation-timing-function: ease-in-out;
13 |   animation-timing-function: ease-in-out;
14 |   background-color: var(--czi-selection-highlight-color-dark) !important;
15 | }
16 | 
17 | @-webkit-keyframes czi_selection_blink {
18 |   from {
19 |     opacity: 1;
20 |   }
21 |   to {
22 |     opacity: 0.7;
23 |   }
24 | }
25 | 


--------------------------------------------------------------------------------
/run_web_server.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | import os
 5 | import platform
 6 | import logging
 7 | import socket    
 8 | from subprocess import check_output
 9 | 
10 | # [FS][03-MAR-2020]
11 | # IRAD-892 Correct licit windows build
12 | # gracefully handling commands WRT OS.
13 | hostname = socket.gethostname()    
14 | IPAddr = socket.gethostbyname(hostname)  
15 | port = '3001'
16 | 
17 | if platform.system()=="Windows": cmd = 'node utils/build_web_server.js ' + ' PORT=' + port + ' IP='+ IPAddr
18 | else: cmd = 'PORT=' + port + ' IP=' + IPAddr + ' node utils/build_web_server.js'
19 | 
20 | print('=' * 80)
21 | print(cmd)
22 | print('=' * 80)
23 | print('run web server at http://' + IPAddr + ':' + port)
24 | print('=' * 80)
25 | os.system(cmd)
26 | 


--------------------------------------------------------------------------------
/src/ui/handleEditorDrop.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | import { EditorView } from 'prosemirror-view';
 4 | 
 5 | import { uploadImageFiles } from '../ImageUploadPlaceholderPlugin.js';
 6 | 
 7 | // https://prosemirror.net/examples/upload/
 8 | export default function handleEditorDrop(
 9 |   view: EditorView,
10 |   event: DragEvent
11 | ): boolean {
12 |   const { dataTransfer } = event;
13 |   if (!dataTransfer) {
14 |     return false;
15 |   }
16 |   const { files } = dataTransfer;
17 |   if (!files?.length) {
18 |     return false;
19 |   }
20 | 
21 |   const filesList = Array.from(files);
22 |   const coords = { x: event.clientX, y: event.clientY };
23 |   if (uploadImageFiles(view, filesList, coords)) {
24 |     event.preventDefault();
25 |     return true;
26 |   }
27 |   return false;
28 | }
29 | 


--------------------------------------------------------------------------------
/src/ui/czi-custom-menu-item.css:
--------------------------------------------------------------------------------
 1 | .czi-custom-menu-item.czi-custom-button {
 2 |   border: none;
 3 |   border-radius: 0;
 4 |   display: block;
 5 |   margin: 0;
 6 |   padding-bottom: 8px;
 7 |   padding-left: 20px;
 8 |   padding-right: 20px;
 9 |   padding-top: 8px;
10 | }
11 | 
12 | .czi-custom-menu-item.czi-custom-button:hover {
13 |   background-color: var(--czi-button-hover-background-color);
14 | }
15 | 
16 | /* [FS] IRAD-1044 2020-09-22
17 | To adjust the width of the custom style menu dropdown. */
18 | 
19 | .custom-style-menu-item {
20 |   width: 250px;
21 | }
22 | 
23 | .czi-custom-menu-item-separator {
24 |   background-color: var(--czi-button-hover-background-color);
25 |   height: 1px;
26 |   margin-bottom: 8px;
27 |   margin-left: 0;
28 |   margin-right: 0;
29 |   margin-top: 8px;
30 | }
31 | 


--------------------------------------------------------------------------------
/src/client/Reporter.js:
--------------------------------------------------------------------------------
 1 | class Reporter {
 2 |   constructor() {
 3 |     this.setAt = 0;
 4 |   }
 5 | 
 6 |   clearState() {
 7 |     if (this.state) {
 8 |       this.state = this.node = null;
 9 |       this.setAt = 0;
10 |     }
11 |   }
12 | 
13 |   failure(err) {
14 |     console.error('fail', err.toString());
15 |   }
16 | 
17 |   delay(err) {
18 |     if (this.state == 'fail') return;
19 |     console.info('delay', err.toString());
20 |   }
21 | 
22 |   show(type, message) {
23 |     this.clearState();
24 |     this.state = type;
25 |     this.setAt = Date.now();
26 |   }
27 | 
28 |   success() {
29 |     if (this.state == 'fail' && this.setAt > Date.now() - 1000 * 10)
30 |       setTimeout(() => this.success(), 5000);
31 |     else this.clearState();
32 |   }
33 | }
34 | 
35 | export default Reporter;
36 | 


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
 1 | # To get started with Dependabot version updates, you'll need to specify which
 2 | # package ecosystems to update and where the package manifests are located.
 3 | # Please see the documentation for all configuration options:
 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
 5 | 
 6 | version: 2
 7 | updates:
 8 |   - package-ecosystem: "npm" # See documentation for possible values
 9 |     directory: "/" # Location of package manifests
10 |     schedule:
11 |       interval: weekly
12 |       day: sunday
13 |     # Raise pull requests for version updates
14 |     # to npm against the `develop` branch
15 |     target-branch: "develop"
16 |     # Labels on pull requests for security and version updates
17 |     labels:
18 |       - "npm dependencies"


--------------------------------------------------------------------------------
/src/createEmptyEditorState.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | import { Schema } from 'prosemirror-model';
 4 | import { EditorState, Plugin } from 'prosemirror-state';
 5 | import convertFromJSON from './convertFromJSON.js';
 6 | import EditorSchema from './EditorSchema.js';
 7 | 
 8 | export const EMPTY_DOC_JSON = {
 9 |   type: 'doc',
10 |   content: [
11 |     {
12 |       type: 'paragraph',
13 |     }, // [FS] IRAD-1710 2022-03-04 - No text content needed
14 |   ],
15 | };
16 | 
17 | export default function createEmptyEditorState(
18 |   schema: ?Schema,
19 |   defaultSchema: ?Schema,
20 |   plugins: Array
21 | ): EditorState {
22 |   // TODO: Check if schema support doc and paragraph nodes.
23 |   return convertFromJSON(
24 |     EMPTY_DOC_JSON,
25 |     schema,
26 |     defaultSchema || EditorSchema,
27 |     plugins
28 |   );
29 | }
30 | 


--------------------------------------------------------------------------------
/src/ui/CustomEditorView.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | import { EditorView } from 'prosemirror-view';
 4 | import * as React from 'react';
 5 | 
 6 | import type { DirectEditorProps, EditorRuntime } from '../Types.js';
 7 | 
 8 | 
 9 | // https://github.com/ProseMirror/prosemirror-view/blob/master/src/index.js
10 | class CustomEditorView extends EditorView {
11 |   disabled: boolean;
12 |   placeholder: ?(string | React.Element);
13 |   readOnly: boolean;
14 |   runtime: ?EditorRuntime;
15 |   constructor(place: HTMLElement, props: DirectEditorProps) {
16 |     super(place, props);
17 |     this.runtime = null;
18 |     this.readOnly = true;
19 |     this.disabled = true;
20 |     this.placeholder = null;
21 |   }
22 | 
23 |   destroy() {
24 |     super.destroy();
25 |     this._props = {};
26 |   }
27 | }
28 | 
29 | export default CustomEditorView;
30 | 


--------------------------------------------------------------------------------
/src/joinDown.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | // https://github.com/ProseMirror/prosemirror-commands/blob/master/src/commands.js
 3 | 
 4 | import { NodeSelection } from 'prosemirror-state';
 5 | import { Transform , canJoin, joinPoint} from 'prosemirror-transform';
 6 | 
 7 | // Join the selected block, or the closest ancestor of the selection
 8 | // that can be joined, with the sibling after it.
 9 | export default function joinDown(tr: Transform): Transform {
10 |   const sel = tr.selection;
11 |   let point;
12 |   if (sel instanceof NodeSelection) {
13 |     if (sel.node.isTextblock || !canJoin(tr.doc, sel.to)) {
14 |       return tr;
15 |     }
16 |     point = sel.to;
17 |   } else {
18 |     point = joinPoint(tr.doc, sel.to, 1);
19 |     if (point === null || point === undefined) {
20 |       return tr;
21 |     }
22 |   }
23 |   tr = tr.join(point);
24 |   return tr;
25 | }
26 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "allowJs": true,
 4 |     "jsx": "react",
 5 |     "target": "ES2022",
 6 |     "module": "esnext",
 7 |     "moduleResolution": "node",
 8 |     "allowSyntheticDefaultImports": true,
 9 |     "lib": [
10 |       "es2018",
11 |       "dom",
12 |       "dom.Iterable"
13 |     ],
14 |     "outDir": "dist",
15 |     "pretty": false,
16 |     "esModuleInterop": true,
17 |     "skipLibCheck": true,
18 |     "types": [
19 |       "jest",
20 |       "node",
21 |       "jest-prosemirror"
22 |     ],
23 |     // Ensure that .d.ts files are created by tsc, but not .js files
24 |     "declaration": true,
25 |     "emitDeclarationOnly": true
26 |   },
27 |   "typeAcquisition": {
28 |     "enable": true
29 |   },
30 |   "include": [
31 |     "src/**/*.ts",
32 |     "src/**/*.js"
33 |   ],
34 |   "exclude": [
35 |     "node_modules",
36 |   ]
37 | }


--------------------------------------------------------------------------------
/src/patchBreakElements.js:
--------------------------------------------------------------------------------
 1 | // @flow
 2 | 
 3 | export default function patchBreakElements(doc: Document): void {
 4 |   // This is a workaround to handle HTML converted from DraftJS that
 5 |   // `

` becomes `



`. 6 | // Block with single `
` inside should be collapsed into `

`. 7 | const selector = 'div > span:only-child > br:only-child'; 8 | Array.from(doc.querySelectorAll(selector)).forEach(patchBreakElement); 9 | } 10 | 11 | function patchBreakElement(brElement: HTMLElement): void { 12 | const { ownerDocument, parentElement } = brElement; 13 | if (!ownerDocument || !parentElement) { 14 | return; 15 | } 16 | const div = brElement.parentElement?.parentElement; 17 | if (!div) { 18 | return; 19 | } 20 | const pp = ownerDocument.createElement('p'); 21 | div.parentElement?.replaceChild(pp, div); 22 | } 23 | -------------------------------------------------------------------------------- /src/ui/handleEditorPaste.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorView } from 'prosemirror-view'; 4 | 5 | import { uploadImageFiles } from '../ImageUploadPlaceholderPlugin.js'; 6 | 7 | // workaround to support ClipboardEvent as a valid type. 8 | // https://github.com/facebook/flow/issues/1856 9 | declare class ClipboardEvent extends Event { 10 | clipboardData: DataTransfer; 11 | } 12 | 13 | export default function handleEditorPaste( 14 | view: EditorView, 15 | event: ClipboardEvent 16 | ): boolean { 17 | const { clipboardData } = event; 18 | if (!clipboardData) { 19 | return false; 20 | } 21 | 22 | const { files } = clipboardData; 23 | if (!files?.length) { 24 | return false; 25 | } 26 | const filesList = Array.from(files); 27 | 28 | if (uploadImageFiles(view, filesList)) { 29 | event.preventDefault(); 30 | return true; 31 | } 32 | return false; 33 | } 34 | -------------------------------------------------------------------------------- /src/BlockquoteNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | import ParagraphNodeSpec,{ getParagraphNodeAttrs, toParagraphDOM } from './ParagraphNodeSpec.js'; 6 | 7 | import type { NodeSpec } from './Types.js'; 8 | 9 | // https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js 10 | // :: NodeSpec A plain paragraph textblock. Represented in the DOM 11 | // as a `

` element. 12 | const BlockquoteNodeSpec: NodeSpec = { 13 | ...ParagraphNodeSpec, 14 | defining: true, 15 | parseDOM: [{ tag: 'blockquote', getAttrs }], 16 | toDOM, 17 | }; 18 | 19 | function toDOM(node: Node): Array { 20 | const dom = toParagraphDOM(node); 21 | dom[0] = 'blockquote'; 22 | return dom; 23 | } 24 | 25 | function getAttrs(dom: HTMLElement): Object { 26 | return getParagraphNodeAttrs(dom); 27 | } 28 | 29 | export default BlockquoteNodeSpec; 30 | -------------------------------------------------------------------------------- /src/HangingIndentMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | import type { MarkSpec } from './Types.js'; 5 | 6 | const HangingIndentMarkSpec: MarkSpec = { 7 | attrs: { 8 | prefix: { default: null }, 9 | overridden: { default: false }, 10 | }, 11 | inline: true, 12 | group: 'inline', 13 | parseDOM: [ 14 | { 15 | tag: 'span[prefix]', 16 | getAttrs: (domNode) => { 17 | const _prefix = domNode.getAttribute('prefix'); 18 | return { prefix: _prefix || null, overridden: true }; 19 | }, 20 | }, 21 | ], 22 | toDOM(node: Node) { 23 | const { prefix } = node.attrs; 24 | const attrs = { prefix, overridden: true }; 25 | return ['span', attrs, 0]; 26 | }, 27 | rank: 5000 28 | }; 29 | 30 | export default HangingIndentMarkSpec; 31 | -------------------------------------------------------------------------------- /src/MarkNames.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js 4 | export const MARK_CODE = 'code'; 5 | export const MARK_EM = 'em'; 6 | export const MARK_FONT_SIZE = 'mark-font-size'; 7 | export const MARK_FONT_TYPE = 'mark-font-type'; 8 | export const MARK_LINK = 'link'; 9 | export const MARK_NO_BREAK = 'mark-no-break'; 10 | export const MARK_STRIKE = 'strike'; 11 | export const MARK_STRONG = 'strong'; 12 | export const MARK_SUPER = 'super'; 13 | export const MARK_SUB = 'sub'; 14 | export const MARK_TEXT_COLOR = 'mark-text-color'; 15 | export const MARK_TEXT_HIGHLIGHT = 'mark-text-highlight'; 16 | export const MARK_TEXT_SELECTION = 'mark-text-selection'; 17 | export const MARK_UNDERLINE = 'underline'; 18 | export const MARK_SPACER = 'spacer'; 19 | export const MARK_OVERRIDE = 'override'; 20 | export const MARK_HANGING_INDENT = 'mark-hanging-indent'; 21 | -------------------------------------------------------------------------------- /src/NodeNames.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js 4 | export const BLOCKQUOTE = 'blockquote'; 5 | export const BOOKMARK = 'bookmark'; 6 | export const BULLET_LIST = 'bullet_list'; 7 | export const CODE_BLOCK = 'code_block'; 8 | export const DOC = 'doc'; 9 | export const HARD_BREAK = 'hard_break'; 10 | export const HEADING = 'heading'; 11 | export const HORIZONTAL_RULE = 'horizontal_rule'; 12 | export const IMAGE = 'image'; 13 | export const LINK = 'link'; 14 | export const LIST_ITEM = 'list_item'; 15 | export const MATH = 'math'; 16 | export const ORDERED_LIST = 'ordered_list'; 17 | export const PARAGRAPH = 'paragraph'; 18 | export const TABLE = 'table'; 19 | export const TABLE_CELL = 'table_cell'; 20 | export const TABLE_HEADER = 'table_header'; 21 | export const TABLE_ROW = 'table_row'; 22 | export const TEXT = 'text'; 23 | export const UNDERLINE = 'underline'; 24 | -------------------------------------------------------------------------------- /style-service.Dockerfile: -------------------------------------------------------------------------------- 1 | # To build: 2 | # docker build . -f style-service.Dockerfile -t style-service:latest 3 | 4 | # To run (simple demo, data is lost when container is removed): 5 | # docker run -d -p 3005:3005 --name style-service style-service 6 | 7 | # To make data persist across containers, create a volume: 8 | # docker volume create style-service-data 9 | # Then bind container to volume when run 10 | # docker run -d -p3005:3005 -v style-service-data:/app/customstyles/ --name style-service style-service 11 | 12 | FROM node:alpine 13 | 14 | RUN mkdir -p /app/customstyles && mkdir -p /app/server && chown -R node:node /app 15 | 16 | USER node:node 17 | 18 | COPY servers/customstyles/run_customstyle_server.bundle.js /app/server/index.js 19 | 20 | # Create volume to save styles if container is stopped. 21 | VOLUME /app/customstyles 22 | 23 | # Expose the server using default port 24 | EXPOSE 3005 25 | 26 | CMD cd /app/server && node . 27 | -------------------------------------------------------------------------------- /src/ui/czi-table-grid-size-editor.css: -------------------------------------------------------------------------------- 1 | .czi-table-grid-size-editor { 2 | background: #fff; 3 | box-shadow: var(--czi-overlay-shadow); 4 | font-family: var(--czi-font-family); 5 | font-size: var(--czi-font-size); 6 | } 7 | 8 | .czi-table-grid-size-editor-body { 9 | position: relative; 10 | } 11 | 12 | .czi-table-grid-size-editor-body::after { 13 | background: rgba(0, 0, 0, 0); 14 | bottom: -50px; 15 | content: ''; 16 | left: 0; 17 | position: absolute; 18 | right: -50px; 19 | top: -50px; 20 | } 21 | 22 | .czi-table-grid-size-editor-cell { 23 | border: var(--czi-border-grey); 24 | box-sizing: border-box; 25 | position: absolute; 26 | z-index: 2; 27 | } 28 | 29 | .czi-table-grid-size-editor-cell.selected { 30 | background: var(--czi-selection-highlight-color); 31 | border-color: var(--czi-selection-highlight-color-dark); 32 | } 33 | 34 | .czi-table-grid-size-editor-footer { 35 | padding-bottom: 5px; 36 | text-align: center; 37 | } 38 | -------------------------------------------------------------------------------- /src/ui/czi-image-url-editor.css: -------------------------------------------------------------------------------- 1 | @import './czi-vars.css'; 2 | 3 | .czi-image-url-editor { 4 | background: #fff; 5 | box-shadow: var(--czi-overlay-shadow); 6 | font-family: var(--czi-font-family); 7 | font-size: var(--czi-font-size); 8 | padding: 4px 10px; 9 | } 10 | 11 | .czi-image-url-editor .czi-form { 12 | max-width: 90vw; 13 | min-width: 40vw; 14 | width: 500px; 15 | } 16 | 17 | .czi-image-url-editor-src-input-row { 18 | position: relative; 19 | } 20 | 21 | .czi-image-url-editor-src-input-row input.czi-image-url-editor-src-input { 22 | padding-right: 40px; 23 | } 24 | 25 | .czi-image-url-editor-input-preview { 26 | background-color: #fff; 27 | background-position: center; 28 | background-repeat: no-repeat; 29 | background-size: contain; 30 | border: solid 1px #fff; 31 | bottom: 1px; 32 | box-sizing: border-box; 33 | outline: var(--czi-border-blue); 34 | position: absolute; 35 | right: 0; 36 | top: 2px; 37 | width: 36px; 38 | } 39 | -------------------------------------------------------------------------------- /src/HorizontalRuleNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | const DOM_ATTRIBUTE_PAGE_BREAK = 'data-page-break'; 6 | 7 | function getAttrs(dom: HTMLElement) { 8 | const attrs = {}; 9 | if ( 10 | dom.getAttribute(DOM_ATTRIBUTE_PAGE_BREAK) || 11 | dom.style.pageBreakBefore === 'always' 12 | ) { 13 | // Google Doc exports page break as HTML: 14 | // `


"` 11 | // at the start of a textblock into a blockquote. 12 | const MACRO_PATTERN = /^\s*>\s$/; 13 | 14 | function handleBlockQuoteInputRule( 15 | state: EditorState, 16 | match: any, 17 | start: any, 18 | end: any 19 | ): Transform { 20 | const { schema } = state; 21 | let { tr } = state; 22 | const nodeType = schema.nodes[BLOCKQUOTE]; 23 | if (!nodeType) { 24 | return tr; 25 | } 26 | 27 | tr = toggleBlockquote(tr, schema); 28 | if (tr.docChanged) { 29 | tr = tr.delete(start, end); 30 | } 31 | return tr; 32 | } 33 | 34 | export default function blockQuoteInputRule(): InputRule { 35 | return new InputRule(MACRO_PATTERN, handleBlockQuoteInputRule); 36 | } 37 | -------------------------------------------------------------------------------- /src/BookmarkNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { NodeSpec } from './Types.js'; 4 | 5 | export const ATTRIBUTE_BOOKMARK_ID = 'data-bookmark-id'; 6 | export const ATTRIBUTE_BOOKMARK_VISIBLE = 'data-bookmark-visible'; 7 | 8 | function getAttrs(dom: HTMLElement) { 9 | const id = dom.getAttribute(ATTRIBUTE_BOOKMARK_ID); 10 | const visible = dom.getAttribute(ATTRIBUTE_BOOKMARK_VISIBLE) === 'true'; 11 | return { 12 | id, 13 | visible, 14 | }; 15 | } 16 | 17 | const BookmarkNodeSpec: NodeSpec = { 18 | inline: true, 19 | attrs: { 20 | id: { default: null }, 21 | visible: { default: null }, 22 | }, 23 | group: 'inline', 24 | draggable: true, 25 | parseDOM: [{ tag: `a[${ATTRIBUTE_BOOKMARK_ID}]`, getAttrs }], 26 | toDOM(node) { 27 | const { id, visible } = node.attrs; 28 | const attrs = id 29 | ? { 30 | [ATTRIBUTE_BOOKMARK_ID]: id, 31 | [ATTRIBUTE_BOOKMARK_VISIBLE]: visible, 32 | id, 33 | } 34 | : {}; 35 | return ['a', attrs]; 36 | }, 37 | }; 38 | 39 | export default BookmarkNodeSpec; 40 | -------------------------------------------------------------------------------- /src/LinkMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { MarkSpec } from './Types.js'; 4 | 5 | const LinkMarkSpec: MarkSpec = { 6 | attrs: { 7 | href: { default: null }, 8 | rel: { default: 'noopener noreferrer nofollow' }, 9 | target: { default: 'blank' }, 10 | title: { default: null }, 11 | selectionId: { 12 | default: null, 13 | }, 14 | }, 15 | inclusive: false, 16 | parseDOM: [ 17 | { 18 | tag: 'a[href]', 19 | getAttrs: (dom) => { 20 | const href = dom.getAttribute('href'); 21 | const target = href?.indexOf('#') === 0 ? '' : 'blank'; 22 | const selectionId = dom.getAttribute('selectionId') ?? ''; 23 | return { 24 | href: dom.getAttribute('href'), 25 | title: dom.getAttribute('title'), 26 | target, 27 | selectionId, 28 | }; 29 | }, 30 | }, 31 | ], 32 | toDOM(node) { 33 | const attrs = { 34 | ...node.attrs, 35 | onclick: 'return false', 36 | }; 37 | return ['a', attrs, 0]; 38 | }, 39 | }; 40 | 41 | export default LinkMarkSpec; 42 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [include] 2 | .*/src/.* 3 | .*/licit/.* 4 | [ignore] 5 | .*/dist/.* 6 | .*/bin/.* 7 | .*/node_modules/@babel.* 8 | .*/node_modules/@emotion/.* 9 | .*/node_modules/babel-plugin-emotion/.* 10 | .*/node_modules/chalk/.* 11 | .*/node_modules/create-emotion-styled/.* 12 | .*/node_modules/create-emotion/.* 13 | .*/node_modules/create-react-context/.* 14 | .*/node_modules/draft-convert/.* 15 | .*/node_modules/draft-js/.* 16 | .*/node_modules/eslint-plugin-jsx-a11y/.* 17 | .*/node_modules/eslint/.* 18 | .*/node_modules/flow-webpack-plugin/.* 19 | .*/node_modules/inquirer/.* 20 | .*/node_modules/jsondiffpatch/.* 21 | .*/node_modules/katex/.* 22 | .*/node_modules/match-at/.* 23 | .*/node_modules/postcss-modules-extract-imports/.* 24 | .*/node_modules/postcss-modules-local-by-default/.* 25 | .*/node_modules/postcss-modules-scope/.* 26 | .*/node_modules/postcss-modules-values/.* 27 | .*/node_modules/postcss/.* 28 | .*/node_modules/react-emotion/.* 29 | .*/node_modules/unstated/.* 30 | .*/node_modules/stylelint/.* 31 | .*/node_modules/gensync/.* 32 | 33 | [libs] 34 | ./flow-typed/.* 35 | 36 | [options] 37 | -------------------------------------------------------------------------------- /scripts/webserver.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | 3 | import WebpackDevServer from 'webpack-dev-server'; 4 | import webpack from 'webpack'; 5 | import config from '../webpack.config'; 6 | import env from './env'; 7 | import path from 'path'; 8 | 9 | const options = config.chromeExtensionBoilerplate || {}; 10 | const excludeEntriesToHotReload = options.notHotReload || []; 11 | 12 | for (const entryName in config.entry) { 13 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) { 14 | config.entry[entryName] = [ 15 | 'webpack-dev-server/client?http://localhost:' + env.PORT, 16 | 'webpack/hot/dev-server', 17 | ].concat(config.entry[entryName]); 18 | } 19 | } 20 | 21 | config.plugins = [new webpack.HotModuleReplacementPlugin()].concat( 22 | config.plugins || [] 23 | ); 24 | 25 | delete config.chromeExtensionBoilerplate; 26 | 27 | const compiler = webpack(config); 28 | 29 | const server = new WebpackDevServer(compiler, { 30 | hot: true, 31 | contentBase: path.join(__dirname, '../build'), 32 | headers: {'Access-Control-Allow-Origin': '*'}, 33 | }); 34 | 35 | server.listen(env.PORT); 36 | -------------------------------------------------------------------------------- /src/TextColorMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | import toCSSColor from './ui/toCSSColor.js'; 6 | 7 | import type { MarkSpec } from './Types.js'; 8 | 9 | const TextColorMarkSpec: MarkSpec = { 10 | attrs: { 11 | color: { default: null }, // Allow missing color 12 | overridden: { default: false }, 13 | }, 14 | inline: true, 15 | group: 'inline', 16 | parseDOM: [ 17 | { 18 | tag: 'span[style*=color]', 19 | getAttrs: (dom: HTMLElement) => { 20 | const { color } = dom.style; 21 | const overridden = dom.getAttribute('overridden') === 'true'; // Extract overridden flag 22 | return { 23 | color: toCSSColor(color), 24 | overridden 25 | }; 26 | }, 27 | }, 28 | ], 29 | toDOM(node: Node) { 30 | const { color, overridden } = node.attrs; 31 | const attrs = {}; 32 | if (color) { 33 | attrs.style = `color: ${color};`; 34 | attrs['overridden'] = overridden?.toString(); 35 | } 36 | return ['span', attrs, 0]; 37 | }, 38 | }; 39 | 40 | export default TextColorMarkSpec; 41 | -------------------------------------------------------------------------------- /src/convertFromDOMElement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { DOMParser, Schema } from 'prosemirror-model'; 4 | import { EditorState, Plugin } from 'prosemirror-state'; 5 | import { getAttrs } from './DocNodeSpec.js'; 6 | import EditorPlugins from './EditorPlugins.js'; 7 | import EditorSchema from './EditorSchema.js'; 8 | 9 | export default function convertFromDOMElement( 10 | el: HTMLElement, 11 | schema: Schema, 12 | plugins: Array 13 | ): EditorState { 14 | const effectiveSchema = schema || EditorSchema; 15 | const effectivePlugins = plugins || EditorPlugins; 16 | const bodyEl = el.querySelector('body'); 17 | 18 | // https://prosemirror.net/docs/ref/#model.ParseOptions.preserveWhitespace 19 | const doc = DOMParser.fromSchema(effectiveSchema).parse(el, { 20 | preserveWhitespace: true, 21 | }); 22 | 23 | if (bodyEl) { 24 | // Unfortunately the root node `doc` does not supoort `parseDOM`, thus 25 | // we'd have to assign its `attrs` manually. 26 | doc.attrs = getAttrs(bodyEl); 27 | } 28 | 29 | return EditorState.create({ 30 | doc, 31 | plugins: effectivePlugins, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/ui/CustomMenuItem.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { CustomButton } from '@modusoperandi/licit-ui-commands'; 4 | import * as React from 'react'; 5 | 6 | class CustomMenuItemSeparator extends React.PureComponent { 7 | render(): React.Element { 8 | return
; 9 | } 10 | } 11 | 12 | class CustomMenuItem extends React.PureComponent { 13 | static Separator = CustomMenuItemSeparator; 14 | 15 | props: { 16 | label: string, 17 | disabled?: ?boolean, 18 | onClick: ?(value: any, e: SyntheticEvent<>) => void, 19 | onMouseEnter: ?(value: any, e: SyntheticEvent<>) => void, 20 | value: any, 21 | }; 22 | 23 | render(): React.Element { 24 | // [FS] IRAD-1044 2020-09-22 25 | // Added a new class to adjust the width of the custom style menu dropdown. 26 | 27 | let className = 'czi-custom-menu-item'; 28 | if (this.props.value._customStyleName) { 29 | className += ' custom-style-menu-item'; 30 | } 31 | return ; 32 | } 33 | } 34 | 35 | export default CustomMenuItem; 36 | -------------------------------------------------------------------------------- /src/joinUp.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // https://github.com/ProseMirror/prosemirror-commands/blob/master/src/commands.js 3 | 4 | import { NodeSelection } from 'prosemirror-state'; 5 | import { Transform, canJoin, joinPoint } from 'prosemirror-transform'; 6 | 7 | // Join the selected block or, if there is a text selection, the 8 | // closest ancestor block of the selection that can be joined, with 9 | // the sibling above it. 10 | export default function joinUp(tr: Transform): Transform { 11 | const sel = tr.selection; 12 | const nodeSel = sel instanceof NodeSelection; 13 | let point; 14 | if (nodeSel) { 15 | if (sel.node.isTextblock || !canJoin(tr.doc, sel.from)) { 16 | return tr; 17 | } 18 | point = sel.from; 19 | } else { 20 | point = joinPoint(tr.doc, sel.from, -1); 21 | if (point === null || point === undefined) { 22 | return tr; 23 | } 24 | } 25 | 26 | tr = tr.join(point); 27 | if (nodeSel) { 28 | tr = tr.setSelection( 29 | NodeSelection.create( 30 | tr.doc, 31 | point - tr.doc.resolve(point).nodeBefore.nodeSize 32 | ) 33 | ); 34 | } 35 | 36 | return tr; 37 | } 38 | -------------------------------------------------------------------------------- /src/ui/czi-image-upload-placeholder.css: -------------------------------------------------------------------------------- 1 | .czi-image-upload-placeholder { 2 | background: #fff; 3 | border-radius: 5px; 4 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.28); 5 | display: inline-block; 6 | height: 46px; 7 | margin: 0 2px 0 0; 8 | position: relative; 9 | width: 56px; 10 | } 11 | 12 | .czi-image-upload-placeholder-child { 13 | animation: cziImageUploadPlaceholder 1.2s cubic-bezier(0, 0.5, 0.5, 1) 14 | infinite; 15 | background: #ccc; 16 | border-radius: 3px; 17 | left: 6px; 18 | position: absolute; 19 | width: 5px; 20 | } 21 | 22 | .czi-image-upload-placeholder-child:nth-child(1) { 23 | animation-delay: -0.24s; 24 | left: 10px; 25 | } 26 | 27 | .czi-image-upload-placeholder-child:nth-child(2) { 28 | animation-delay: -0.12s; 29 | left: 24px; 30 | } 31 | 32 | .czi-image-upload-placeholder-child:nth-child(3) { 33 | animation-delay: 0; 34 | left: 38px; 35 | } 36 | 37 | @keyframes cziImageUploadPlaceholder { 38 | 0% { 39 | height: 40px; 40 | opacity: 0.5; 41 | top: 4px; 42 | } 43 | 44 | 50%, 45 | 100% { 46 | height: 26px; 47 | opacity: 1; 48 | top: 10px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/findActiveFontType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | 5 | import { MARK_FONT_TYPE } from '../MarkNames.js'; 6 | import findActiveMark from '../findActiveMark.js'; 7 | 8 | // This should map to `--czi-content-font-size` at `czi-editor.css`. 9 | export const FONT_TYPE_NAME_DEFAULT = 'Arial'; 10 | 11 | export default function findActiveFontType(state: EditorState): string { 12 | const { schema, doc, selection, tr } = state; 13 | const markType = schema.marks[MARK_FONT_TYPE]; 14 | if (!markType) { 15 | return FONT_TYPE_NAME_DEFAULT; 16 | } 17 | const { from, to, empty } = selection; 18 | 19 | if (empty) { 20 | const storedMarks = 21 | tr.storedMarks || 22 | state.storedMarks || 23 | selection.$cursor?.marks?.() || []; 24 | const sm = storedMarks.find((m) => m.type === markType); 25 | return (sm?.attrs.name) || FONT_TYPE_NAME_DEFAULT; 26 | } 27 | 28 | const mark = findActiveMark(doc, from, to, markType); 29 | const fontName = mark?.attrs.name; 30 | if (!fontName) { 31 | return FONT_TYPE_NAME_DEFAULT; 32 | } 33 | 34 | return fontName; 35 | } 36 | -------------------------------------------------------------------------------- /src/rebaseDocWithSteps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Step } from 'prosemirror-transform'; 4 | import EditorSchema from './EditorSchema'; 5 | 6 | type RebaseResult = { 7 | docJSON: Object, 8 | stepsJSON: Array, 9 | }; 10 | 11 | export default function rebaseDocWithSteps( 12 | clientID: string, 13 | docJSON: Object, 14 | stepsJSON: Array 15 | ): Promise { 16 | return new Promise((resolve, reject) => { 17 | // TODO: Move this into a separate server request. 18 | let docNode = EditorSchema.nodeFromJSON(docJSON); 19 | 20 | const steps = stepsJSON.map((step) => { 21 | const result = Step.fromJSON(EditorSchema, step); 22 | result.clientID = clientID; 23 | return result; 24 | }); 25 | 26 | steps.forEach((step) => { 27 | const result = step.apply(docNode); 28 | docNode = result.doc; 29 | }); 30 | 31 | const newDocJSON = docNode.toJSON(); 32 | 33 | const newStepsJSON = steps.map((step) => { 34 | return step.toJSON(); 35 | }); 36 | 37 | resolve({ 38 | docJSON: newDocJSON, 39 | stepsJSON: newStepsJSON, 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/SpacerMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | import type { MarkSpec } from './Types.js'; 6 | 7 | export const DOM_ATTRIBUTE_SIZE = 'data-spacer-size'; 8 | export const SPACER_SIZE_TAB = 'tab'; 9 | export const SPACER_SIZE_TAB_LARGE = 'tab-large'; 10 | 11 | // See http://jkorpela.fi/chars/spaces.html 12 | export const HAIR_SPACE_CHAR = '\u200A'; 13 | 14 | const SpacerMarkSpec: MarkSpec = { 15 | attrs: { 16 | size: { default: SPACER_SIZE_TAB }, 17 | }, 18 | defining: true, 19 | draggable: false, 20 | excludes: '_', 21 | group: 'inline', 22 | inclusive: false, 23 | inline: true, 24 | spanning: false, 25 | parseDOM: [ 26 | { 27 | tag: `span[${DOM_ATTRIBUTE_SIZE}]`, 28 | getAttrs: (el) => { 29 | return { 30 | size: el.getAttribute(DOM_ATTRIBUTE_SIZE) || SPACER_SIZE_TAB, 31 | }; 32 | }, 33 | }, 34 | ], 35 | toDOM(node: Node) { 36 | const { size } = node.attrs; 37 | return [ 38 | 'span', 39 | { 40 | [DOM_ATTRIBUTE_SIZE]: size, 41 | }, 42 | 0, 43 | ]; 44 | }, 45 | }; 46 | 47 | export default SpacerMarkSpec; 48 | -------------------------------------------------------------------------------- /src/HistoryRedoCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { redo } from 'prosemirror-history'; 4 | import { EditorState } from 'prosemirror-state'; 5 | import { Transform } from 'prosemirror-transform'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import * as React from 'react'; 8 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 9 | 10 | class HistoryRedoCommand extends UICommand { 11 | execute = ( 12 | state: EditorState, 13 | dispatch: ?(tr: Transform) => void, 14 | view: ?EditorView 15 | ): boolean => { 16 | return redo(state, dispatch); 17 | }; 18 | waitForUserInput = ( 19 | _state: EditorState, 20 | _dispatch: ?(tr: Transform) => void, 21 | _view: ?EditorView, 22 | _event: ?React.SyntheticEvent 23 | ): Promise => { 24 | return Promise.resolve(undefined); 25 | }; 26 | 27 | executeWithUserInput = ( 28 | _state: EditorState, 29 | _dispatch: ?(tr: Transform) => void, 30 | _view: ?EditorView, 31 | _inputs: ?string 32 | ): boolean => { 33 | return false; 34 | }; 35 | 36 | cancel(): void { 37 | return null; 38 | } 39 | } 40 | 41 | export default HistoryRedoCommand; 42 | -------------------------------------------------------------------------------- /src/HistoryUndoCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { undo } from 'prosemirror-history'; 4 | import { EditorState } from 'prosemirror-state'; 5 | import { Transform } from 'prosemirror-transform'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import * as React from 'react'; 8 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 9 | 10 | class HistoryUndoCommand extends UICommand { 11 | execute = ( 12 | state: EditorState, 13 | dispatch: ?(tr: Transform) => void, 14 | view: ?EditorView 15 | ): boolean => { 16 | return undo(state, dispatch); 17 | }; 18 | waitForUserInput = ( 19 | _state: EditorState, 20 | _dispatch: ?(tr: Transform) => void, 21 | _view: ?EditorView, 22 | _event: ?React.SyntheticEvent 23 | ): Promise => { 24 | return Promise.resolve(undefined); 25 | }; 26 | 27 | executeWithUserInput = ( 28 | _state: EditorState, 29 | _dispatch: ?(tr: Transform) => void, 30 | _view: ?EditorView, 31 | _inputs: ?string 32 | ): boolean => { 33 | return false; 34 | }; 35 | 36 | cancel(): void { 37 | return null; 38 | } 39 | } 40 | 41 | export default HistoryUndoCommand; 42 | -------------------------------------------------------------------------------- /src/ui/TableNodeView.js: -------------------------------------------------------------------------------- 1 | import { Node } from 'prosemirror-model'; 2 | import { EditorView } from 'prosemirror-view'; 3 | import { TableView } from 'prosemirror-tables'; 4 | 5 | // A custom table view that renders the margin-left style. 6 | export default class TableNodeView extends TableView { 7 | constructor(node: Node, colMinWidth: number, view: EditorView) { 8 | super(node, colMinWidth, view); 9 | this._updateAttrs(node); 10 | } 11 | update(node: Node): boolean { 12 | const updated = super.update(node); 13 | if (updated) { 14 | this._updateAttrs(node); 15 | } 16 | return updated; 17 | } 18 | 19 | _updateAttrs(node: Node): void { 20 | // Handle marginLeft 21 | const marginLeft = node.attrs?.marginLeft || 0; 22 | this.table.style.marginLeft = marginLeft ? `${marginLeft}px` : ''; 23 | 24 | // Handle vignette 25 | if (node.attrs?.vignette) { 26 | this.table.style.border = 'none'; 27 | } 28 | 29 | // Handle dirty -> sets a data attribute for DOM/state sync 30 | if (node.attrs?.dirty) { 31 | this.table.setAttribute('dirty', 'true'); 32 | } else { 33 | this.table.removeAttribute('dirty'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ui/PasteMenu.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import uuid from './uuid'; 3 | 4 | // [FS] IRAD-1076 2020-10-15 5 | // Popup menu UI with paste options. 6 | 7 | class PasteMenu extends React.PureComponent { 8 | props: { 9 | close: (?string) => void, 10 | }; 11 | 12 | _menu = null; 13 | _id = uuid(); 14 | 15 | state = { 16 | expanded: false, 17 | }; 18 | 19 | render() { 20 | const children = []; 21 | children.push( 22 | 31 | ); 32 | children.push( 33 | 42 | ); 43 | 44 | return
{children}
; 45 | } 46 | 47 | _onUIEnter = (id: string) => { 48 | this.props.close(id); 49 | }; 50 | } 51 | 52 | export default PasteMenu; 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Portions Copyright (c) 2020 Modus Operandi Inc 4 | Portions Copyright (c) 2018-2019 Chan Zuckerberg Initiative 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/HeadingNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | import ParagraphNodeSpec, { 5 | getParagraphNodeAttrs, 6 | toParagraphDOM, 7 | } from './ParagraphNodeSpec'; 8 | import type { NodeSpec } from './Types'; 9 | 10 | // https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js 11 | // :: NodeSpec A plain paragraph textblock. Represented in the DOM 12 | // as a `

` element. 13 | const HeadingNodeSpec: NodeSpec = { 14 | ...ParagraphNodeSpec, 15 | attrs: { 16 | ...ParagraphNodeSpec.attrs, 17 | }, 18 | defining: true, 19 | parseDOM: [ 20 | { tag: 'h1', getAttrs }, 21 | { tag: 'h2', getAttrs }, 22 | { tag: 'h3', getAttrs }, 23 | { tag: 'h4', getAttrs }, 24 | { tag: 'h5', getAttrs }, 25 | { tag: 'h6', getAttrs }, 26 | ], 27 | toDOM, 28 | }; 29 | 30 | function toDOM(node: Node): Array { 31 | // [FS-SEA][06-04-2023] 32 | // returns paragraph node to dom when a header node paste 33 | const dom = toParagraphDOM(node); 34 | return dom; 35 | } 36 | 37 | function getAttrs(dom: HTMLElement): Object { 38 | const attrs = getParagraphNodeAttrs(dom); 39 | return attrs; 40 | } 41 | 42 | export default HeadingNodeSpec; 43 | -------------------------------------------------------------------------------- /src/ui/bindScrollHandler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type ScrollHandle = { 4 | dispose: () => void, 5 | }; 6 | 7 | export default function bindScrollHandler( 8 | target: Element, 9 | callback: Function 10 | ): ScrollHandle { 11 | const defaultView = target.ownerDocument.defaultView; 12 | const els = []; 13 | 14 | let rid = 0; 15 | 16 | let onScroll = () => { 17 | // Debounce the scroll handler. 18 | rid && cancelAnimationFrame(rid); 19 | rid = requestAnimationFrame(callback); 20 | }; 21 | 22 | let el: any = target; 23 | 24 | // Scroll event does not bubble, so we need to look up all the scrollable 25 | // elements. 26 | while (el) { 27 | const overflow = defaultView.getComputedStyle(el).overflow; 28 | if ((onScroll && overflow === 'auto') || overflow === 'scroll') { 29 | el.addEventListener('scroll', onScroll, false); 30 | els.push(el); 31 | } 32 | el = el.parentElement; 33 | } 34 | 35 | return { 36 | dispose() { 37 | while (onScroll && els.length) { 38 | el = els.pop(); 39 | el?.removeEventListener('scroll', onScroll, false); 40 | } 41 | onScroll = null; 42 | rid && window.cancelAnimationFrame(rid); 43 | rid = 0; 44 | }, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/ui/injectStyleSheet.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import url from 'url'; 3 | const addedElements = new Map(); 4 | 5 | function createElement(tag: string, attrs: Object): Element { 6 | const el: any = document.createElement(tag); 7 | Object.keys(attrs).forEach((key) => { 8 | if (key === 'className') { 9 | el[key] = attrs[key]; 10 | } else { 11 | el.setAttribute(key, attrs[key]); 12 | } 13 | }); 14 | return el; 15 | } 16 | 17 | export default function injectStyleSheet(urlStr: string): void { 18 | const parsedURL = new URL(urlStr); 19 | const { protocol } = parsedURL; 20 | const protocolPattern = /^(http:|https:)/; 21 | if (!protocolPattern.test(protocol || '')) { 22 | if (protocolPattern.test(window.location.protocol)) { 23 | parsedURL.protocol = window.location.protocol; 24 | } else { 25 | parsedURL.protocol = 'http:'; 26 | } 27 | } 28 | const href = url.format(parsedURL); 29 | if (addedElements.has(href)) { 30 | return; 31 | } 32 | const el = createElement('link', { 33 | crossorigin: 'anonymous', 34 | href, 35 | rel: 'stylesheet', 36 | }); 37 | addedElements.set(href, el); 38 | const root = document.head || document.documentElement || document.body; 39 | root?.appendChild(el); 40 | } 41 | -------------------------------------------------------------------------------- /src/ui/toCSSColor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Color from 'color'; 4 | 5 | const RGBA_PATTERN = /^rgba/i; 6 | const RGBA_TRANSPARENT = 'rgba(0,0,0,0)'; 7 | 8 | const ColorMaping = { 9 | transparent: RGBA_TRANSPARENT, 10 | inherit: '', 11 | }; 12 | 13 | export function isTransparent(source: any): boolean { 14 | if (!source) { 15 | return true; 16 | } 17 | const hex = toCSSColor(source); 18 | return !hex || hex === RGBA_TRANSPARENT; 19 | } 20 | 21 | export function toCSSColor(source: any): string { 22 | if (!source) { 23 | return ''; 24 | } 25 | if (source in ColorMaping) { 26 | return ColorMaping[source]; 27 | } 28 | 29 | if (source && RGBA_PATTERN.test(source)) { 30 | const color = Color(source); 31 | if (color.valpha === 0) { 32 | ColorMaping[source] = RGBA_TRANSPARENT; 33 | return RGBA_TRANSPARENT; 34 | } 35 | const rgba = color.toString(); 36 | ColorMaping[source] = rgba.toString(); 37 | return rgba; 38 | } 39 | 40 | let hex = ''; 41 | try { 42 | hex = Color(source).hex().toLowerCase(); 43 | ColorMaping[source] = hex; 44 | } catch (ex) { 45 | console.warn('unable to convert to hex', source, ex); 46 | ColorMaping[source] = ''; 47 | } 48 | return hex; 49 | } 50 | 51 | export default toCSSColor; 52 | -------------------------------------------------------------------------------- /utils/build_web_server.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import WebpackDevServer from 'webpack-dev-server'; 3 | import config from '../webpack.config.js'; 4 | 5 | import env from './env.js'; 6 | import path, { dirname } from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | const options = config.chromeExtensionBoilerplate || {}; 12 | const excludeEntriesToHotReload = options.notHotReload || []; 13 | 14 | for (const entryName in config.entry) { 15 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) { 16 | config.entry[entryName] = [ 17 | 'webpack-dev-server/client?http://localhost:' + env.PORT, 18 | 'webpack/hot/dev-server', 19 | ].concat(config.entry[entryName]); 20 | } 21 | } 22 | 23 | config.plugins = [new webpack.HotModuleReplacementPlugin()].concat( 24 | config.plugins || [] 25 | ); 26 | 27 | delete config.chromeExtensionBoilerplate; 28 | 29 | const compiler = webpack(config); 30 | 31 | const server = new WebpackDevServer( 32 | { 33 | hot: true, 34 | static: path.join(__dirname, '../bin'), 35 | headers: { 'Access-Control-Allow-Origin': '*' }, 36 | port: env.PORT, 37 | }, 38 | compiler 39 | ); 40 | 41 | server.start(); 42 | -------------------------------------------------------------------------------- /src/ui/czi-math-view.css: -------------------------------------------------------------------------------- 1 | .czi-math-view { 2 | display: inline-block; 3 | margin: 0; 4 | position: relative; 5 | text-align: center; 6 | white-space: nowrap; 7 | } 8 | 9 | .czi-math-view.ProseMirror-selectednode { 10 | outline: none; 11 | } 12 | 13 | .czi-math-view-body.selected { 14 | background-color: var(--czi-selection-highlight-color-dark); 15 | } 16 | 17 | .czi-math-view-body.active { 18 | background-color: transparent; 19 | box-shadow: 0 0 1px 2px var(--czi-selection-highlight-color-dark); 20 | } 21 | 22 | .czi-math-view-body { 23 | border: solid 1px transparent; 24 | display: inline-block; 25 | padding: 0 2px; 26 | user-select: none; 27 | } 28 | 29 | .czi-math-view-body-img { 30 | bottom: 0; 31 | left: 0; 32 | opacity: 0; 33 | position: absolute; 34 | right: 0; 35 | top: 0; 36 | } 37 | 38 | .czi-math-view-body-content { 39 | pointer-events: none; 40 | } 41 | 42 | .czi-math-view-body-content .katex { 43 | font-display: block; 44 | } 45 | 46 | .czi-math-view.align-left { 47 | float: left; 48 | margin: 0 20px 20px 0; 49 | } 50 | 51 | .czi-math-view.align-right { 52 | float: right; 53 | margin: 0 0 20px 20px; 54 | } 55 | 56 | .czi-math-view.align-center { 57 | clear: both; 58 | display: inline-block; 59 | float: none; 60 | margin: 20px 0; 61 | width: 100%; 62 | } 63 | -------------------------------------------------------------------------------- /src/ui/czi-loading-indicator.css: -------------------------------------------------------------------------------- 1 | .czi-loading-indicator { 2 | display: inline-block; 3 | height: 13px; 4 | position: relative; 5 | width: 64px; 6 | } 7 | 8 | .czi-loading-indicator .frag { 9 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 10 | background: var(--czi-selection-highlight-color-dark); 11 | border-radius: 50%; 12 | height: 11px; 13 | position: absolute; 14 | top: 3px; 15 | width: 11px; 16 | } 17 | 18 | .czi-loading-indicator-frag-1 { 19 | animation: czi_loading_animation 0.6s infinite; 20 | left: 6px; 21 | } 22 | 23 | .czi-loading-indicator-frag-2 { 24 | animation: lds-ellipsis2 0.6s infinite; 25 | left: 6px; 26 | } 27 | 28 | .czi-loading-indicator-frag-3 { 29 | animation: lds-ellipsis2 0.6s infinite; 30 | left: 26px; 31 | } 32 | 33 | .czi-loading-indicator-frag-4 { 34 | animation: lds-ellipsis3 0.6s infinite; 35 | left: 45px; 36 | } 37 | 38 | @keyframes czi_loading_animation { 39 | 0% { 40 | transform: scale(0); 41 | } 42 | 43 | 100% { 44 | transform: scale(1); 45 | } 46 | } 47 | 48 | @keyframes lds-ellipsis3 { 49 | 0% { 50 | transform: scale(1); 51 | } 52 | 53 | 100% { 54 | transform: scale(0); 55 | } 56 | } 57 | 58 | @keyframes lds-ellipsis2 { 59 | 0% { 60 | transform: translate(0, 0); 61 | } 62 | 63 | 100% { 64 | transform: translate(19px, 0); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-react", 10 | "@babel/preset-flow" 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-proposal-class-properties", 14 | "@babel/plugin-proposal-export-default-from", 15 | "flow-react-proptypes", 16 | "transform-react-remove-prop-types", 17 | "@babel/plugin-transform-flow-strip-types", 18 | "@babel/plugin-proposal-object-rest-spread", 19 | "@babel/plugin-transform-parameters", 20 | [ 21 | "@babel/plugin-transform-runtime", 22 | { 23 | "helpers": false, 24 | "regenerator": true, 25 | "absoluteRuntime": "babel-runtime" 26 | } 27 | ], 28 | "@babel/plugin-syntax-dynamic-import", 29 | "@babel/plugin-syntax-import-meta", 30 | [ 31 | "@babel/plugin-proposal-decorators", 32 | { 33 | "legacy": true 34 | } 35 | ], 36 | "@babel/plugin-proposal-function-sent", 37 | "@babel/plugin-proposal-export-namespace-from", 38 | "@babel/plugin-proposal-throw-expressions", 39 | "@babel/plugin-proposal-logical-assignment-operators", 40 | [ 41 | "@babel/plugin-proposal-pipeline-operator", 42 | { 43 | "proposal": "minimal" 44 | } 45 | ], 46 | "@babel/plugin-proposal-do-expressions" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/PrintCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { EditorView } from 'prosemirror-view'; 6 | import * as React from 'react'; 7 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 8 | 9 | class PrintCommand extends UICommand { 10 | isActive = (state: EditorState): boolean => { 11 | return false; 12 | }; 13 | 14 | isEnabled = (state: EditorState): boolean => { 15 | return !!window.print; 16 | }; 17 | 18 | execute = ( 19 | state: EditorState, 20 | dispatch: ?(tr: Transform) => void, 21 | view: ?EditorView 22 | ): boolean => { 23 | if (dispatch && window.print) { 24 | window.print(); 25 | return true; 26 | } 27 | return false; 28 | }; 29 | 30 | waitForUserInput = ( 31 | _state: EditorState, 32 | _dispatch: ?(tr: Transform) => void, 33 | _view: ?EditorView, 34 | _event: ?React.SyntheticEvent 35 | ): Promise => { 36 | return Promise.resolve(undefined); 37 | }; 38 | 39 | executeWithUserInput = ( 40 | _state: EditorState, 41 | _dispatch: ?(tr: Transform) => void, 42 | _view: ?EditorView, 43 | _inputs: ?string 44 | ): boolean => { 45 | return false; 46 | }; 47 | 48 | cancel(): void { 49 | return null; 50 | } 51 | } 52 | 53 | export default PrintCommand; 54 | -------------------------------------------------------------------------------- /src/patchAnchorElements.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | ATTRIBUTE_BOOKMARK_ID, 5 | ATTRIBUTE_BOOKMARK_VISIBLE, 6 | } from './BookmarkNodeSpec.js'; 7 | 8 | const BLOCK_NODE_NAME_PATTERN = /(P|H1|H2|H3|H4|H5|H6)/; 9 | 10 | export default function patchAnchorElements(doc: Document): void { 11 | Array.from(doc.querySelectorAll('a[id]')).forEach(patchAnchorElement); 12 | } 13 | 14 | function patchAnchorElement(node: HTMLElement): void { 15 | const { id } = node; 16 | if (id && node.childElementCount === 0) { 17 | // This looks like a bookmark generated from Google Doc, will render 18 | // this as BookmarkNode. 19 | node.setAttribute(ATTRIBUTE_BOOKMARK_ID, id); 20 | 21 | // Google Doc always inject anchor links before . 22 | // 23 | // 24 | //
25 | // and these anchor link should not be visible. 26 | const visible = !node.id.startsWith('t.'); 27 | 28 | visible && node.setAttribute(ATTRIBUTE_BOOKMARK_VISIBLE, 'true'); 29 | } 30 | const nextNode = node.nextElementSibling; 31 | if (!nextNode) { 32 | return; 33 | } 34 | // If this is next to a block element, make that block element the bookmark. 35 | if (BLOCK_NODE_NAME_PATTERN.test(nextNode.nodeName)) { 36 | nextNode.insertBefore(node, nextNode.firstChild); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ui/TableCellMenu.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { EditorState, PluginView } from 'prosemirror-state'; 3 | import { EditorView } from 'prosemirror-view'; 4 | import { Node } from 'prosemirror-model'; 5 | import * as React from 'react'; 6 | 7 | import CommandMenuButton from './CommandMenuButton.js'; 8 | import { TABLE_COMMANDS_GROUP } from './EditorToolbarConfig.js'; 9 | import Icon from './Icon.js'; 10 | 11 | type Props = { 12 | editorState: EditorState, 13 | editorView: EditorView, 14 | pluginView: PluginView, 15 | actionNode: Node, 16 | }; 17 | 18 | class TableCellMenu extends React.PureComponent { 19 | _menu = null; 20 | 21 | props: Props; 22 | 23 | render(): React.Element { 24 | const { editorState, editorView, pluginView, actionNode } = this.props; 25 | let cmdGrps = null; 26 | 27 | if (pluginView.getMenu) { 28 | cmdGrps = pluginView.getMenu(editorState, actionNode, TABLE_COMMANDS_GROUP); 29 | } 30 | 31 | if (!cmdGrps) { 32 | cmdGrps = TABLE_COMMANDS_GROUP; 33 | } 34 | 35 | return ( 36 | 45 | ); 46 | } 47 | } 48 | 49 | export default TableCellMenu; 50 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": true 8 | } 9 | } 10 | ], 11 | "@babel/preset-react", 12 | "@babel/preset-flow" 13 | ], 14 | "plugins": [ 15 | "@babel/plugin-proposal-class-properties", 16 | "@babel/plugin-proposal-export-default-from", 17 | "flow-react-proptypes", 18 | "transform-react-remove-prop-types", 19 | "@babel/plugin-transform-flow-strip-types", 20 | "@babel/plugin-proposal-object-rest-spread", 21 | "@babel/plugin-transform-parameters", 22 | [ 23 | "@babel/plugin-transform-runtime", 24 | { 25 | "helpers": false, 26 | "regenerator": true, 27 | "absoluteRuntime": "babel-runtime" 28 | } 29 | ], 30 | "@babel/plugin-syntax-dynamic-import", 31 | "@babel/plugin-syntax-import-meta", 32 | [ 33 | "@babel/plugin-proposal-decorators", 34 | { 35 | "legacy": true 36 | } 37 | ], 38 | "@babel/plugin-proposal-function-sent", 39 | "@babel/plugin-proposal-export-namespace-from", 40 | "@babel/plugin-proposal-throw-expressions", 41 | "@babel/plugin-proposal-logical-assignment-operators", 42 | [ 43 | "@babel/plugin-proposal-pipeline-operator", 44 | { 45 | "proposal": "minimal" 46 | } 47 | ], 48 | "@babel/plugin-proposal-do-expressions" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/czi-image-upload-editor.css: -------------------------------------------------------------------------------- 1 | @import './czi-vars.css'; 2 | 3 | .czi-image-upload-editor { 4 | background: #fff; 5 | box-shadow: var(--czi-overlay-shadow); 6 | font-family: var(--czi-font-family); 7 | font-size: var(--czi-font-size); 8 | padding: 4px 10px; 9 | } 10 | 11 | .czi-image-upload-editor-body { 12 | border: var(--czi-border-grey); 13 | box-sizing: border-box; 14 | display: flex; 15 | flex-direction: column; 16 | height: 200px; 17 | justify-content: center; 18 | margin: 0 0 20px 0; 19 | overflow: hidden; 20 | position: relative; 21 | width: 450px; 22 | } 23 | 24 | .czi-image-upload-editor-body:hover { 25 | border-color: var(--czi-selection-highlight-color); 26 | } 27 | 28 | .czi-image-upload-editor-label { 29 | color: var(--czi-link-color); 30 | padding: 8px; 31 | text-align: center; 32 | } 33 | 34 | .czi-image-upload-editor-body:hover .czi-image-upload-editor-label { 35 | text-decoration: underline; 36 | } 37 | 38 | .czi-image-upload-editor-input { 39 | cursor: pointer; 40 | font-size: 10000px; 41 | height: 1000px; 42 | left: 0; 43 | opacity: 0; 44 | padding: 10000px; 45 | position: absolute; 46 | top: 0; 47 | width: 1000px; 48 | } 49 | 50 | .czi-image-upload-editor.pending .czi-image-upload-editor-input { 51 | cursor: wait; 52 | } 53 | 54 | .czi-image-upload-editor.pending .czi-image-upload-editor-label { 55 | color: unset; 56 | text-decoration: none; 57 | } 58 | -------------------------------------------------------------------------------- /src/TextHighlightMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | import { isTransparent, toCSSColor } from './ui/toCSSColor.js'; 5 | 6 | import type { MarkSpec } from './Types.js'; 7 | 8 | const TextHighlightMarkSpec: MarkSpec = { 9 | attrs: { 10 | highlightColor: { default: null }, // Allow missing color 11 | overridden: { default: false }, 12 | }, 13 | inline: true, 14 | group: 'inline', 15 | parseDOM: [ 16 | { 17 | tag: 'span[style*=background-color]', 18 | priority: 100, 19 | getAttrs: (dom: HTMLElement) => { 20 | const { backgroundColor } = dom.style; 21 | const color = toCSSColor(backgroundColor); 22 | const overridden = dom.getAttribute('overridden') === 'true'; // Extract overridden flag 23 | 24 | return { 25 | highlightColor: isTransparent(color) ? '' : color, 26 | overridden, // Ensure overridden is captured 27 | }; 28 | }, 29 | }, 30 | ], 31 | 32 | toDOM(node: Node) { 33 | const { highlightColor, overridden } = node.attrs; 34 | const attrs = {}; 35 | 36 | if (highlightColor) { 37 | attrs.style = `background-color: ${highlightColor};`; 38 | } 39 | 40 | // Store overridden flag properly as a data attribute 41 | attrs['overridden'] = overridden?.toString(); 42 | 43 | return ['span', attrs, 0]; 44 | }, 45 | }; 46 | 47 | export default TextHighlightMarkSpec; 48 | -------------------------------------------------------------------------------- /src/ui/canUseCSSFont.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const cached = {}; 4 | 5 | export default function canUseCSSFont(fontName: string): Promise { 6 | const doc: any = document; 7 | 8 | if (cached.hasOwnProperty(fontName)) { 9 | return Promise.resolve(cached[fontName]); 10 | } 11 | 12 | if ( 13 | !doc.fonts?.check || 14 | !doc.fonts?.ready || 15 | !doc.fonts?.status || 16 | !doc.fonts?.values 17 | ) { 18 | // Feature is not supported, install the CSS anyway 19 | // https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/check#Browser_compatibility 20 | // TODO: Polyfill this. 21 | console.log('FontFaceSet is not supported'); 22 | return Promise.resolve(false); 23 | } 24 | 25 | return new Promise((resolve) => { 26 | // https://stackoverflow.com/questions/5680013/how-to-be-notified-once-a-web-font-has-loaded 27 | // All fonts in use by visible text have loaded. 28 | const check = () => { 29 | if (doc.fonts.status !== 'loaded') { 30 | setTimeout(check, 350); 31 | return; 32 | } 33 | // Do not use `doc.fonts.check()` because it may return falsey result. 34 | const fontFaces = Array.from(doc.fonts.values()); 35 | const matched = fontFaces.find((ff) => ff.family === fontName); 36 | const result = !!matched; 37 | cached[fontName] = result; 38 | resolve(result); 39 | }; 40 | doc.fonts.ready.then(check); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/ListSplitCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Schema } from 'prosemirror-model'; 4 | import { EditorState } from 'prosemirror-state'; 5 | import { Transform } from 'prosemirror-transform'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import * as React from 'react'; 8 | import splitListItem from './splitListItem.js'; 9 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 10 | 11 | class ListSplitCommand extends UICommand { 12 | constructor(schema: Schema) { 13 | super(); 14 | } 15 | 16 | execute = ( 17 | state: EditorState, 18 | dispatch: ?(tr: Transform) => void, 19 | view: ?EditorView 20 | ): boolean => { 21 | const { selection, schema } = state; 22 | const tr = splitListItem(state.tr.setSelection(selection), schema); 23 | if (tr.docChanged) { 24 | dispatch && dispatch(tr); 25 | return true; 26 | } else { 27 | return false; 28 | } 29 | }; 30 | 31 | waitForUserInput = ( 32 | _state: EditorState, 33 | _dispatch: ?(tr: Transform) => void, 34 | _view: ?EditorView, 35 | _event: ?React.SyntheticEvent 36 | ): Promise => { 37 | return Promise.resolve(undefined); 38 | }; 39 | 40 | executeWithUserInput = ( 41 | _state: EditorState, 42 | _dispatch: ?(tr: Transform) => void, 43 | _view: ?EditorView, 44 | _inputs: ?string 45 | ): boolean => { 46 | return false; 47 | }; 48 | 49 | cancel(): void { 50 | return null; 51 | } 52 | } 53 | 54 | export default ListSplitCommand; 55 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence, 6 | # @MO-Movia, @rodgersgb, and @tranqui341 will be requested for 7 | # review when someone opens a pull request. 8 | * @MO-Movia @rodgersgb @tranqui341 9 | 10 | # Order is important; the last matching pattern takes the most 11 | # precedence. When someone opens a pull request that only 12 | # modifies JS files, only @js-owner and not the global 13 | # owner(s) will be requested for a review. 14 | *.js @MO-Movia @melgish 15 | *.ts @MO-Movia @melgish 16 | *.css @MO-Movia @melgish 17 | 18 | # You can also use email addresses if you prefer. They'll be 19 | # used to look up users just like we do for commit author 20 | # emails. 21 | # *.go docs@example.com 22 | 23 | # In this example, @doctocat owns any files in the build/logs 24 | # directory at the root of the repository and any of its 25 | # subdirectories. 26 | # /build/logs/ @doctocat 27 | 28 | # The `docs/*` pattern will match files like 29 | # `docs/getting-started.md` but not further nested files like 30 | # `docs/build-app/troubleshooting.md`. 31 | # docs/* docs@example.com 32 | 33 | # In this example, @octocat owns any file in an apps directory 34 | # anywhere in your repository. 35 | # apps/ @octocat 36 | 37 | # In this example, @doctocat owns any file in the `/docs` 38 | # directory in the root of your repository. 39 | # /docs/ @doctocat 40 | 41 | -------------------------------------------------------------------------------- /src/ui/LinkTooltip.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorView } from 'prosemirror-view'; 4 | import * as React from 'react'; 5 | 6 | import { CustomButton } from '@modusoperandi/licit-ui-commands'; 7 | 8 | 9 | class LinkTooltip extends React.PureComponent { 10 | props: { 11 | editorView: EditorView, 12 | href: string, 13 | }; 14 | 15 | render(): React.Element { 16 | const { href, tocItemPos_, selectionId_ } = 17 | this.props; 18 | const getLabel = () => { 19 | if (tocItemPos_ && selectionId_) { 20 | return tocItemPos_.textContent === '' ? 'Reference not found' : tocItemPos_.textContent; 21 | } else if (!tocItemPos_ && selectionId_) { 22 | return 'Reference not found'; 23 | } 24 | return href; 25 | }; 26 | 27 | const label = getLabel(); 28 | const isRemoved = label === 'Reference not found'; 29 | 30 | return ( 31 |
32 |
33 |
34 | 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | 49 | } 50 | 51 | export default LinkTooltip; 52 | -------------------------------------------------------------------------------- /src/ui/toCSSLineSpacing.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Line spacing names and their values. 4 | export const LINE_SPACING_100 = '125%'; 5 | export const LINE_SPACING_115 = '138%'; 6 | export const LINE_SPACING_150 = '165%'; 7 | export const LINE_SPACING_200 = '232%'; 8 | 9 | // Normalize the css line-height vlaue to percentage-based value if applicable. 10 | // Also, it calibrates the incorrect line spacing value exported from Google 11 | // Doc. 12 | export default function toCSSLineSpacing(source: any): string { 13 | if (!source) { 14 | return ''; 15 | } 16 | 17 | let strValue = String(source); 18 | 19 | // e.g. line-height: 1.5; 20 | if (!isNaN(Number(strValue))) { 21 | const numValue = Number(strValue); 22 | strValue = String(Math.round(numValue * 100)) + '%'; 23 | } 24 | 25 | // Google Doc exports line spacing with wrong values. For instance: 26 | // - Single => 100% 27 | // - 1.15 => 115% 28 | // - Double => 200% 29 | // But the actual CSS value measured in Google Doc is like this: 30 | // - Single => 125% 31 | // - 1.15 => 138% 32 | // - Double => 232% 33 | // The following `if` block will calibrate the value if applicable. 34 | 35 | if (strValue === '100%') { 36 | return LINE_SPACING_100; 37 | } 38 | 39 | if (strValue === '115%') { 40 | return LINE_SPACING_115; 41 | } 42 | 43 | if (strValue === '150%') { 44 | return LINE_SPACING_150; 45 | } 46 | 47 | if (strValue === '200%') { 48 | return LINE_SPACING_200; 49 | } 50 | 51 | // e.g. line-height: 15px; 52 | return strValue; 53 | } 54 | -------------------------------------------------------------------------------- /src/convertFromJSON.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Schema } from 'prosemirror-model'; 4 | import { EditorState, Plugin } from 'prosemirror-state'; 5 | import createEmptyEditorState from './createEmptyEditorState.js'; 6 | 7 | export default function convertFromJSON( 8 | json: Object | string, 9 | schema: ?Schema, 10 | defaultSchema: Schema, 11 | effectivePlugins: Array 12 | ): EditorState { 13 | const editorSchema = schema || defaultSchema; 14 | let error = false; 15 | 16 | if (typeof json === 'string') { 17 | try { 18 | json = JSON.parse(json); 19 | } catch (ex) { 20 | console.error('convertFromJSON:', ex); 21 | error = true; 22 | } 23 | } 24 | 25 | if (!json || typeof json !== 'object') { 26 | console.error('convertFromJSON: invalid object', json); 27 | error = true; 28 | } 29 | 30 | if (error) { 31 | // Use the effectivePlugins, editor hangs, b'coz of missing default core plugins 32 | return createEmptyEditorState(schema, defaultSchema, effectivePlugins); 33 | } 34 | 35 | // Handle gracefully when error thrown on invalid json 36 | let doc = null; 37 | 38 | try { 39 | if (undefined === json.content) { 40 | json.content = [{ type: 'paragraph' }]; 41 | } 42 | doc = editorSchema.nodeFromJSON(json); 43 | } catch (error) { 44 | console.error('Failed to convert JSON to valid ProseMirror: ', error); 45 | return null; 46 | } 47 | 48 | return EditorState.create({ 49 | doc: doc, 50 | schema: editorSchema, 51 | plugins: effectivePlugins, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/dependencycheck.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do scan of dependencies to check for issues like vulnerabilities. 2 | 3 | name: dependencycheck 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: '0 0 * * *' # 12:00 AM Daily 9 | pull_request: 10 | branches: [main, master] 11 | 12 | jobs: 13 | dependencycheck: 14 | runs-on: self-hosted 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20.x] 19 | 20 | timeout-minutes: 60 # Default 5 days for self hosted 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm update --ignore-scripts 27 | 28 | # Dependency-Check 29 | - name: Dependency-Check 30 | run: npx owasp-dependency-check --scan . --failOnCVSS 1 --nvdApiKey ${{ secrets.NVD_KEY }} --ossIndexUsername ${{ secrets.SONATYPE_OSS_INDEX_USERNAME }} --ossIndexPassword ${{ secrets.SONATYPE_OSS_INDEX_TOKEN }} --disableYarnAudit --exclude node_modules/node-gyp/**/* --exclude dependency-check-bin/**/* --suppression https://github.com/MO-Movia/suppressions/raw/refs/heads/main/suppression.xml 31 | - name: Upload Report 32 | uses: actions/upload-artifact@v4 33 | if: ${{ failure() }} 34 | with: 35 | name: dependency-check-reports 36 | path: dependency-check-reports 37 | # Dependency-Track 38 | #- name: Dependency-Track 39 | # run: npx @cyclonedx/cyclonedx-npm --package-lock-only --output-file SBOM.json 40 | 41 | -------------------------------------------------------------------------------- /src/joinListNode.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Schema } from 'prosemirror-model'; 4 | import { TextSelection } from 'prosemirror-state'; 5 | import { Transform } from 'prosemirror-transform'; 6 | 7 | import { isListNode } from '@modusoperandi/licit-ui-commands'; 8 | import joinDown from './joinDown'; 9 | import joinUp from './joinUp'; 10 | 11 | export default function joinListNode( 12 | tr: Transform, 13 | schema: Schema, 14 | listNodePos: number 15 | ): Transform { 16 | if (!tr.doc || !tr.selection) { 17 | return tr; 18 | } 19 | const node = tr.doc.nodeAt(listNodePos); 20 | if (!isListNode(node)) { 21 | return tr; 22 | } 23 | const initialSelection = tr.selection; 24 | const listFromPos = listNodePos; 25 | const listToPos = listFromPos + node.nodeSize; 26 | const $fromPos = tr.doc.resolve(listFromPos); 27 | const $toPos = tr.doc.resolve(listToPos); 28 | 29 | let selectionOffset = 0; 30 | if ( 31 | $toPos.nodeAfter && 32 | $toPos.nodeAfter.type === node.type && 33 | $toPos.nodeAfter.attrs.level === node.attrs.level 34 | ) { 35 | tr = joinDown(tr); 36 | } 37 | 38 | if ( 39 | $fromPos.nodeBefore && 40 | $fromPos.nodeBefore.type === node.type && 41 | $fromPos.nodeBefore.attrs.level === node.attrs.level 42 | ) { 43 | selectionOffset -= 2; 44 | tr = joinUp(tr); 45 | } 46 | 47 | const selection = TextSelection.create( 48 | tr.doc, 49 | initialSelection.from + selectionOffset, 50 | initialSelection.to + selectionOffset 51 | ); 52 | 53 | tr = tr.setSelection(selection); 54 | return tr; 55 | } 56 | -------------------------------------------------------------------------------- /src/ListItemNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | import type { NodeSpec } from './Types.js'; 6 | 7 | export const ATTRIBUTE_LIST_STYLE_TYPE = 'data-list-style-type'; 8 | 9 | const ALIGN_PATTERN = /(left|right|center|justify)/; 10 | 11 | function getAttrs(dom: HTMLElement) { 12 | const attrs = {}; 13 | const { textAlign } = dom.style; 14 | let align = dom.getAttribute('data-align') || textAlign || ''; 15 | align = ALIGN_PATTERN.test(align) ? align : null; 16 | 17 | if (align) { 18 | attrs.align = align; 19 | } 20 | return attrs; 21 | } 22 | 23 | const ListItemNodeSpec: NodeSpec = { 24 | attrs: { 25 | align: { default: null }, 26 | }, 27 | 28 | // NOTE: 29 | // This spec does not support nested lists (e.g. `'paragraph block*'`) 30 | // as content because of the complexity of dealing with indentation 31 | // (context: https://github.com/ProseMirror/prosemirror/issues/92). 32 | // content: '(bullet_list|paragraph)+', 33 | content: 'paragraph block*', 34 | 35 | parseDOM: [{ tag: 'li', getAttrs }], 36 | 37 | // NOTE: 38 | // This method only defines the minimum HTML attributes needed when the node 39 | // is serialized to HTML string. Usually this is called when user copies 40 | // the node to clipboard. 41 | // The actual DOM rendering logic is defined at `src/ui/ListItemNodeView.js`. 42 | toDOM(node: Node): Array { 43 | const attrs = {}; 44 | const { align } = node.attrs; 45 | if (align) { 46 | attrs['data-align'] = align; 47 | } 48 | return ['li', attrs, 0]; 49 | }, 50 | }; 51 | 52 | export default ListItemNodeSpec; 53 | -------------------------------------------------------------------------------- /src/insertTable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Fragment, Schema } from 'prosemirror-model'; 4 | import { TextSelection } from 'prosemirror-state'; 5 | import { Transform } from 'prosemirror-transform'; 6 | 7 | import { PARAGRAPH, TABLE, TABLE_CELL, TABLE_ROW } from './NodeNames.js'; 8 | 9 | export default function insertTable( 10 | tr: Transform, 11 | schema: Schema, 12 | rows: number, 13 | cols: number 14 | ): Transform { 15 | if (!tr.selection || !tr.doc) { 16 | return tr; 17 | } 18 | const { from, to } = tr.selection; 19 | if (from !== to) { 20 | return tr; 21 | } 22 | 23 | const { nodes } = schema; 24 | const cell = nodes[TABLE_CELL]; 25 | const paragraph = nodes[PARAGRAPH]; 26 | const row = nodes[TABLE_ROW]; 27 | const table = nodes[TABLE]; 28 | if (!(cell && paragraph && row && table)) { 29 | return tr; 30 | } 31 | 32 | const rowNodes = []; 33 | for (let rr = 0; rr < rows; rr++) { 34 | const cellNodes = []; 35 | for (let cc = 0; cc < cols; cc++) { 36 | // [FS] IRAD-950 2020-05-25 37 | // Fix:Extra arrow key required for cell navigation using arrow right/Left 38 | const cellNode = cell.create( 39 | undefined, 40 | Fragment.fromArray([paragraph.create()]) 41 | ); 42 | cellNodes.push(cellNode); 43 | } 44 | const rowNode = row.create({}, Fragment.from(cellNodes)); 45 | rowNodes.push(rowNode); 46 | } 47 | const tableNode = table.create({}, Fragment.from(rowNodes)); 48 | tr = tr.insert(from, Fragment.from(tableNode)); 49 | 50 | const selection = TextSelection.create(tr.doc, from + 5, from + 5); 51 | 52 | tr = tr.setSelection(selection); 53 | return tr; 54 | } 55 | -------------------------------------------------------------------------------- /src/ui/ImageInlineEditor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { CustomButton } from '@modusoperandi/licit-ui-commands'; 4 | import * as React from 'react'; 5 | 6 | export type PropValue = { 7 | value: ?string, 8 | text: ?string, 9 | }; 10 | 11 | type Key = 'NONE' | 'LEFT' | 'CENTER' | 'RIGHT'; 12 | 13 | const ImageAlignValues: { [key: Key]: PropValue } = { 14 | NONE: { 15 | value: null, 16 | text: 'Inline', 17 | }, 18 | LEFT: { 19 | value: 'left', 20 | text: 'Float left', 21 | }, 22 | CENTER: { 23 | value: 'center', 24 | text: 'Break text', 25 | }, 26 | RIGHT: { 27 | value: 'right', 28 | text: 'Float right', 29 | }, 30 | }; 31 | 32 | export type ImageInlineEditorValue = { 33 | align: ?string, 34 | }; 35 | 36 | class ImageInlineEditor extends React.PureComponent { 37 | props: { 38 | onSelect: (val: ImageInlineEditorValue) => void, 39 | value: ?ImageInlineEditorValue, 40 | }; 41 | 42 | render(): React.Element { 43 | const align = this.props.value ? this.props.value.align : null; 44 | const onClick = this._onClick; 45 | const buttons = Object.keys(ImageAlignValues).map((key) => { 46 | const { value, text } = ImageAlignValues[key]; 47 | return ( 48 | 55 | ); 56 | }); 57 | 58 | return
{buttons}
; 59 | } 60 | 61 | _onClick = (align: ?string): void => { 62 | this.props.onSelect({ align: align }); 63 | }; 64 | } 65 | 66 | export default ImageInlineEditor; 67 | -------------------------------------------------------------------------------- /src/FontSizeMarkSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | import { toClosestFontPtSize } from './toClosestFontPtSize.js'; 5 | import type { MarkSpec } from './Types.js'; 6 | 7 | const FontSizeMarkSpec: MarkSpec = { 8 | attrs: { 9 | pt: { default: null }, 10 | overridden: { default: false }, 11 | }, 12 | inline: true, 13 | group: 'inline', 14 | parseDOM: [ 15 | { 16 | tag: 'span[style*=font-size]', 17 | getAttrs: (domNode) => { 18 | const fontSize = domNode.style.fontSize || ''; 19 | const _mOverriden = domNode.getAttribute('overridden'); 20 | let ptValue = 0; 21 | let _mptValue = 0; 22 | 23 | const parentFontsize = domNode.parentNode?.style.fontSize || ''; 24 | const mparent_overriden = domNode.parentNode?.getAttribute('overridden'); 25 | if (fontSize !== '') { 26 | ptValue = toClosestFontPtSize(fontSize); 27 | } 28 | if (parentFontsize !== '') { 29 | _mptValue = toClosestFontPtSize(parentFontsize); 30 | } 31 | 32 | const overridden = (_mOverriden === 'true' && fontSize !== '') || (parentFontsize !== '' && mparent_overriden === 'true'); // Check if the font is overridden 33 | 34 | return { 35 | pt: ptValue || _mptValue, 36 | overridden: overridden, 37 | }; 38 | }, 39 | }, 40 | ], 41 | toDOM(node: Node) { 42 | const { pt, overridden } = node.attrs; 43 | const attrs = { overridden }; 44 | 45 | if (pt) { 46 | attrs.style = `font-size: ${pt}pt;`; 47 | attrs.class = 'czi-font-size-mark'; 48 | } 49 | 50 | return ['span', attrs, 0]; 51 | }, 52 | }; 53 | 54 | export default FontSizeMarkSpec; 55 | -------------------------------------------------------------------------------- /src/ui/AlertInfo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | class AlertInfo extends React.PureComponent { 7 | _unmounted = false; 8 | 9 | constructor(props: any) { 10 | super(props); 11 | } 12 | 13 | // [FS] IRAD-1005 2020-07-07 14 | // Upgrade outdated packages. 15 | // To take care of the property type declaration. 16 | static propsTypes = { 17 | initialValue: PropTypes.object, 18 | close: function (props: any, propName: string) { 19 | const fn = props[propName]; 20 | if ( 21 | !fn.prototype || 22 | (typeof fn.prototype.constructor !== 'function' && 23 | fn.prototype.constructor.length !== 1) 24 | ) { 25 | return new Error( 26 | propName + ' must be a function with 1 arg of type ImageLike' 27 | ); 28 | } 29 | return null; 30 | }, 31 | }; 32 | 33 | state = { 34 | ...(this.props.initialValue || {}), 35 | validValue: null, 36 | }; 37 | 38 | componentWillUnmount(): void { 39 | this._unmounted = true; 40 | } 41 | 42 | render(): React.Element { 43 | const title = this.props.title || 'Document Error!'; 44 | const content = 45 | this.props.content || 46 | 'Unable to load the document. Have issues in Json format, please verify...'; 47 | return ( 48 |
49 | 50 | × 51 | 52 | {title} 53 | {content} 54 |
55 | ); 56 | } 57 | 58 | _cancel = (): void => { 59 | this.props.close(); 60 | }; 61 | } 62 | 63 | export default AlertInfo; 64 | -------------------------------------------------------------------------------- /.github/workflows/version-bump.yml: -------------------------------------------------------------------------------- 1 | # Manual action for incrementing the version 2 | 3 | name: Bump Version 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | type: 9 | description: 'Version Type' 10 | required: true 11 | default: 'patch' 12 | type: choice 13 | options: 14 | - patch 15 | - minor 16 | - major 17 | 18 | jobs: 19 | bump: 20 | runs-on: self-hosted 21 | 22 | strategy: 23 | matrix: 24 | node-version: [20.x] 25 | 26 | timeout-minutes: 60 # Default 5 days for self hosted 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-node@v4 30 | name: Use Node.js ${{ matrix.node-version }} 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | 34 | - name: Bump Version 35 | run: npm version ${{ inputs.type }} --no-commit-hooks --no-git-tag-version 36 | - name: Update Packages 37 | # update non-peer deps to latest in package.json 38 | # npm update to update all lock dependencies 39 | # npm i to fix any lock descrepencies from update 40 | run: | 41 | npx npm-check-updates --peer --target semver -u 42 | npm update --ignore-scripts 43 | npm i --ignore-scripts 44 | - name: Prepare Release Branch 45 | run: | 46 | TAG=v$(npm pkg get version --workspaces=false | tr -d \") 47 | git checkout -b "Release/${TAG}" 48 | git commit -a -m "Bump version to ${TAG}" 49 | git push -u origin "Release/${TAG}" 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | # PR must be done manually. PR with GITHUB_TOKEN won't trigger workflows. 53 | -------------------------------------------------------------------------------- /src/BlockquoteToggleCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { findParentNodeOfType } from 'prosemirror-utils'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import * as React from 'react'; 8 | import { BLOCKQUOTE } from './NodeNames'; 9 | import toggleBlockquote from './toggleBlockquote'; 10 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 11 | 12 | class BlockquoteToggleCommand extends UICommand { 13 | isActive = (state: EditorState): boolean => { 14 | const blockquote = state.schema.nodes[BLOCKQUOTE]; 15 | return !!(blockquote && findParentNodeOfType(blockquote)(state.selection)); 16 | }; 17 | 18 | execute = ( 19 | state: EditorState, 20 | dispatch: ?(tr: Transform) => void, 21 | view: ?EditorView 22 | ): boolean => { 23 | const { schema, selection } = state; 24 | const tr = toggleBlockquote(state.tr.setSelection(selection), schema); 25 | if (tr.docChanged) { 26 | dispatch && dispatch(tr); 27 | return true; 28 | } else { 29 | return false; 30 | } 31 | }; 32 | 33 | waitForUserInput = ( 34 | _state: EditorState, 35 | _dispatch: ?(tr: Transform) => void, 36 | _view: ?EditorView, 37 | _event: ?React.SyntheticEvent 38 | ): Promise => { 39 | return Promise.resolve(undefined); 40 | }; 41 | 42 | executeWithUserInput = ( 43 | _state: EditorState, 44 | _dispatch: ?(tr: Transform) => void, 45 | _view: ?EditorView, 46 | _inputs: ?string 47 | ): boolean => { 48 | return false; 49 | }; 50 | 51 | cancel(): void { 52 | return null; 53 | } 54 | } 55 | 56 | export default BlockquoteToggleCommand; 57 | -------------------------------------------------------------------------------- /src/ui/FontTypeCommandMenuButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import CommandMenuButton from './CommandMenuButton.js'; 4 | import { 5 | FontTypeCommand 6 | } from '@modusoperandi/licit-ui-commands'; 7 | import * as React from 'react'; 8 | import findActiveFontType, { 9 | FONT_TYPE_NAME_DEFAULT 10 | } from './findActiveFontType.js'; 11 | import { 12 | EditorState 13 | } from 'prosemirror-state'; 14 | import { 15 | EditorView 16 | } from 'prosemirror-view'; 17 | import { 18 | FONT_TYPE_NAMES 19 | } from '../FontTypeMarkSpec.js'; 20 | import { 21 | Transform 22 | } from 'prosemirror-transform'; 23 | 24 | const FONT_TYPE_COMMANDS: Object = { 25 | [FONT_TYPE_NAME_DEFAULT]: new FontTypeCommand(''), 26 | }; 27 | 28 | FONT_TYPE_NAMES.forEach((name) => { 29 | FONT_TYPE_COMMANDS[name] = new FontTypeCommand(name); 30 | }); 31 | 32 | const COMMAND_GROUPS = [FONT_TYPE_COMMANDS]; 33 | 34 | class FontTypeCommandMenuButton extends React.PureComponent { 35 | props: { 36 | dispatch: (tr: Transform) => void, 37 | editorState: EditorState, 38 | editorView: ?EditorView, 39 | }; 40 | 41 | render(): React.Element { 42 | const { dispatch, editorState, editorView } = this.props; 43 | const fontType = findActiveFontType(editorState); 44 | return ( 45 | 56 | ); 57 | } 58 | } 59 | 60 | export default FontTypeCommandMenuButton; 61 | -------------------------------------------------------------------------------- /src/createCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { EditorView } from 'prosemirror-view'; 6 | import * as React from 'react'; 7 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 8 | 9 | type ExecuteCall = ( 10 | state: EditorState, 11 | dispatch?: ?(tr: Transform) => void, 12 | view?: ?EditorView 13 | ) => boolean; 14 | 15 | export default function createCommand(execute: ExecuteCall): UICommand { 16 | class CustomCommand extends UICommand { 17 | isEnabled = (state: EditorState): boolean => { 18 | return this.execute(state); 19 | }; 20 | 21 | execute = ( 22 | state: EditorState, 23 | dispatch: ?(tr: Transform) => void, 24 | view: ?EditorView 25 | ): boolean => { 26 | const tr = state.tr; 27 | let endTr = tr; 28 | execute( 29 | state, 30 | (nextTr) => { 31 | endTr = nextTr; 32 | dispatch && dispatch(endTr); 33 | }, 34 | view 35 | ); 36 | return endTr.docChanged || tr !== endTr; 37 | }; 38 | 39 | waitForUserInput = ( 40 | _state: EditorState, 41 | _dispatch: ?(tr: Transform) => void, 42 | _view: ?EditorView, 43 | _event: ?React.SyntheticEvent 44 | ): Promise => { 45 | return Promise.resolve(undefined); 46 | }; 47 | 48 | executeWithUserInput = ( 49 | _state: EditorState, 50 | _dispatch: ?(tr: Transform) => void, 51 | _view: ?EditorView, 52 | _inputs: ?string 53 | ): boolean => { 54 | return false; 55 | }; 56 | 57 | cancel(): void { 58 | return null; 59 | } 60 | } 61 | return new CustomCommand(); 62 | } 63 | -------------------------------------------------------------------------------- /src/ui/findActiveFontSize.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { findParentNodeOfType } from 'prosemirror-utils'; 5 | 6 | import { MARK_FONT_SIZE } from '../MarkNames.js'; 7 | import { HEADING } from '../NodeNames.js'; 8 | import findActiveMark from '../findActiveMark.js'; 9 | 10 | // This should map to `--czi-content-font-size` at `czi-editor.css`. 11 | const FONT_PT_SIZE_DEFAULT = 11; 12 | 13 | // This should map to `czi-heading.css`. 14 | const MAP_HEADING_LEVEL_TO_FONT_PT_SIZE = { 15 | '1': 20, 16 | '2': 18, 17 | '3': 16, 18 | '4': 14, 19 | '5': 11, 20 | '6': 11, 21 | }; 22 | 23 | export default function findActiveFontSize(state: EditorState): string { 24 | const { schema, doc, selection, tr } = state; 25 | const markType = schema.marks[MARK_FONT_SIZE]; 26 | const heading = schema.nodes[HEADING]; 27 | const defaultSize = String(FONT_PT_SIZE_DEFAULT); 28 | if (!markType) { 29 | return defaultSize; 30 | } 31 | 32 | const { from, to, empty } = selection; 33 | if (empty) { 34 | const storedMarks = 35 | tr.storedMarks || 36 | state.storedMarks || 37 | selection.$cursor?.marks?.() || []; 38 | const sm = storedMarks.find((m) => m.type === markType); 39 | return sm ? String(sm.attrs.pt || defaultSize) : defaultSize; 40 | } 41 | 42 | const mark = findActiveMark(doc, from, to, markType); 43 | if (mark) { 44 | return String(mark.attrs.pt); 45 | } 46 | if (!heading) { 47 | return defaultSize; 48 | } 49 | const result = findParentNodeOfType(heading)(state.selection); 50 | if (!result) { 51 | return defaultSize; 52 | } 53 | const level = String(result.node.attrs.level); 54 | return MAP_HEADING_LEVEL_TO_FONT_PT_SIZE[level] || defaultSize; 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will attest and publish build artifacts ready for release 2 | 3 | name: publish 4 | 5 | on: 6 | push: 7 | branches: [main, master] 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest # GitHub hosted runner required for provenance 12 | permissions: 13 | contents: write # write for release 14 | id-token: write # required for provenance cert #https://docs.npmjs.com/generating-provenance-statements 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20.x] 19 | 20 | timeout-minutes: 30 # Default 5 days for self hosted 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | name: Use Node.js ${{ matrix.node-version }} 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | registry-url: 'https://registry.npmjs.org' 28 | 29 | # Ensure npm 11.5.1 or later is installed for secure auth 30 | - name: Update npm 31 | run: npm install -g npm@latest 32 | 33 | - run: npm ci --ignore-scripts 34 | 35 | - name: Build 36 | run: npm run ci:build 37 | 38 | - name: NPM Publish 39 | # Using trusted provider auth https://docs.npmjs.com/trusted-publishers#supported-cicd-providers 40 | run: npm publish ./dist --access public --provenance # provenance requires a github hosted runner 41 | 42 | - name: Set Version number 43 | run: echo "TAG=$(npm pkg get version --workspaces=false | tr -d \")" >> "$GITHUB_ENV" 44 | 45 | - name: Release 46 | # https://github.com/softprops/action-gh-release 47 | uses: softprops/action-gh-release@v2 48 | with: 49 | tag_name: ${{ env.TAG }} 50 | target_commitish: main 51 | generate_release_notes: true 52 | -------------------------------------------------------------------------------- /src/BulletListNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | 5 | import { ATTRIBUTE_LIST_STYLE_TYPE } from './ListItemNodeSpec.js'; 6 | import { LIST_ITEM } from './NodeNames.js'; 7 | import { ATTRIBUTE_INDENT, MIN_INDENT_LEVEL } from './ParagraphNodeSpec.js'; 8 | 9 | import type { NodeSpec } from './Types.js'; 10 | 11 | const AUTO_LIST_STYLE_TYPES = ['disc', 'square', 'circle']; 12 | 13 | const BulletListNodeSpec: NodeSpec = { 14 | attrs: { 15 | id: { default: null }, 16 | indent: { default: 0 }, 17 | listStyleType: { default: null }, 18 | }, 19 | group: 'block', 20 | content: LIST_ITEM + '+', 21 | parseDOM: [ 22 | { 23 | tag: 'ul', 24 | getAttrs(dom: HTMLElement) { 25 | const listStyleType = 26 | dom.getAttribute(ATTRIBUTE_LIST_STYLE_TYPE) || null; 27 | 28 | const indent = dom.hasAttribute(ATTRIBUTE_INDENT) 29 | ? parseInt(dom.getAttribute(ATTRIBUTE_INDENT), 10) 30 | : MIN_INDENT_LEVEL; 31 | return { 32 | indent, 33 | listStyleType, 34 | }; 35 | }, 36 | }, 37 | ], 38 | 39 | toDOM(node: Node) { 40 | const { indent, listStyleType } = node.attrs; 41 | const attrs = {}; 42 | // [FS] IRAD-947 2020-05-26 43 | // Bullet list type changing fix 44 | attrs[ATTRIBUTE_INDENT] = indent; 45 | if (listStyleType) { 46 | attrs[ATTRIBUTE_LIST_STYLE_TYPE] = listStyleType; 47 | } 48 | 49 | let htmlListStyleType = listStyleType; 50 | 51 | if (!htmlListStyleType || htmlListStyleType === 'disc') { 52 | htmlListStyleType = 53 | AUTO_LIST_STYLE_TYPES[indent % AUTO_LIST_STYLE_TYPES.length]; 54 | } 55 | 56 | attrs.type = htmlListStyleType; 57 | return ['ul', attrs, 0]; 58 | }, 59 | }; 60 | 61 | export default BulletListNodeSpec; 62 | -------------------------------------------------------------------------------- /src/ui/CustomRadioButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PointerSurface, preventEventDefault } from '@modusoperandi/licit-ui-commands'; 4 | import * as React from 'react'; 5 | import cx from 'classnames'; 6 | import uuid from './uuid.js'; 7 | 8 | import type { PointerSurfaceProps } from '@modusoperandi/licit-ui-commands'; 9 | 10 | class CustomRadioButton extends React.PureComponent { 11 | props: PointerSurfaceProps & { 12 | checked?: ?boolean, 13 | inline?: ?boolean, 14 | label?: string | React.Element | null, 15 | name?: ?string, 16 | onSelect?: ?(val: any, e: SyntheticEvent<>) => void, 17 | }; 18 | 19 | _name = uuid(); 20 | 21 | render(): React.Element { 22 | const { 23 | title, 24 | className, 25 | checked, 26 | label, 27 | inline, 28 | name, 29 | onSelect, 30 | disabled, 31 | ...pointerProps 32 | } = this.props; 33 | 34 | const klass = cx(className, 'czi-custom-radio-button', { 35 | checked: checked, 36 | inline: inline, 37 | }); 38 | 39 | return ( 40 | 47 | 56 | 57 | {label} 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default CustomRadioButton; 64 | -------------------------------------------------------------------------------- /src/ui/czi-icon.css: -------------------------------------------------------------------------------- 1 | .czi-icon { 2 | direction: ltr; 3 | display: inline-block; 4 | font-family: 'Material Icons', sans-serif !important; 5 | 6 | /* Support for IE. */ 7 | font-feature-settings: 'liga'; 8 | font-size: 20px !important; /* Preferred icon size */ 9 | 10 | /* Support for all WebKit browsers. */ 11 | -webkit-font-smoothing: subpixel-antialiased; 12 | font-style: normal; 13 | font-weight: normal; 14 | letter-spacing: normal; 15 | line-height: 1; 16 | 17 | /* Support for Firefox. */ 18 | -moz-osx-font-smoothing: grayscale; 19 | 20 | /* Support for Safari and Chrome. */ 21 | text-rendering: optimizeLegibility; 22 | text-transform: none; 23 | white-space: nowrap; 24 | word-wrap: normal; 25 | } 26 | 27 | .czi-icon.h1, 28 | .czi-icon.h2, 29 | .czi-icon.h3, 30 | .czi-icon.hn { 31 | font-family: 'Helvetica', 'Arial', sans-serif; 32 | font-size: 13px; 33 | text-transform: capitalize; 34 | } 35 | 36 | .czi-icon.hr { 37 | /* [FS] IRAD-1009 2020-07-16 38 | hr not grayed out on editor disable state 39 | Temporary Fix need to bring image instead of content */ 40 | content: '---'; 41 | height: 30px; 42 | margin-bottom: 0 !important; 43 | margin-left: 0 !important; 44 | margin-right: 0 !important; 45 | margin-top: -12px !important; 46 | width: 20px; 47 | } 48 | 49 | .czi-icon.hr::before { 50 | content: '---'; 51 | font-weight: bold; 52 | } 53 | 54 | .czi-icon.superscript, 55 | .czi-icon.subscript { 56 | display: inline-block; 57 | font-family: 'Arial', sans-serif !important; 58 | font-size: 12px !important; 59 | height: 16px; 60 | text-align: center; 61 | width: 20px; 62 | } 63 | 64 | .czi-icon .superscript-top { 65 | margin: 0 2px; 66 | vertical-align: super; 67 | } 68 | 69 | .czi-icon .subscript-bottom { 70 | margin: 0 2px; 71 | vertical-align: sub; 72 | } 73 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import './ui/czi-vars.css'; 2 | @import './ui/czi-heading.css'; 3 | @import './ui/czi-indent.css'; 4 | @import './ui/czi-list.css'; 5 | @import './ui/czi-table.css'; 6 | @import './ui/czi-bookmark-view.css'; 7 | @import './ui/czi-body-layout-editor.css'; 8 | @import './ui/czi-cursor-placeholder.css'; 9 | @import './ui/czi-custom-menu.css'; 10 | @import './ui/czi-custom-menu-button.css'; 11 | @import './ui/czi-custom-menu-item.css'; 12 | @import './ui/czi-custom-radio-button.css'; 13 | @import './ui/czi-custom-scrollbar.css'; 14 | @import './ui/czi-editor.css'; 15 | @import './ui/czi-editor-frameset.css'; 16 | @import './ui/czi-editor-toolbar.css'; 17 | @import './ui/czi-form.css'; 18 | @import './ui/czi-frag.css'; 19 | @import './ui/czi-icon.css'; 20 | @import './ui/czi-image-resize-box.css'; 21 | @import './ui/czi-image-upload-editor.css'; 22 | @import './ui/czi-image-upload-placeholder.css'; 23 | @import './ui/czi-image-url-editor.css'; 24 | @import './ui/czi-image-view.css'; 25 | @import './ui/czi-inline-editor.css'; 26 | @import './ui/czi-link-tooltip.css'; 27 | @import './ui/czi-loading-indicator.css'; 28 | @import './ui/czi-math-view.css'; 29 | @import './ui/czi-selection-placeholder.css'; 30 | @import './ui/czi-table-cell-menu.css'; 31 | @import './ui/czi-table-grid-size-editor.css'; 32 | @import './client/licit.css'; 33 | @import '@modusoperandi/licit-ui-commands/styles.css'; 34 | 35 | /* Now loaded locally, so that it work in closed network as well. */ 36 | @import './ui/fonts.css'; 37 | @import './ui/icon-font.css'; 38 | @import 'prosemirror-gapcursor/style/gapcursor.css'; 39 | @import 'prosemirror-view/style/prosemirror.css'; 40 | @import 'react-tooltip/dist/react-tooltip.css'; 41 | @import './ui/listType.css'; 42 | 43 | /** 44 | .ProseMirror { 45 | content-visibility: auto /* (browser) Only render visable content / 46 | } 47 | */ -------------------------------------------------------------------------------- /src/OverrideMarkSpec.js: -------------------------------------------------------------------------------- 1 | import type { MarkSpec } from './Types.js'; 2 | const OverrideMarkSpec: MarkSpec = { 3 | attrs: { 4 | strong: { default: false }, 5 | em: { default: false }, 6 | underline: { default: false }, 7 | strike: { default: false }, 8 | }, 9 | inline: true, 10 | group: 'inline', 11 | parseDOM: [ 12 | { 13 | tag: 'span', 14 | getAttrs: (dom) => { 15 | const strong = dom.getAttribute('cs-strong') === 'true'; 16 | const em = dom.getAttribute('cs-em') === 'true'; 17 | const underline = dom.getAttribute('cs-underline') === 'true'; 18 | const strike = dom.getAttribute('cs-strike') === 'true'; 19 | 20 | // Only create the mark if at least one attribute is true 21 | if (strong || em || underline || strike) { 22 | return { strong, em, underline, strike }; 23 | } 24 | 25 | return false; // Ignore spans where all attributes are false 26 | }, 27 | }, 28 | ], 29 | toDOM: (mark) => { 30 | // Only render the if at least one attribute is true 31 | if (mark.attrs.strong || mark.attrs.em || mark.attrs.underline || mark.attrs.strike) { 32 | return [ 33 | 'span', 34 | { 35 | 'cs-strong': mark.attrs.strong, 36 | 'cs-em': mark.attrs.em, 37 | 'cs-underline': mark.attrs.underline, 38 | 'cs-strike': mark.attrs.strike, 39 | }, 40 | 0, 41 | ]; 42 | } 43 | 44 | // If no attributes are true, return nothing (ProseMirror will ignore this) 45 | return null; 46 | }, 47 | }; 48 | 49 | export default OverrideMarkSpec; 50 | -------------------------------------------------------------------------------- /src/ui/FontSizeCommandMenuButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { EditorView } from 'prosemirror-view'; 6 | import * as React from 'react'; 7 | 8 | import {FontSizeCommand} from '@modusoperandi/licit-ui-commands'; 9 | import CommandMenuButton from './CommandMenuButton.js'; 10 | import findActiveFontSize from './findActiveFontSize.js'; 11 | 12 | export const FONT_PT_SIZES = [ 13 | 8, 14 | 9, 15 | 10, 16 | 11, 17 | 12, 18 | 14, 19 | 18, 20 | 24, 21 | 30, 22 | 36, 23 | 48, 24 | 60, 25 | 72, 26 | 90, 27 | ]; 28 | 29 | const FONT_PT_SIZE_COMMANDS = FONT_PT_SIZES.reduce((memo, size) => { 30 | memo[` ${size} `] = new FontSizeCommand(size); 31 | return memo; 32 | }, {}); 33 | 34 | const COMMAND_GROUPS = [ 35 | { Default: new FontSizeCommand(0) }, 36 | FONT_PT_SIZE_COMMANDS, 37 | ]; 38 | 39 | class FontSizeCommandMenuButton extends React.PureComponent { 40 | props: { 41 | dispatch: (tr: Transform) => void, 42 | editorState: EditorState, 43 | editorView: ?EditorView, 44 | }; 45 | 46 | render(): React.Element { 47 | const { dispatch, editorState, editorView } = this.props; 48 | const fontSize = findActiveFontSize(editorState); 49 | const className = String(fontSize).length <= 2 ? 'width-30' : 'width-60'; 50 | return ( 51 | 62 | ); 63 | } 64 | } 65 | 66 | export default FontSizeCommandMenuButton; 67 | -------------------------------------------------------------------------------- /src/client/http.js: -------------------------------------------------------------------------------- 1 | // A simple wrapper for XHR. 2 | export function req(conf) { 3 | const req = new XMLHttpRequest(), 4 | aborted = false; 5 | const result = new Promise((success, failure) => { 6 | req.open(conf.method, conf.url, true); 7 | req.addEventListener('load', () => { 8 | if (aborted) return; 9 | if (req.status < 400) { 10 | success(req.responseText); 11 | } else { 12 | const text = req.responseText; 13 | const err = new Error( 14 | 'Request failed: ' + req.statusText + (text ? '\n\n' + text : '') 15 | ); 16 | err.status = req.status; 17 | 18 | failure(err); 19 | } 20 | }); 21 | req.addEventListener('error', () => { 22 | if (!aborted) failure(new Error('Network error')); 23 | }); 24 | if (conf.headers) 25 | for (const header in conf.headers) 26 | req.setRequestHeader(header, conf.headers[header]); 27 | req.send(conf.body || null); 28 | }); 29 | result.abort = () => { 30 | if (!aborted) { 31 | req.abort(); 32 | aborted = true; 33 | } 34 | }; 35 | return result; 36 | } 37 | 38 | export function GET(url) { 39 | return req({ url, method: 'GET' }); 40 | } 41 | 42 | export function POST(url, body, type) { 43 | return req({ url, method: 'POST', body, headers: { 'Content-Type': type } }); 44 | } 45 | 46 | export function PUT(url, body, type) { 47 | return req({ url, method: 'PUT', body, headers: { 'Content-Type': type } }); 48 | } 49 | 50 | // [FS] IRAD-1128 2021-02-03 51 | // http DELETE request overrided 52 | export function DELETE(url, type) { 53 | return req({ url, method: 'DELETE', headers: { 'Content-Type': type } }); 54 | } 55 | 56 | // [FS] IRAD-1128 2021-02-03 57 | // http PATCH request overrided 58 | export function PATCH(url, body, type) { 59 | return req({ url, method: 'PATCH', body, headers: { 'Content-Type': type } }); 60 | } 61 | -------------------------------------------------------------------------------- /src/DocNodeSpec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import convertToCSSPTValue from './convertToCSSPTValue.js'; 4 | 5 | export const LAYOUT = { 6 | US_LETTER_LANDSCAPE: 'us_letter_landscape', 7 | US_LETTER_PORTRAIT: 'us_letter_portrait', 8 | A4_LANDSCAPE: 'a4_landscape', 9 | A4_PORTRAIT: 'a4_portrait', 10 | }; 11 | 12 | export const ATTRIBUTE_LAYOUT = 'data-layout'; 13 | 14 | export function getAttrs(el: HTMLElement): Object { 15 | const attrs: Object = { 16 | layout: null, 17 | width: null, 18 | padding: null, 19 | }; 20 | 21 | const { width, maxWidth, padding } = el.style || {}; 22 | const ww = convertToCSSPTValue(width) || convertToCSSPTValue(maxWidth); 23 | const pp = convertToCSSPTValue(padding); 24 | if (ww) { 25 | // 1pt = 1/72in 26 | // letter size: 8.5in x 11inch 27 | const ptWidth = ww + pp * 2; 28 | const inWidth = ptWidth / 72; 29 | const cmWidth = inWidth * 2.54; 30 | if (inWidth >= 10.9 && inWidth <= 11.1) { 31 | // Round up to letter size. 32 | attrs.layout = LAYOUT.US_LETTER_LANDSCAPE; 33 | } else if (inWidth >= 8.4 && inWidth <= 8.6) { 34 | // Round up to letter size. 35 | attrs.layout = LAYOUT.US_LETTER_PORTRAIT; 36 | } else if (cmWidth >= 29.5 && cmWidth <= 30.1) { 37 | attrs.layout = LAYOUT.A4_LANDSCAPE; 38 | } else if (cmWidth >= 20.5 && cmWidth <= 21.5) { 39 | attrs.layout = LAYOUT.A4_PORTRAIT; 40 | } else { 41 | attrs.width = ptWidth; 42 | if (pp) { 43 | attrs.padding = pp; 44 | } 45 | } 46 | } 47 | 48 | return attrs; 49 | } 50 | 51 | const DocNodeSpec = { 52 | attrs: { 53 | layout: { default: null }, 54 | padding: { default: null }, 55 | width: { default: null }, 56 | // [FS] IRAD-1202 2021-02-15 57 | // Counter flags for Numbering 58 | counterFlags: { default: null }, 59 | }, 60 | content: 'block+', 61 | }; 62 | export default DocNodeSpec; 63 | -------------------------------------------------------------------------------- /src/ui/czi-editor-frameset.css: -------------------------------------------------------------------------------- 1 | .czi-editor-frameset { 2 | position: relative; 3 | } 4 | 5 | .czi-editor-frame-main { 6 | position: relative; 7 | z-index: 0; 8 | } 9 | 10 | .czi-editor-frame-body { 11 | background-color: var(--czi-page-background-color); 12 | } 13 | 14 | .czi-editor-frameset.embedded .czi-editor-frame-body { 15 | background-color: unset; 16 | } 17 | 18 | .czi-editor-frame-body-scroll { 19 | box-sizing: border-box; 20 | padding-top: 20px; 21 | } 22 | 23 | .czi-editor-frameset.embedded .czi-editor-frame-body-scroll { 24 | padding-top: unset; 25 | } 26 | 27 | .czi-editor-frameset.with-fixed-layout .czi-editor-frame-main { 28 | display: flex; 29 | flex-direction: column; 30 | height: 100%; 31 | } 32 | 33 | .czi-editor-frame-head { 34 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.21); 35 | position: relative; 36 | z-index: 2; 37 | } 38 | 39 | .czi-editor-frameset.with-fixed-layout .czi-editor-frame-body { 40 | flex: 1; 41 | position: relative; 42 | z-index: 1; 43 | } 44 | 45 | .czi-editor-frameset.with-fixed-layout .czi-editor-frame-body-scroll { 46 | bottom: 0; 47 | left: 0; 48 | overflow: auto; 49 | position: absolute; 50 | right: 0; 51 | top: 0; 52 | } 53 | 54 | .czi-editor-frame-footer { 55 | display: flex; 56 | } 57 | 58 | @media only print { 59 | .czi-editor-frame-body { 60 | background-color: transparent; 61 | } 62 | 63 | .czi-editor-frameset.with-fixed-layout { 64 | height: auto !important; 65 | width: auto !important; 66 | } 67 | 68 | .czi-editor-frameset.with-fixed-layout .czi-editor-frameset { 69 | height: auto; 70 | } 71 | 72 | .czi-editor-frameset.with-fixed-layout .czi-editor-frame-body-scroll { 73 | position: relative; 74 | } 75 | 76 | .czi-editor-frameset.with-fixed-layout .czi-editor-frame-body { 77 | height: auto !important; 78 | max-height: 100% !important; 79 | overflow: visible; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/HTMLMutator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import uuid from './ui/uuid.js'; 4 | 5 | // Utility Class that allows developer to insert HTML snippets then updates 6 | // document's innerHTML accordingly. 7 | export default class HTMLMutator { 8 | _doc: Document; 9 | _htmls: Map; 10 | 11 | constructor(doc: Document) { 12 | this._doc = doc; 13 | this._htmls = new Map(); 14 | } 15 | 16 | insertHTMLBefore(html: string, el: Element): HTMLMutator { 17 | return this._insertHTML(html, 'before', el); 18 | } 19 | 20 | insertHTMLAfter(html: string, el: Element): HTMLMutator { 21 | return this._insertHTML(html, 'after', el); 22 | } 23 | 24 | execute(): void { 25 | const doc = this._doc; 26 | const root = doc?.body || doc?.documentElement; 27 | let newHtml = root.innerHTML; 28 | this._htmls.forEach((html, token) => { 29 | newHtml = newHtml.replace(token, html); 30 | }); 31 | root.innerHTML = newHtml; 32 | } 33 | 34 | _insertHTML( 35 | html: string, 36 | position: 'before' | 'after', 37 | el: Element 38 | ): HTMLMutator { 39 | if (el.ownerDocument !== this._doc) { 40 | throw new Error('element does not belong to the document'); 41 | } 42 | // This does not insert the HTML into the document directly. 43 | // Instead, this inserts a comment token that can be replaced by the HTML 44 | // later. 45 | const token = `\u200b_HTMLMutator_token_${uuid()}_\u200b`; 46 | const node = this._doc.createComment(token); 47 | const parentElement = el?.parentElement; 48 | if (position === 'before') { 49 | parentElement.insertBefore(node, el); 50 | } else if (position === 'after') { 51 | parentElement.insertBefore(node, el.nextSibling); 52 | } else { 53 | throw new Error(`Invalid position ${position}`); 54 | } 55 | this._htmls.set('', html); 56 | return this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ui/czi-custom-radio-button.css: -------------------------------------------------------------------------------- 1 | /* */ 2 | @import './czi-vars.css'; 3 | 4 | .czi-custom-radio-button { 5 | cursor: pointer; 6 | display: block; 7 | font-family: var(--czi-font-family); 8 | font-size: var(--czi-font-size); 9 | line-height: 25px; 10 | margin-bottom: 12px; 11 | margin-left: 0; 12 | margin-right: 12px; 13 | margin-top: 0; 14 | padding-top: 0; 15 | padding-right: 0; 16 | padding-bottom: 0; 17 | padding-left: 36px; 18 | position: relative; 19 | user-select: none; 20 | } 21 | 22 | .czi-custom-radio-button-input { 23 | height: 1px; 24 | left: 0; 25 | opacity: 0; 26 | pointer-events: none; 27 | position: absolute; 28 | top: 0; 29 | width: 1px; 30 | } 31 | 32 | .czi-custom-radio-button.inline { 33 | display: inline-block; 34 | margin-top: 0; 35 | margin-right: 12px; 36 | margin-bottom: 0; 37 | margin-left: 0; 38 | } 39 | 40 | .czi-custom-radio-button-icon { 41 | background-color: #eee; 42 | border-radius: 50%; 43 | height: 25px; 44 | left: 0; 45 | position: absolute; 46 | top: 0; 47 | width: 25px; 48 | } 49 | 50 | .czi-custom-radio-button-input:focus + .czi-custom-radio-button-icon, 51 | .czi-custom-radio-button:hover .czi-custom-radio-button-icon { 52 | background-color: #ccc; 53 | } 54 | 55 | .czi-custom-radio-button.checked .czi-custom-radio-button-icon { 56 | background-color: #3981e7; 57 | } 58 | 59 | .czi-custom-radio-button.checked .czi-custom-radio-button-icon::after { 60 | background: #fff; 61 | border-radius: 50%; 62 | content: ''; 63 | height: 8px; 64 | left: 9px; 65 | position: absolute; 66 | top: 9px; 67 | width: 8px; 68 | } 69 | 70 | .czi-custom-radio-button.disabled, 71 | .czi-custom-radio-button.disabled:hover { 72 | color: #dedede; 73 | cursor: unset; 74 | pointer-events: none; 75 | } 76 | 77 | .czi-custom-radio-button-label { 78 | display: inline-block; 79 | vertical-align: middle; 80 | } 81 | -------------------------------------------------------------------------------- /src/Types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react'; 4 | export type NodeSpec = { 5 | attrs?: ?{ [key: string]: any }, 6 | content?: ?string, 7 | draggable?: ?boolean, 8 | group?: ?string, 9 | inline?: ?boolean, 10 | name?: ?string, 11 | parseDOM?: ?Array, 12 | toDOM?: ?(node: any) => Array, 13 | }; 14 | 15 | export type MarkSpec = { 16 | attrs?: ?{ [key: string]: any }, 17 | name?: ?string, 18 | parseDOM: Array, 19 | toDOM: (node: any) => Array, 20 | }; 21 | 22 | export type RecentColor = { 23 | id: number, 24 | color: string 25 | }; 26 | 27 | export type EditorProps = { 28 | // TODO: Fill the interface. 29 | // https://github.com/ProseMirror/prosemirror-view/blob/master/src/index.js 30 | }; 31 | 32 | export type DirectEditorProps = EditorProps & { 33 | // TODO: Fill the interface. 34 | // https://github.com/ProseMirror/prosemirror-view/blob/master/src/index.js 35 | }; 36 | 37 | export type RenderCommentProps = { 38 | commentThreadId: string, 39 | isActive: boolean, 40 | requestCommentThreadDeletion: Function, 41 | requestCommentThreadReflow: Function, 42 | }; 43 | 44 | export type ImageLike = { 45 | height: number, 46 | id: string, 47 | src: string, 48 | width: number, 49 | }; 50 | 51 | 52 | 53 | 54 | 55 | 56 | export type EditorRuntime = { 57 | // Image Proxy 58 | canProxyImageSrc?: (src: string) => boolean, 59 | getProxyImageSrc?: (src: string) => string, 60 | 61 | // Image Upload 62 | canUploadImage?: () => boolean, 63 | uploadImage?: (obj: Blob) => Promise, 64 | 65 | // Comments 66 | canComment?: () => boolean, 67 | createCommentThreadID?: () => string, 68 | renderComment?: (props: RenderCommentProps) => ?React.Element, 69 | 70 | // External HTML 71 | canLoadHTML?: () => boolean, 72 | loadHTML?: () => Promise, 73 | 74 | }; 75 | export type EditorState = any; 76 | 77 | export const INNER_LINK = 'INNER______LINK'; 78 | -------------------------------------------------------------------------------- /src/ui/czi-vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --czi-blockquote-background-color: rgb(0 0 0 / 5%); 3 | --czi-blockquote-border: solid 8px rgb(0 0 0 / 60%); 4 | --czi-blockquote-color: #555; 5 | --czi-blockquote-font-family: 'Open Sans'; 6 | --czi-border-blue: solid 1px #5882eb; 7 | --czi-border-grey: solid 1px #ccc; 8 | --czi-border-red: solid 1px #f00; 9 | --czi-button-hover-background-color: rgb(0 0 0 / 50%); 10 | --czi-button-radius: 5px; 11 | --czi-button-text-color: #666; 12 | --czi-color-grey-200: rgb(0 0 0 / 46%); 13 | --czi-font-family: helvetica; 14 | --czi-font-size-small: 12px; 15 | --czi-font-size: 13px; 16 | --czi-link-color: rgb(0 0 238); 17 | --czi-overlay-radius: 3px; 18 | --czi-overlay-shadow: 0 1px 2px rgb(0 0 0 / 46%); 19 | --czi-page-background-color: #e6e6e6; 20 | --czi-placeholder-text-color: #dedede; 21 | --czi-selection-highlight-color-dark: rgb(152 204 253 / 80%); 22 | --czi-selection-highlight-color: rgb(152 204 253 / 40%); 23 | --czi-table-header-background-color: rgb(0 0 0 / 5%); 24 | --czi-table-border-color: #000; 25 | --czi-doc-padding-default: 14.5mm; 26 | --czi-doc-width-us-letter-portrait: 216mm; 27 | --czi-doc-width-us-letter-landscape: 279mm; 28 | --czi-doc-width-a4-portrait: 210mm; 29 | --czi-doc-width-a4-landscape: 297mm; 30 | --czi-doc-width-us-desktop-screen-4-3: 120vh; 31 | --czi-doc-width-us-desktop-screen-16-9: 142vh; 32 | 33 | /* content styles */ 34 | --czi-content-body-background-color: #fff; 35 | --czi-content-font-color: #000; 36 | --czi-content-font-family: arial; 37 | --czi-content-font-size-h1: 18px; 38 | --czi-content-font-size-h2: 16px; 39 | --czi-content-font-size-h3: 13px; 40 | --czi-content-font-size: 11pt; 41 | 42 | /* This maps to the default line spacing value of 1.5 in Google Doc */ 43 | --czi-content-line-height: 138%; 44 | --czi-content-link-color: rgb(0 0 238); 45 | /* hanging indent */ 46 | --hangingIndentMargin: 250px; 47 | } 48 | -------------------------------------------------------------------------------- /src/MarksClearCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState, AllSelection, TextSelection } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { EditorView } from 'prosemirror-view'; 6 | import * as React from 'react'; 7 | import { clearMarks, clearHeading } from '@modusoperandi/licit-ui-commands'; 8 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 9 | import { CellSelection } from 'prosemirror-tables'; 10 | 11 | class MarksClearCommand extends UICommand { 12 | isActive = (state: EditorState): boolean => { 13 | return false; 14 | }; 15 | 16 | isEnabled = (state: EditorState) => { 17 | const { selection } = state; 18 | return ( 19 | !selection.empty && 20 | (selection instanceof TextSelection || selection instanceof AllSelection || selection instanceof CellSelection) 21 | ); 22 | }; 23 | 24 | execute = ( 25 | state: EditorState, 26 | dispatch: ?(tr: Transform) => void, 27 | view: ?EditorView 28 | ): boolean => { 29 | let tr = clearMarks(state.tr.setSelection(state.selection), state.schema); 30 | // [FS] IRAD-948 2021-02-22 31 | 32 | // Clear Header formatting 33 | tr = clearHeading(tr, state.schema); 34 | 35 | if (dispatch && tr.docChanged) { 36 | dispatch(tr); 37 | return true; 38 | } 39 | return false; 40 | }; 41 | 42 | waitForUserInput = ( 43 | _state: EditorState, 44 | _dispatch: ?(tr: Transform) => void, 45 | _view: ?EditorView, 46 | _event: ?React.SyntheticEvent 47 | ): Promise => { 48 | return Promise.resolve(undefined); 49 | }; 50 | 51 | executeWithUserInput = ( 52 | _state: EditorState, 53 | _dispatch: ?(tr: Transform) => void, 54 | _view: ?EditorView, 55 | _inputs: ?string 56 | ): boolean => { 57 | return false; 58 | }; 59 | 60 | cancel(): void { 61 | return null; 62 | } 63 | 64 | } 65 | 66 | export default MarksClearCommand; 67 | -------------------------------------------------------------------------------- /src/EditorNodes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Schema } from 'prosemirror-model'; 4 | import BlockquoteNodeSpec from './BlockquoteNodeSpec.js'; 5 | import BookmarkNodeSpec from './BookmarkNodeSpec.js'; 6 | import BulletListNodeSpec from './BulletListNodeSpec.js'; 7 | import DocNodeSpec from './DocNodeSpec.js'; 8 | import HardBreakNodeSpec from './HardBreakNodeSpec.js'; 9 | import HorizontalRuleNodeSpec from './HorizontalRuleNodeSpec.js'; 10 | import ListItemNodeSpec from './ListItemNodeSpec.js'; 11 | import * as NodeNames from './NodeNames.js'; 12 | import OrderedListNodeSpec from './OrderedListNodeSpec.js'; 13 | import ParagraphNodeSpec from './ParagraphNodeSpec.js'; 14 | import TableNodesSpecs from './TableNodesSpecs.js'; 15 | import TextNodeSpec from './TextNodeSpec.js'; 16 | 17 | const { 18 | BLOCKQUOTE, 19 | BOOKMARK, 20 | BULLET_LIST, 21 | //CODE_BLOCK, 22 | DOC, 23 | HARD_BREAK, 24 | HEADING, 25 | HORIZONTAL_RULE, 26 | LIST_ITEM, 27 | ORDERED_LIST, 28 | PARAGRAPH, 29 | TEXT, 30 | } = NodeNames; 31 | 32 | // https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js 33 | 34 | // !! Be careful with the order of these nodes, which may effect the parsing 35 | // outcome.!! 36 | // [FS-SEA][06-04-2023] 37 | // Changed HeadingNodeSpec to ParagraphNodeSpec for HEADING to handle Header as a paragraph 38 | const nodes = { 39 | [DOC]: DocNodeSpec, 40 | [PARAGRAPH]: ParagraphNodeSpec, 41 | [BLOCKQUOTE]: BlockquoteNodeSpec, 42 | [HORIZONTAL_RULE]: HorizontalRuleNodeSpec, 43 | [HEADING]: ParagraphNodeSpec, 44 | [TEXT]: TextNodeSpec, 45 | [HARD_BREAK]: HardBreakNodeSpec, 46 | [BULLET_LIST]: BulletListNodeSpec, 47 | [ORDERED_LIST]: OrderedListNodeSpec, 48 | [LIST_ITEM]: ListItemNodeSpec, 49 | [BOOKMARK]: BookmarkNodeSpec, 50 | }; 51 | 52 | const marks = {}; 53 | const schema = new Schema({ nodes, marks }); 54 | const EditorNodes = schema.spec.nodes.append(TableNodesSpecs); 55 | export default EditorNodes; 56 | -------------------------------------------------------------------------------- /src/client/SimpleConnector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { Schema } from 'prosemirror-model'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import ReactDOM from 'react-dom'; 8 | 9 | export type SetStateCall = ( 10 | state: { editorState: EditorState }, 11 | callback: Function 12 | ) => void; 13 | 14 | class SimpleConnector { 15 | _setState: SetStateCall; 16 | _editorState: EditorState; 17 | // This flag is used to deteremine if data passed in or not 18 | // If not passed in, use the data from collab server when in collab mode. 19 | // else use empty content. 20 | _dataDefined: boolean; 21 | 22 | constructor(editorState: EditorState, setState: SetStateCall) { 23 | this._editorState = editorState; 24 | this._setState = setState; 25 | } 26 | 27 | onEdit = (transaction: Transform, view: EditorView): void => { 28 | ReactDOM.unstable_batchedUpdates(() => { 29 | const editorState = this._editorState.apply(transaction); 30 | // [FS] IRAD-1236 2020-03-05 31 | // The state property should not be directly mutated. Use the updateState method. 32 | if (view) { 33 | view.updateState(editorState); 34 | } 35 | 36 | const state = { 37 | editorState, 38 | data: transaction.doc.toJSON(), 39 | }; 40 | this._setState(state, () => { 41 | this._editorState = editorState; 42 | }); 43 | }); 44 | }; 45 | 46 | // FS IRAD-989 2020-18-06 47 | // updating properties should automatically render the changes 48 | getState = (): EditorState => { 49 | return this._editorState; 50 | }; 51 | 52 | // FS IRAD-1040 2020-09-02 53 | // Send the modified schema to server 54 | updateSchema = (schema: Schema, data: any) => {}; 55 | 56 | updateContent = (data: any) => {}; 57 | 58 | cleanUp = () => {}; 59 | } 60 | 61 | export default SimpleConnector; 62 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import react from 'eslint-plugin-react'; 2 | import babelParser from '@babel/eslint-parser'; 3 | import jest from 'eslint-plugin-jest'; 4 | import globals from 'globals'; 5 | 6 | export default [ 7 | { 8 | plugins: { 9 | react, 10 | }, 11 | 12 | languageOptions: { 13 | globals: { 14 | ...globals.jest, 15 | ...globals.browser, 16 | SyntheticEvent: false, 17 | SyntheticInputEvent: false, 18 | $ReadOnlyArray: false, 19 | }, 20 | 21 | parser: babelParser, 22 | ecmaVersion: 13, 23 | sourceType: 'module', 24 | 25 | parserOptions: { 26 | allowImportExportEverywhere: false, 27 | codeFrame: true, 28 | 29 | ecmaFeatures: { 30 | jsx: true, 31 | }, 32 | sourceType: 'module', 33 | }, 34 | }, 35 | 36 | rules: { 37 | 'react/jsx-sort-props': 'error', 38 | 'react/jsx-uses-react': 'error', 39 | 'react/jsx-uses-vars': 'error', 40 | 'consistent-return': 'error', 41 | 'no-debugger': 'error', 42 | 'no-invalid-regexp': 'error', 43 | 'no-mixed-spaces-and-tabs': 'error', 44 | 'no-trailing-spaces': 'error', 45 | 'no-undef': 'error', 46 | 47 | 'no-unused-vars': [ 48 | 'error', 49 | { 50 | vars: 'all', 51 | args: 'none', 52 | ignoreRestSiblings: false, 53 | }, 54 | ], 55 | 56 | 'no-var': 'error', 57 | 'prefer-const': 'error', 58 | 59 | quotes: [ 60 | 2, 61 | 'single', 62 | { 63 | avoidEscape: true, 64 | }, 65 | ], 66 | 67 | semi: [2, 'always'], 68 | strict: 0, 69 | }, 70 | }, 71 | { 72 | files: ['**/*.test.js'], 73 | 74 | plugins: { 75 | jest, 76 | }, 77 | 78 | languageOptions: { 79 | globals: { 80 | ...jest.environments.globals.globals, 81 | }, 82 | }, 83 | }, 84 | ]; 85 | -------------------------------------------------------------------------------- /src/ui/BookmarkNodeView.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | import { Decoration } from 'prosemirror-view'; 5 | import * as React from 'react'; 6 | 7 | import { 8 | ATTRIBUTE_BOOKMARK_ID, 9 | ATTRIBUTE_BOOKMARK_VISIBLE, 10 | } from './../BookmarkNodeSpec.js'; 11 | import CustomNodeView from './CustomNodeView.js'; 12 | import Icon from './Icon.js'; 13 | 14 | import type { NodeViewProps } from './CustomNodeView.js'; 15 | 16 | class BookmarkViewBody extends React.PureComponent { 17 | props: NodeViewProps; 18 | 19 | render(): React.Element { 20 | const { id, visible } = this.props.node.attrs; 21 | const icon = id && visible ? Icon.get('bookmark') : null; 22 | return {icon}; 23 | } 24 | 25 | _onClick = (e: SyntheticEvent<>): void => { 26 | e.preventDefault(); 27 | const { id } = this.props.node.attrs; 28 | const hash = '#' + id; 29 | if (window.location.hash !== hash) { 30 | window.location.hash = hash; 31 | } 32 | }; 33 | } 34 | 35 | class BookmarkNodeView extends CustomNodeView { 36 | // @override 37 | createDOMElement(): HTMLElement { 38 | const el = document.createElement('a'); 39 | el.className = 'czi-bookmark-view'; 40 | this._updateDOM(el); 41 | return el; 42 | } 43 | 44 | // @override 45 | update(node: Node, decorations: Array): boolean { 46 | super.update(node, decorations); 47 | return true; 48 | } 49 | 50 | // @override 51 | renderReactComponent(): React.Element { 52 | return ; 53 | } 54 | 55 | _updateDOM(el: HTMLElement): void { 56 | const { id, visible } = this.props.node.attrs; 57 | el.setAttribute('id', id); 58 | el.setAttribute('title', id); 59 | el.setAttribute(ATTRIBUTE_BOOKMARK_ID, id); 60 | visible && el.setAttribute(ATTRIBUTE_BOOKMARK_VISIBLE, 'true'); 61 | } 62 | } 63 | 64 | export default BookmarkNodeView; 65 | -------------------------------------------------------------------------------- /src/CodeBlockCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { findParentNodeOfType } from 'prosemirror-utils'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import * as React from 'react'; 8 | import { CODE_BLOCK } from './NodeNames'; 9 | import { noop } from '@modusoperandi/licit-ui-commands'; 10 | import toggleCodeBlock from './toggleCodeBlock'; 11 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 12 | 13 | class CodeBlockCommand extends UICommand { 14 | isActive = (state: EditorState): boolean => { 15 | const result = this._findCodeBlock(state); 16 | return !!result?.node; 17 | }; 18 | 19 | execute = ( 20 | state: EditorState, 21 | dispatch: ?(tr: Transform) => void, 22 | view: ?EditorView 23 | ): boolean => { 24 | const { selection, schema } = state; 25 | let { tr } = state; 26 | tr = tr.setSelection(selection); 27 | tr = toggleCodeBlock(tr, schema); 28 | if (tr.docChanged) { 29 | dispatch && dispatch(tr); 30 | return true; 31 | } else { 32 | return false; 33 | } 34 | }; 35 | 36 | _findCodeBlock(state: EditorState): ?Object { 37 | const codeBlock = state.schema.nodes[CODE_BLOCK]; 38 | const findCodeBlock = codeBlock ? findParentNodeOfType(codeBlock) : noop; 39 | return findCodeBlock(state.selection); 40 | } 41 | 42 | waitForUserInput = ( 43 | _state: EditorState, 44 | _dispatch: ?(tr: Transform) => void, 45 | _view: ?EditorView, 46 | _event: ?React.SyntheticEvent 47 | ): Promise => { 48 | return Promise.resolve(undefined); 49 | }; 50 | 51 | executeWithUserInput = ( 52 | _state: EditorState, 53 | _dispatch: ?(tr: Transform) => void, 54 | _view: ?EditorView, 55 | _inputs: ?string 56 | ): boolean => { 57 | return false; 58 | }; 59 | 60 | cancel(): void { 61 | return null; 62 | } 63 | } 64 | 65 | export default CodeBlockCommand; 66 | -------------------------------------------------------------------------------- /src/HorizontalRuleCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Fragment, Schema } from 'prosemirror-model'; 4 | import { EditorState } from 'prosemirror-state'; 5 | import { Transform } from 'prosemirror-transform'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import * as React from 'react'; 8 | import { HORIZONTAL_RULE } from './NodeNames.js'; 9 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 10 | 11 | function insertHorizontalRule(tr: Transform, schema: Schema): Transform { 12 | const { selection } = tr; 13 | if (!selection) { 14 | return tr; 15 | } 16 | const { from, to } = selection; 17 | if (from !== to) { 18 | return tr; 19 | } 20 | 21 | const horizontalRule = schema.nodes[HORIZONTAL_RULE]; 22 | if (!horizontalRule) { 23 | return tr; 24 | } 25 | 26 | const node = horizontalRule.create({}, null, null); 27 | const frag = Fragment.from(node); 28 | tr = tr.insert(from, frag); 29 | return tr; 30 | } 31 | 32 | class HorizontalRuleCommand extends UICommand { 33 | execute = ( 34 | state: EditorState, 35 | dispatch: ?(tr: Transform) => void, 36 | view: ?EditorView 37 | ): boolean => { 38 | const { selection, schema } = state; 39 | const tr = insertHorizontalRule(state.tr.setSelection(selection), schema); 40 | if (tr.docChanged) { 41 | dispatch && dispatch(tr); 42 | return true; 43 | } else { 44 | return false; 45 | } 46 | }; 47 | 48 | waitForUserInput = ( 49 | _state: EditorState, 50 | _dispatch: ?(tr: Transform) => void, 51 | _view: ?EditorView, 52 | _event: ?React.SyntheticEvent 53 | ): Promise => { 54 | return Promise.resolve(undefined); 55 | }; 56 | 57 | executeWithUserInput = ( 58 | _state: EditorState, 59 | _dispatch: ?(tr: Transform) => void, 60 | _view: ?EditorView, 61 | _inputs: ?string 62 | ): boolean => { 63 | return false; 64 | }; 65 | 66 | cancel(): void { 67 | return null; 68 | } 69 | } 70 | 71 | export default HorizontalRuleCommand; 72 | -------------------------------------------------------------------------------- /src/ui/CommandButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { EditorView } from 'prosemirror-view'; 6 | import * as React from 'react'; 7 | 8 | import { CustomButton } from '@modusoperandi/licit-ui-commands'; 9 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 10 | 11 | class CommandButton extends React.PureComponent { 12 | props: { 13 | className?: ?string, 14 | command: UICommand, 15 | disabled?: ?boolean, 16 | dispatch: (tr: Transform) => void, 17 | editorState: EditorState, 18 | editorView: ?EditorView, 19 | icon?: string | React.Element | null, 20 | label?: string | React.Element | null, 21 | title?: ?string, 22 | }; 23 | 24 | render(): React.Element { 25 | const { 26 | label, 27 | className, 28 | command, 29 | editorState, 30 | icon, 31 | title, 32 | editorView, 33 | } = this.props; 34 | let disabled = this.props.disabled; 35 | if (!!disabled === false) { 36 | disabled = !editorView || !command.isEnabled(editorState, editorView, ''); 37 | } 38 | return ( 39 | 50 | ); 51 | } 52 | 53 | _onUIEnter = ( 54 | command: UICommand, 55 | event: SyntheticEvent 56 | ): void => { 57 | if (command.shouldRespondToUIEvent(event)) { 58 | this._execute(command, event); 59 | } 60 | }; 61 | 62 | _execute = (value: any, event: SyntheticEvent): void => { 63 | const { command, editorState, dispatch, editorView } = this.props; 64 | command.execute(editorState, dispatch, editorView, event); 65 | }; 66 | } 67 | 68 | export default CommandButton; 69 | -------------------------------------------------------------------------------- /licit/server/collab/route.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {parse} from 'url'; 4 | 5 | // A URL router for the server. 6 | class Router { 7 | // fix_flow_errors: declarion to avoid flow errors 8 | routes = []; 9 | // end 10 | constructor() { 11 | this.routes = []; 12 | } 13 | 14 | add(method: any, url: any, handler: any) { 15 | this.routes.push({method, url, handler}); 16 | } 17 | 18 | matchEx(pattern: any, path: any) { 19 | const parts = path.slice(1).split('/'); 20 | if (parts.length && !parts[parts.length - 1]) parts.pop(); 21 | if (parts.length != pattern.length) return null; 22 | const result = []; 23 | for (let i = 0; i < parts.length; i++) { 24 | const pat = pattern[i]; 25 | if (pat) { 26 | if (pat != parts[i]) return null; 27 | } else { 28 | result.push(parts[i]); 29 | } 30 | } 31 | return result; 32 | } 33 | 34 | // : (union, string) → union 35 | // Check whether a route pattern matches a given URL path. 36 | match(pattern: any, path: any) { 37 | let result; 38 | if (typeof pattern == 'string') { 39 | if (pattern == path) result = []; 40 | } else if (pattern instanceof RegExp) { 41 | const match = pattern.exec(path); 42 | if (match) { 43 | result = match.slice(1); 44 | } 45 | } else { 46 | result = this.matchEx(pattern, path); 47 | } 48 | return result; 49 | } 50 | 51 | // Resolve a request, letting the matching route write a response. 52 | resolve(request: any, response: any) { 53 | const parsed = parse(request.url, true); 54 | const path = parsed.pathname; 55 | request.query = parsed.query; 56 | 57 | return this.routes.some((route) => { 58 | const match = 59 | route.method == request.method && this.match(route.url, path); 60 | if (!match) return false; 61 | 62 | const urlParts = match.map(decodeURIComponent); 63 | route.handler(request, response, ...urlParts); 64 | return true; 65 | }); 66 | } 67 | } 68 | 69 | export default Router; 70 | -------------------------------------------------------------------------------- /src/BlockquoteInsertNewLineCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Fragment, Schema } from 'prosemirror-model'; 4 | import { EditorState, TextSelection } from 'prosemirror-state'; 5 | import { Transform } from 'prosemirror-transform'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import * as React from 'react'; 8 | import { HARD_BREAK } from './NodeNames'; 9 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 10 | 11 | // This handles the case when user press SHIFT + ENTER key to insert a new line 12 | // into blockquote. 13 | function insertNewLine(tr: Transform, schema: Schema): Transform { 14 | const { selection } = tr; 15 | if (!selection) { 16 | return tr; 17 | } 18 | const { from, empty } = selection; 19 | if (!empty) { 20 | return tr; 21 | } 22 | const br = schema.nodes[HARD_BREAK]; 23 | if (!br) { 24 | return tr; 25 | } 26 | tr = tr.insert(from, Fragment.from(br.create())); 27 | tr = tr.setSelection(TextSelection.create(tr.doc, from + 1, from + 1)); 28 | return tr; 29 | } 30 | 31 | class BlockquoteInsertNewLineCommand extends UICommand { 32 | execute = ( 33 | state: EditorState, 34 | dispatch: ?(tr: Transform) => void, 35 | view: ?EditorView 36 | ): boolean => { 37 | const { schema, selection } = state; 38 | const tr = insertNewLine(state.tr.setSelection(selection), schema); 39 | if (tr.docChanged) { 40 | dispatch && dispatch(tr); 41 | return true; 42 | } else { 43 | return false; 44 | } 45 | }; 46 | 47 | waitForUserInput = ( 48 | _state: EditorState, 49 | _dispatch: ?(tr: Transform) => void, 50 | _view: ?EditorView, 51 | _event: ?React.SyntheticEvent 52 | ): Promise => { 53 | return Promise.resolve(undefined); 54 | }; 55 | 56 | executeWithUserInput = ( 57 | _state: EditorState, 58 | _dispatch: ?(tr: Transform) => void, 59 | _view: ?EditorView, 60 | _inputs: ?string 61 | ): boolean => { 62 | return false; 63 | }; 64 | 65 | cancel(): void { 66 | return null; 67 | } 68 | } 69 | 70 | export default BlockquoteInsertNewLineCommand; 71 | -------------------------------------------------------------------------------- /src/buildEditorPlugins.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { baseKeymap } from 'prosemirror-commands'; 3 | import { dropCursor } from 'prosemirror-dropcursor'; 4 | import { gapCursor } from 'prosemirror-gapcursor'; 5 | import { history } from 'prosemirror-history'; 6 | import { keymap } from 'prosemirror-keymap'; 7 | import { Schema } from 'prosemirror-model'; 8 | import { Plugin, PluginKey } from 'prosemirror-state'; 9 | import ContentPlaceholderPlugin from './ContentPlaceholderPlugin.js'; 10 | import CursorPlaceholderPlugin from './CursorPlaceholderPlugin.js'; 11 | import EditorPageLayoutPlugin from './EditorPageLayoutPlugin.js'; 12 | import LinkTooltipPlugin from './LinkTooltipPlugin.js'; 13 | import SelectionPlaceholderPlugin from './SelectionPlaceholderPlugin.js'; 14 | import TablePlugins from './TablePlugins.js'; 15 | import buildInputRules from './buildInputRules.js'; 16 | import createEditorKeyMap from './createEditorKeyMap.js'; 17 | // Creates the default plugin for the editor. 18 | export default class DefaultEditorPlugins { 19 | plugins: Array; 20 | 21 | constructor(schema: Schema) { 22 | this.plugins = [ 23 | new ContentPlaceholderPlugin(), 24 | new CursorPlaceholderPlugin(), 25 | new EditorPageLayoutPlugin(), 26 | new LinkTooltipPlugin(), 27 | new SelectionPlaceholderPlugin(), 28 | this.setPluginKey(buildInputRules(schema), 'InputRules'), 29 | this.setPluginKey(dropCursor(), 'DropCursor'), 30 | this.setPluginKey(gapCursor(), 'GapCursor'), 31 | history(), 32 | this.setPluginKey(keymap(createEditorKeyMap()), 'EditorKeyMap'), 33 | this.setPluginKey(keymap(baseKeymap), 'BaseKeymap'), 34 | ].concat(TablePlugins); 35 | } 36 | // [FS] IRAD-1005 2020-07-07 37 | // Upgrade outdated packages. 38 | // set plugin keys so that to avoid duplicate key error when keys are assigned automatically. 39 | setPluginKey(plugin: Plugin, key: string) { 40 | plugin.spec.key = new PluginKey(key + 'Plugin'); 41 | plugin.key = plugin.spec.key.key; 42 | return plugin; 43 | } 44 | 45 | get(): Array { 46 | return this.plugins; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/czi-table.css: -------------------------------------------------------------------------------- 1 | @import './czi-vars.css'; 2 | 3 | /* Table Styles */ 4 | 5 | .ProseMirror .tableWrapper { 6 | overflow-x: auto; 7 | } 8 | .ProseMirror .tableWrapper::-webkit-scrollbar{ 9 | height: 12px; 10 | } 11 | .ProseMirror table { 12 | border: 1px solid var(--czi-table-border-color); 13 | border-collapse:collapse; 14 | border-spacing: 0; 15 | border-width: 0 thin thin 0; 16 | margin: 0; 17 | overflow: hidden; 18 | page-break-inside: avoid; 19 | table-layout: fixed; 20 | width: 100%; 21 | } 22 | 23 | .ProseMirror td, 24 | .ProseMirror th { 25 | background-color: #fff; 26 | border: 1px solid var(--czi-table-border-color); 27 | border-width: thin 0 0 thin; 28 | box-sizing: border-box; 29 | min-width: 1em; 30 | padding: 8px; 31 | position: relative; 32 | vertical-align: top; 33 | } 34 | 35 | .ProseMirror th { 36 | background-color: var(--czi-table-header-background-color); 37 | font-weight: bold; 38 | text-align: left; 39 | } 40 | 41 | .ProseMirror .column-resize-handle { 42 | background-color: #adf; 43 | border: solid 1px #fff; 44 | border-width: 0 1px; 45 | bottom: 0; 46 | position: absolute; 47 | right: -2px; 48 | top: 0; 49 | width: 4px; 50 | z-index: 20; 51 | } 52 | 53 | .ProseMirror th:last-child > .column-resize-handle, 54 | .ProseMirror td:last-child > .column-resize-handle { 55 | right: 0; 56 | } 57 | 58 | .ProseMirror .column-resize-handle.for-margin-left.for-margin-left { 59 | left: 0; 60 | right: auto; 61 | } 62 | 63 | /* [FS] IRAD-949 2020-05-27 64 | Rezie cursor position issue fixed. */ 65 | .ProseMirror .column-resize-handle:focus, 66 | .ProseMirror .column-resize-handle:hover { 67 | cursor: ew-resize; 68 | cursor: col-resize; 69 | } 70 | 71 | /* Give selected cells a blue overlay */ 72 | .ProseMirror .selectedCell::after { 73 | background: var(--czi-selection-highlight-color); 74 | bottom: 0; 75 | content: ''; 76 | left: 0; 77 | pointer-events: none; 78 | position: absolute; 79 | right: 0; 80 | top: 0; 81 | z-index: 2; 82 | } 83 | 84 | 85 | @media only print { 86 | .ProseMirror table { 87 | width: 100% !important; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/patchMathElements.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default function patchMathElements(doc: Document): void { 4 | Array.from(doc.querySelectorAll('img')).forEach(patchGoogleEquationElement); 5 | } 6 | 7 | // See https://developers.google.com/chart/image/docs/chart_params#gcharts_cht 8 | const PARAM_CHART_CHART_TYPE = 'cht'; 9 | const PARAM_CHART_LABEL = 'chl'; 10 | 11 | // Google Doc exports math equation content as single image element that loads 12 | // its content from google. For example: 13 | // 14 | // Unfortunately, such image often fails to load because its url contains the 15 | // value that the Google Chart API does not support. 16 | // The workaround is to use KaTex (https://katex.org/) whoch supports a broader 17 | // set of characters that can be safely converted into math quations. 18 | 19 | function patchGoogleEquationElement(el: HTMLElement): void { 20 | const { ownerDocument, parentElement } = el; 21 | if (!ownerDocument || !parentElement) { 22 | return; 23 | } 24 | const src = el.getAttribute('src'); 25 | const content = getGoogleEquationContent(src); 26 | if (!content) { 27 | return; 28 | } 29 | 30 | // Replace `` with ``. 31 | // Note that this requires the schema to support `MathNodeSpec`. 32 | const math = ownerDocument.createElement('math'); 33 | math.setAttribute('data-latex', content); 34 | parentElement.insertBefore(math, el); 35 | parentElement.removeChild(el); 36 | } 37 | 38 | function getGoogleEquationContent(src: ?string): ?string { 39 | if (!src) { 40 | return null; 41 | } 42 | const { host, pathname, query } = new URL(src); 43 | if (host !== 'www.google.com' || pathname !== '/chart') { 44 | return null; 45 | } 46 | 47 | const params = new URL(query); 48 | const chartType = params[PARAM_CHART_CHART_TYPE]; 49 | const label = params[PARAM_CHART_LABEL]; 50 | 51 | // Google exports math equation as a special chart with plan text only 52 | // contents. 53 | if (chartType !== 'tx' || !label) { 54 | return null; 55 | } 56 | 57 | return label; 58 | } 59 | -------------------------------------------------------------------------------- /src/EditorPageLayoutPlugin.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState, Plugin, PluginKey } from 'prosemirror-state'; 4 | 5 | import { ATTRIBUTE_LAYOUT, LAYOUT } from './DocNodeSpec.js'; 6 | 7 | const SPEC = { 8 | // [FS] IRAD-1005 2020-07-07 9 | // Upgrade outdated packages. 10 | key: new PluginKey('EditorPageLayoutPlugin'), 11 | props: { 12 | attributes: renderAttributes, 13 | }, 14 | }; 15 | 16 | function renderAttributes(editorState: EditorState): Object { 17 | const { doc } = editorState; 18 | const attrs: Object = { 19 | class: 'czi-prosemirror-editor', 20 | }; 21 | 22 | const { width, padding, layout } = doc.attrs; 23 | 24 | let style = ''; 25 | let computedLayout; 26 | if (width) { 27 | const inWidth = width / 72; 28 | const cmWidth = inWidth * 2.54; 29 | if (!computedLayout && inWidth >= 10.9 && inWidth <= 11.1) { 30 | // Round up to letter size. 31 | computedLayout = LAYOUT.US_LETTER_LANDSCAPE; 32 | } else if (!computedLayout && inWidth >= 8.4 && inWidth <= 8.6) { 33 | // Round up to letter size. 34 | computedLayout = LAYOUT.US_LETTER_PORTRAIT; 35 | } else if (!computedLayout && cmWidth >= 29.5 && cmWidth <= 30.1) { 36 | // Round up to letter size. 37 | computedLayout = LAYOUT.A4_LANDSCAPE; 38 | } else if (!computedLayout && cmWidth >= 20.5 && cmWidth <= 21.5) { 39 | // Round up to letter size. 40 | computedLayout = LAYOUT.A4_PORTRAIT; 41 | } else { 42 | // Use custom width (e.g. imported from google doc). 43 | style += `width: ${width}pt;`; 44 | } 45 | if (padding) { 46 | style += `padding-left: ${padding}pt;`; 47 | style += `padding-right: ${padding}pt;`; 48 | } 49 | attrs.style = style; 50 | } else { 51 | computedLayout = layout; 52 | } 53 | if (computedLayout) { 54 | attrs[ATTRIBUTE_LAYOUT] = computedLayout; 55 | } 56 | return attrs; 57 | } 58 | 59 | // Unfortunately the root node `doc` does not supoort `toDOM`, thus 60 | // we'd have to assign its `attributes` manually. 61 | class EditorPageLayoutPlugin extends Plugin { 62 | constructor() { 63 | super(SPEC); 64 | } 65 | } 66 | 67 | export default EditorPageLayoutPlugin; 68 | -------------------------------------------------------------------------------- /src/findActionableCell.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Node } from 'prosemirror-model'; 4 | import { EditorState, TextSelection } from 'prosemirror-state'; 5 | import { CellSelection, TableMap } from 'prosemirror-tables'; 6 | import { findParentNodeOfType } from 'prosemirror-utils'; 7 | 8 | import { TABLE_CELL, TABLE_HEADER } from './NodeNames.js'; 9 | 10 | type Result = { 11 | node: Node, 12 | pos: number, 13 | }; 14 | 15 | function findActionableCellFromSelection(selection: CellSelection): ?Result { 16 | const { $anchorCell } = selection; 17 | const start = $anchorCell.start(-1); 18 | const table = $anchorCell.node(-1); 19 | const tableMap = TableMap.get(table); 20 | let topRightRect; 21 | let posFound = null; 22 | let nodeFound = null; 23 | selection.forEachCell((cell, cellPos) => { 24 | const cellRect = tableMap.findCell(cellPos - start); 25 | if ( 26 | !topRightRect || 27 | (cellRect.top >= topRightRect.top && cellRect.left > topRightRect.left) 28 | ) { 29 | topRightRect = cellRect; 30 | posFound = cellPos; 31 | nodeFound = cell; 32 | } 33 | }); 34 | 35 | return posFound === null 36 | ? null 37 | : { 38 | node: nodeFound, 39 | pos: posFound, 40 | }; 41 | } 42 | 43 | export default function findActionableCell(state: EditorState): ?Result { 44 | const { doc, selection, schema } = state; 45 | const tdType = schema.nodes[TABLE_CELL]; 46 | const thType = schema.nodes[TABLE_HEADER]; 47 | if (!tdType && !thType) { 48 | return null; 49 | } 50 | 51 | let userSelection = selection; 52 | 53 | if (userSelection instanceof TextSelection) { 54 | const { from, to } = selection; 55 | if (from !== to) { 56 | return null; 57 | } 58 | const result = 59 | (tdType && findParentNodeOfType(tdType)(selection)) || 60 | (thType && findParentNodeOfType(thType)(selection)); 61 | 62 | if (!result) { 63 | return null; 64 | } 65 | 66 | userSelection = CellSelection.create(doc, result.pos); 67 | } 68 | 69 | if (userSelection instanceof CellSelection) { 70 | return findActionableCellFromSelection(userSelection); 71 | } 72 | 73 | return null; 74 | } 75 | -------------------------------------------------------------------------------- /src/TableCellColorCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { EditorState } from 'prosemirror-state'; 4 | import { setCellAttr } from 'prosemirror-tables'; 5 | import { Transform } from 'prosemirror-transform'; 6 | import { EditorView } from 'prosemirror-view'; 7 | import { ColorEditor } from '@modusoperandi/color-picker'; 8 | import { atAnchorRight, createPopUp } from '@modusoperandi/licit-ui-commands'; 9 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 10 | 11 | const setCellBackgroundBlack = setCellAttr('background', '#000000'); 12 | 13 | class TableCellColorCommand extends UICommand { 14 | _popUp = null; 15 | 16 | shouldRespondToUIEvent = (e: SyntheticEvent<> | MouseEvent): boolean => { 17 | return e.type === UICommand.EventType.MOUSEENTER; 18 | }; 19 | 20 | isEnabled = (state: EditorState): boolean => { 21 | return setCellBackgroundBlack(state.tr); 22 | }; 23 | 24 | waitForUserInput = ( 25 | state: EditorState, 26 | dispatch: ?(tr: Transform) => void, 27 | view: ?EditorView, 28 | event: ?SyntheticEvent<> 29 | ): Promise => { 30 | if (this._popUp) { 31 | return Promise.resolve(undefined); 32 | } 33 | const target = event?.currentTarget; 34 | if (!(target instanceof HTMLElement)) { 35 | return Promise.resolve(undefined); 36 | } 37 | 38 | const anchor = event ? event.currentTarget : null; 39 | return new Promise((resolve) => { 40 | this._popUp = createPopUp(ColorEditor, null, { 41 | anchor, 42 | autoDismiss: false, 43 | position: atAnchorRight, 44 | onClose: (val) => { 45 | if (this._popUp) { 46 | this._popUp = null; 47 | resolve(val); 48 | } 49 | }, 50 | }); 51 | }); 52 | }; 53 | 54 | executeWithUserInput = ( 55 | state: EditorState, 56 | dispatch: ?(tr: Transform) => void, 57 | view: ?EditorView, 58 | hex: ?{ color: string, selectedPosition?: string[] } 59 | ): boolean => { 60 | if (dispatch && hex !== undefined) { 61 | const cmd = setCellAttr('background', hex.color); 62 | cmd(state, dispatch, view); 63 | return true; 64 | } 65 | return false; 66 | }; 67 | 68 | cancel(): void { 69 | return null; 70 | } 71 | } 72 | 73 | export default TableCellColorCommand; 74 | -------------------------------------------------------------------------------- /src/ListItemInsertNewLineCommand.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Fragment, Schema } from 'prosemirror-model'; 4 | import { EditorState, TextSelection } from 'prosemirror-state'; 5 | import { Transform } from 'prosemirror-transform'; 6 | import { findParentNodeOfType } from 'prosemirror-utils'; 7 | import { EditorView } from 'prosemirror-view'; 8 | import * as React from 'react'; 9 | import { HARD_BREAK, LIST_ITEM } from './NodeNames.js'; 10 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 11 | 12 | // This handles the case when user press SHIFT + ENTER key to insert a new line 13 | // into list item. 14 | function insertNewLine(tr: Transform, schema: Schema): Transform { 15 | const { selection } = tr; 16 | if (!selection) { 17 | return tr; 18 | } 19 | const { from, empty } = selection; 20 | if (!empty) { 21 | return tr; 22 | } 23 | const br = schema.nodes[HARD_BREAK]; 24 | if (!br) { 25 | return tr; 26 | } 27 | const blockquote = schema.nodes[LIST_ITEM]; 28 | const result = findParentNodeOfType(blockquote)(selection); 29 | if (!result) { 30 | return tr; 31 | } 32 | tr = tr.insert(from, Fragment.from(br.create())); 33 | tr = tr.setSelection(TextSelection.create(tr.doc, from + 1, from + 1)); 34 | return tr; 35 | } 36 | 37 | class ListItemInsertNewLineCommand extends UICommand { 38 | execute = ( 39 | state: EditorState, 40 | dispatch: ?(tr: Transform) => void, 41 | view: ?EditorView 42 | ): boolean => { 43 | const { schema, selection } = state; 44 | const tr = insertNewLine(state.tr.setSelection(selection), schema); 45 | if (tr.docChanged) { 46 | dispatch && dispatch(tr); 47 | return true; 48 | } else { 49 | return false; 50 | } 51 | }; 52 | 53 | waitForUserInput = ( 54 | _state: EditorState, 55 | _dispatch: ?(tr: Transform) => void, 56 | _view: ?EditorView, 57 | _event: ?React.SyntheticEvent 58 | ): Promise => { 59 | return Promise.resolve(undefined); 60 | }; 61 | 62 | executeWithUserInput = ( 63 | _state: EditorState, 64 | _dispatch: ?(tr: Transform) => void, 65 | _view: ?EditorView, 66 | _inputs: ?string 67 | ): boolean => { 68 | return false; 69 | }; 70 | 71 | cancel(): void { 72 | return null; 73 | } 74 | } 75 | 76 | export default ListItemInsertNewLineCommand; 77 | -------------------------------------------------------------------------------- /src/ui/EditorFrameset.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import cx from 'classnames'; 3 | import * as React from 'react'; 4 | 5 | export type EditorFramesetProps = { 6 | body: ?React.Element, 7 | className: ?string, 8 | embedded: ?boolean, 9 | header: ?React.Element, 10 | height: ?(string | number), 11 | toolbarPlacement?: 'header' | 'body' | null, 12 | toolbar: ?React.Element, 13 | width: ?(string | number), 14 | }; 15 | 16 | export const FRAMESET_BODY_CLASSNAME = 'czi-editor-frame-body'; 17 | 18 | function toCSS(val: ?(number | string)): string | any { 19 | if (!val || val === 'auto') { 20 | // '', 0, null, false, 'auto' are all treated as undefined 21 | // instead of auto... 22 | return undefined; 23 | } 24 | if (isNaN(val)) { 25 | return `${val}`; 26 | } 27 | return `${val}px`; 28 | } 29 | 30 | class EditorFrameset extends React.PureComponent { 31 | props: EditorFramesetProps; 32 | 33 | render(): React.Element { 34 | const { 35 | body, 36 | className, 37 | embedded, 38 | header, 39 | height, 40 | toolbarPlacement, 41 | toolbar, 42 | width, 43 | } = this.props; 44 | 45 | const mainStyle = { 46 | width: toCSS(width), 47 | height: toCSS(height), 48 | }; 49 | 50 | const mainClassName = cx(className, { 51 | 'czi-editor-frameset': true, 52 | // Layout is fixed when either width or height is set. 53 | 'with-fixed-layout': mainStyle.width || mainStyle.height, 54 | embedded: embedded, 55 | }); 56 | 57 | const toolbarHeader = 58 | toolbarPlacement === 'header' || !toolbarPlacement ? toolbar : null; 59 | const toolbarBody = toolbarPlacement === 'body' && toolbar; 60 | 61 | return ( 62 |
63 |
64 |
65 | {header} 66 | {toolbarHeader} 67 |
68 |
69 | {toolbarBody} 70 |
{body}
71 |
72 |
73 |
74 |
75 | ); 76 | } 77 | } 78 | 79 | export default EditorFrameset; 80 | -------------------------------------------------------------------------------- /src/ui/ListTypeMenu.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { UICommand } from '@modusoperandi/licit-doc-attrs-step'; 3 | import { EditorState } from 'prosemirror-state'; 4 | import { Transform } from 'prosemirror-transform'; 5 | import { EditorView } from 'prosemirror-view'; 6 | import uuid from './uuid.js'; 7 | 8 | // [FS] IRAD-1039 2020-09-24 9 | // UI to show the list buttons 10 | 11 | class ListTypeMenu extends React.PureComponent { 12 | _activeCommand: ?UICommand = null; 13 | props: { 14 | className?: ?string, 15 | commandGroups: Array<{ [string]: UICommand }>, 16 | disabled?: ?boolean, 17 | dispatch: (tr: Transform) => void, 18 | editorState: EditorState, 19 | editorView: ?EditorView, 20 | icon?: string | React.Element | null, 21 | label?: string | React.Element | null, 22 | title?: ?string, 23 | }; 24 | 25 | _menu = null; 26 | _id = uuid(); 27 | 28 | state = { 29 | expanded: false, 30 | }; 31 | 32 | render() { 33 | const { commandGroups } = this.props; 34 | const children = []; 35 | 36 | commandGroups.forEach((group, ii) => { 37 | Object.keys(group).forEach((label) => { 38 | const command = group[label]; 39 | children.push( 40 | 52 | ); 53 | }); 54 | }); 55 | return
{children}
; 56 | } 57 | 58 | _onUIEnter = (command: UICommand, event: SyntheticEvent<*>) => { 59 | this._activeCommand && this._activeCommand.cancel(); 60 | this._activeCommand = command; 61 | this._execute(command, event); 62 | }; 63 | 64 | _execute = (command, e) => { 65 | const { dispatch, editorState, editorView, onCommand } = this.props; 66 | if (command.execute(editorState, dispatch, editorView, e)) { 67 | onCommand?.(); 68 | } 69 | }; 70 | } 71 | 72 | 73 | 74 | export function preventEventDefault(e: React.SyntheticEvent): void { 75 | e.preventDefault(); 76 | } 77 | 78 | export default ListTypeMenu; 79 | -------------------------------------------------------------------------------- /src/ui/ListTypeCommandButton.js: -------------------------------------------------------------------------------- 1 | // [FS] IRAD-1039 2020-09-23 2 | // Command button to handle different type of list types 3 | // Need to add Icons instead of label 4 | import * as React from 'react'; 5 | import { EditorState } from 'prosemirror-state'; 6 | import { Transform } from 'prosemirror-transform'; 7 | import { EditorView } from 'prosemirror-view'; 8 | import { 9 | ListToggleCommand, 10 | hasImageNode, 11 | } from '../ListToggleCommand.js'; 12 | import ListTypeButton from './ListTypeButton.js'; 13 | 14 | const LIST_TYPE_NAMES = [ 15 | { 16 | name: 'decimal', 17 | label: '1.', 18 | }, 19 | { 20 | name: 'x.x.x', 21 | label: '1.1.1', 22 | }, 23 | { 24 | name: 'num_bracket', 25 | label: '1)', 26 | }, 27 | { 28 | name: 'num_bracket_closed', 29 | label: '(1)', 30 | }, 31 | { 32 | name: 'upper_alpha_bracket', 33 | label: 'A)', 34 | }, 35 | { 36 | name: 'lower_alpha_bracket', 37 | label: 'a)', 38 | }, 39 | { 40 | name: 'lower_alpha_bracket_closed', 41 | label: '(a)', 42 | }, 43 | ]; 44 | const LIST_TYPE_COMMANDS: Object = { 45 | ['decimal']: new ListToggleCommand(true, 'decimal'), 46 | }; 47 | LIST_TYPE_NAMES.forEach((obj) => { 48 | LIST_TYPE_COMMANDS[obj.name] = new ListToggleCommand(true, obj.name); 49 | LIST_TYPE_COMMANDS[obj.name].label = obj.label; 50 | }); 51 | 52 | const COMMAND_GROUPS = [LIST_TYPE_COMMANDS]; 53 | 54 | class ListTypeCommandButton extends React.PureComponent { 55 | props: { 56 | dispatch: (tr: Transform) => void, 57 | editorState: EditorState, 58 | editorView: ?EditorView, 59 | }; 60 | 61 | render(): React.Element { 62 | const { dispatch, editorState, editorView } = this.props; 63 | let disabled = false; 64 | if (editorState && editorView) { 65 | // [FS] IRAD-1317 2021-05-06 66 | // To disable the list menu when select an image 67 | disabled = 68 | hasImageNode(editorState); 69 | disabled = editorView.disabled || disabled; 70 | } 71 | return ( 72 | 81 | ); 82 | } 83 | } 84 | 85 | export default ListTypeCommandButton; 86 | --------------------------------------------------------------------------------