├── .eslintignore
├── .gitignore
├── src
├── core
│ ├── index.js
│ ├── node-views
│ │ ├── index.js
│ │ ├── table.js
│ │ ├── citation.js
│ │ ├── highlight.js
│ │ └── image.js
│ ├── plugins
│ │ ├── read-only.js
│ │ ├── schema-transform.js
│ │ ├── node-id.js
│ │ ├── placeholder.js
│ │ ├── pull-item-data.js
│ │ ├── math.js
│ │ ├── trailing-paragraph.js
│ │ ├── text-color.js
│ │ ├── drag.js
│ │ ├── image.js
│ │ ├── highlight-color.js
│ │ ├── link.js
│ │ └── underline-color.js
│ ├── schema
│ │ ├── colors.js
│ │ ├── index.js
│ │ ├── transformer.js
│ │ ├── marks.js
│ │ ├── README.md
│ │ └── metadata.js
│ ├── provider.js
│ ├── math.js
│ ├── input-rules.js
│ ├── keymap.js
│ └── helpers.js
├── ui
│ ├── noticebar.js
│ ├── toolbar-elements
│ │ ├── align-dropdown.js
│ │ ├── button.js
│ │ ├── insert-dropdown.js
│ │ ├── text-color-dropdown.js
│ │ ├── highlight-color-dropdown.js
│ │ ├── text-dropdown.js
│ │ └── dropdown.js
│ ├── popups
│ │ ├── image-popup.js
│ │ ├── highlight-popup.js
│ │ ├── popup.js
│ │ ├── citation-popup.js
│ │ ├── table-popup.js
│ │ └── link-popup.js
│ ├── custom-icons.js
│ ├── editor.js
│ └── findbar.js
├── stylesheets
│ ├── components
│ │ ├── ui
│ │ │ ├── _noticebar.scss
│ │ │ ├── _popup.scss
│ │ │ ├── _findbar.scss
│ │ │ ├── _editor.scss
│ │ │ └── _toolbar.scss
│ │ └── core
│ │ │ ├── _math.scss
│ │ │ ├── _image.scss
│ │ │ └── _prosemirror-math.scss
│ ├── main.scss
│ ├── abstracts
│ │ └── _mixins.scss
│ ├── base
│ │ └── _base.scss
│ └── themes
│ │ ├── _dark.scss
│ │ └── _light.scss
├── fluent.js
├── index.web.js
└── index.android.js
├── postcss.config.js
├── .editorconfig
├── res
└── icons
│ ├── 16
│ ├── page.svg
│ ├── insert-row-above.svg
│ ├── insert-row-below.svg
│ ├── show-item.svg
│ ├── delete-row.svg
│ ├── insert-column-right.svg
│ ├── cite.svg
│ ├── insert-column-left.svg
│ ├── delete-table.svg
│ ├── checkmark.svg
│ ├── remove-color.svg
│ ├── delete-column.svg
│ ├── edit.svg
│ ├── hide.svg
│ └── unlink.svg
│ └── 20
│ ├── chevron-left.svg
│ ├── chevron-down.svg
│ ├── chevron-up.svg
│ ├── image.svg
│ ├── italic.svg
│ ├── plus.svg
│ ├── align-left.svg
│ ├── align-right.svg
│ ├── align-center.svg
│ ├── underline.svg
│ ├── monospaced-1.25.svg
│ ├── text-color.svg
│ ├── table.svg
│ ├── bold.svg
│ ├── magnifier.svg
│ ├── sidebar.svg
│ ├── cite.svg
│ ├── sidebar-bottom.svg
│ ├── options.svg
│ ├── highlight.svg
│ ├── clear-format.svg
│ ├── superscript.svg
│ ├── subscript.svg
│ ├── format-text.svg
│ ├── math.svg
│ ├── strikethrough.svg
│ └── link.svg
├── babel.config.js
├── html
├── editor.web.html
├── editor.android.html
├── editor.ios.html
├── editor.zotero.html
└── editor.dev.html
├── scripts
└── upload
├── README.md
├── .eslintrc
├── .github
└── workflows
│ └── ci.yml
├── package.json
├── webpack.zotero-locale-plugin.js
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .*
2 | !.gitignore
3 | !.editorconfig
4 | node_modules
5 | build
6 | locales
7 |
--------------------------------------------------------------------------------
/src/core/index.js:
--------------------------------------------------------------------------------
1 | import EditorCore from './editor-core';
2 |
3 | export { EditorCore };
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'autoprefixer': {},
4 | 'postcss-rtlcss': {}
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | end_of_line = lf
3 | insert_final_newline = true
4 |
5 | [*.{js, json, scss, yml, html}]
6 | charset = utf-8
7 | indent_style = tab
8 | trim_trailing_whitespace = true
9 |
--------------------------------------------------------------------------------
/res/icons/20/chevron-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/core/node-views/index.js:
--------------------------------------------------------------------------------
1 | import citation from './citation';
2 | import image from './image';
3 | import highlight from './highlight';
4 | import table from './table';
5 |
6 | export default {
7 | citation,
8 | image,
9 | highlight,
10 | table
11 | };
12 |
--------------------------------------------------------------------------------
/res/icons/16/page.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/20/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/res/icons/20/chevron-up.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/res/icons/20/image.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/ui/noticebar.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import cx from 'classnames';
4 | import React from 'react';
5 |
6 | function Noticebar({ message, children }) {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | }
13 |
14 | export default Noticebar;
15 |
--------------------------------------------------------------------------------
/res/icons/16/insert-row-above.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/20/italic.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/20/plus.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/res/icons/16/insert-row-below.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/stylesheets/components/ui/_noticebar.scss:
--------------------------------------------------------------------------------
1 | .noticebar {
2 | border-bottom: 1px solid #d9d9d9;
3 | justify-content: center;
4 | text-align: center;
5 | background: #fff86e;
6 | color: black;
7 | font-size: 12px;
8 | padding: 4px 10px;
9 | line-height: 1.4;
10 | cursor: default;
11 | }
12 |
--------------------------------------------------------------------------------
/res/icons/20/align-left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/20/align-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@babel/preset-react',
4 | [
5 | '@babel/preset-env',
6 | {
7 | modules: false,
8 | useBuiltIns: 'usage',
9 | corejs: { version: '3.24', proposals: true },
10 | },
11 | ],
12 | ],
13 | plugins: ['@babel/plugin-transform-runtime'],
14 | };
15 |
--------------------------------------------------------------------------------
/res/icons/20/align-center.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/html/editor.web.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Zotero Note Editor
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/html/editor.android.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Zotero Note Editor
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/scripts/upload:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | S3_URI=s3://zotero-download/ci/client-note-editor/
3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 | cd $SCRIPT_DIR/../build/zotero
5 | HASH=$(git rev-parse HEAD)
6 | FILENAME=$HASH.zip
7 | zip -r ../$FILENAME .
8 | cd ..
9 | aws s3 cp $FILENAME $S3_URI$FILENAME
10 | rm $FILENAME
11 |
--------------------------------------------------------------------------------
/src/core/plugins/read-only.js:
--------------------------------------------------------------------------------
1 | import { Plugin, PluginKey } from 'prosemirror-state';
2 |
3 | export function readOnly(options) {
4 | return new Plugin({
5 | filterTransaction(tr) {
6 | if (options.enable) {
7 | if (tr.docChanged) {
8 | return false;
9 | }
10 | }
11 | return true;
12 | }
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/src/core/plugins/schema-transform.js:
--------------------------------------------------------------------------------
1 | import { Plugin } from 'prosemirror-state';
2 | import { schemaTransform } from '../schema/transformer';
3 |
4 | export function transform(options) {
5 | return new Plugin({
6 | appendTransaction(transactions, oldState, newState) {
7 | return schemaTransform(newState);
8 | }
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Zotero Note Editor
2 |
3 | ## Build
4 |
5 | Clone the repository:
6 |
7 | ```
8 | git clone https://github.com/zotero/zotero-note-editor
9 | ```
10 |
11 | Run `npm run build` to produce `build/web` and `build/zotero`
12 |
13 | ## Development
14 |
15 | Run `npm start` to open the automatically refreshing development window
16 |
--------------------------------------------------------------------------------
/res/icons/20/underline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/20/monospaced-1.25.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/20/text-color.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/16/show-item.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/20/table.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/html/editor.ios.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Zotero Note Editor
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/res/icons/16/delete-row.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/html/editor.zotero.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Zotero Note Editor
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/res/icons/20/bold.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/16/insert-column-right.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/res/icons/16/cite.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/res/icons/20/magnifier.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/16/insert-column-left.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/core/schema/colors.js:
--------------------------------------------------------------------------------
1 |
2 | export const TEXT_COLORS = [
3 | ['red', '#ff2020'],
4 | ['orange', '#ff7700'],
5 | ['yellow', '#ffcb00'],
6 | ['green', '#4eb31c'],
7 | ['purple', '#7953e3'],
8 | ['magenta', '#eb52f7'],
9 | ['blue', '#05a2ef'],
10 | ['gray', '#7e8386'],
11 | ];
12 |
13 | export const HIGHLIGHT_COLORS = [
14 | ['red', '#ff666680'],
15 | ['orange', '#f1983780'],
16 | ['yellow', '#ffd40080'],
17 | ['green', '#5fb23680'],
18 | ['purple', '#a28ae580'],
19 | ['magenta', '#e56eee80'],
20 | ['blue', '#2ea8e580'],
21 | ['gray', '#aaaaaa80'],
22 | ];
23 |
--------------------------------------------------------------------------------
/res/icons/20/sidebar.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/res/icons/20/cite.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/res/icons/20/sidebar-bottom.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "globals": {
8 | },
9 | "extends": [
10 | "@zotero",
11 | "plugin:react/recommended"
12 | ],
13 | "parser": "@babel/eslint-parser",
14 | "parserOptions": {
15 | "ecmaVersion": 2018,
16 | "ecmaFeatures": {
17 | "jsx": true
18 | },
19 | "sourceType": "module",
20 | "babelOptions": {
21 | "configFile": "./babel.config.js"
22 | }
23 | },
24 | "plugins": [
25 | "react"
26 | ],
27 | "settings": {
28 | "react": {
29 | "version": "16.14"
30 | }
31 | },
32 | "rules": {}
33 | }
34 |
--------------------------------------------------------------------------------
/res/icons/20/options.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/16/delete-table.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/16/checkmark.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/res/icons/16/remove-color.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/res/icons/20/highlight.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/16/delete-column.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/res/icons/20/clear-format.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/stylesheets/main.scss:
--------------------------------------------------------------------------------
1 |
2 | $mobile: false;
3 | @if $platform == 'ios' or $platform == 'android' {
4 | $mobile: true;
5 | }
6 |
7 | //
8 | // Abstracts
9 | //
10 |
11 | @import "abstracts/mixins";
12 |
13 | //
14 | // Themes
15 | //
16 |
17 | @import "themes/light";
18 | @import "themes/dark";
19 |
20 | //
21 | // Base
22 | //
23 |
24 | @import "base/base";
25 |
26 | //
27 | // Components
28 | //
29 |
30 | @import "components/core/core";
31 | @import "components/core/image";
32 | @import "components/core/math";
33 |
34 | @import "components/ui/popup";
35 | @import "components/ui/editor";
36 | @import "components/ui/findbar";
37 | @import "components/ui/noticebar";
38 | @import "components/ui/toolbar";
39 |
--------------------------------------------------------------------------------
/src/core/node-views/table.js:
--------------------------------------------------------------------------------
1 |
2 | class TableView {
3 | constructor(node) {
4 | this.node = node
5 | this.dom = document.createElement("div")
6 | this.dom.className = "tableWrapper"
7 | this.table = this.dom.appendChild(document.createElement("table"))
8 | this.colgroup = this.table.appendChild(document.createElement("colgroup"))
9 | this.contentDOM = this.table.appendChild(document.createElement("tbody"))
10 | }
11 |
12 | update(node) {
13 | if (node.type != this.node.type) return false
14 | this.node = node
15 | return true
16 | }
17 | }
18 |
19 | export default function (options) {
20 | return function (node, view, getPos) {
21 | return new TableView(node, view, getPos, options);
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/res/icons/20/superscript.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/20/subscript.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/res/icons/16/edit.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/core/plugins/node-id.js:
--------------------------------------------------------------------------------
1 | import { Plugin, PluginKey } from 'prosemirror-state';
2 | import { randomString } from '../utils';
3 |
4 | function addOrDeduplicateIDs(state) {
5 | let nodeIDs = [];
6 | let tr = state.tr;
7 | let updated = false;
8 | state.doc.descendants((node, pos) => {
9 | if (node.type.attrs.nodeID) {
10 | let nodeID = node.attrs.nodeID;
11 | if (!nodeID || nodeIDs.includes(nodeID)) {
12 | nodeID = randomString();
13 | tr.setNodeMarkup(pos, null, {
14 | ...node.attrs,
15 | nodeID
16 | });
17 | updated = true;
18 | }
19 | }
20 | });
21 |
22 | return updated && tr || null;
23 | }
24 |
25 | export function nodeID(options) {
26 | return new Plugin({
27 | appendTransaction(transactions, oldState, newState) {
28 | return addOrDeduplicateIDs(newState);
29 | }
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/src/core/schema/index.js:
--------------------------------------------------------------------------------
1 | import { Schema } from 'prosemirror-model';
2 | import nodes from './nodes';
3 | import marks from './marks';
4 | import { buildToHTML, buildFromHTML, buildClipboardSerializer } from './utils';
5 | import { TEXT_COLORS, HIGHLIGHT_COLORS } from './colors';
6 |
7 | const schema = new Schema({ nodes, marks });
8 | // Update in Zotero 'editorInstance.js' as well!
9 | schema.version = 10;
10 |
11 | const toHTML = buildToHTML(schema);
12 | const fromHTML = buildFromHTML(schema);
13 |
14 | // Note: Upgrade schema version if introducing new quotation marks
15 | const QUOTATION_MARKS = ["'",'"', '“', '”', '‘', '’', '„','«','»'];
16 |
17 | export {
18 | nodes,
19 | marks,
20 | schema,
21 | toHTML,
22 | fromHTML,
23 | buildClipboardSerializer,
24 | QUOTATION_MARKS,
25 | TEXT_COLORS,
26 | HIGHLIGHT_COLORS
27 | };
28 |
--------------------------------------------------------------------------------
/res/icons/20/format-text.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/core/plugins/placeholder.js:
--------------------------------------------------------------------------------
1 | import { Plugin, PluginKey } from 'prosemirror-state';
2 | import { Decoration, DecorationSet } from 'prosemirror-view';
3 |
4 | export function placeholder(options) {
5 | return new Plugin({
6 | props: {
7 | decorations: (state) => {
8 | const decorations = [];
9 | if (options.text && state.doc.content.childCount === 1) {
10 | state.doc.descendants((node, pos) => {
11 | if (node.type.isBlock && node.childCount === 0 /*&& state.selection.$anchor.parent !== node*/) {
12 | decorations.push(
13 | Decoration.node(pos, pos + node.nodeSize, {
14 | class: 'empty-node',
15 | 'data-placeholder': options.text
16 | })
17 | );
18 | }
19 | return false;
20 | });
21 | }
22 | return DecorationSet.create(state.doc, decorations);
23 | }
24 | }
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/res/icons/20/math.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/fluent.js:
--------------------------------------------------------------------------------
1 | import { FluentBundle, FluentResource } from '@fluent/bundle';
2 |
3 | import zotero from '../locales/en-US/zotero.ftl';
4 | import reader from '../locales/en-US/note-editor.ftl';
5 |
6 | export let bundle = new FluentBundle('en-US', {
7 | functions: {
8 | PLATFORM: () => 'web',
9 | },
10 | });
11 |
12 | bundle.addResource(new FluentResource(zotero));
13 | bundle.addResource(new FluentResource(reader));
14 |
15 | if (__BUILD__ !== 'zotero') {
16 | bundle.addResource(new FluentResource('-app-name = Zotero'));
17 | }
18 |
19 | export function getLocalizedString(key, args = {}) {
20 | const message = bundle.getMessage(key);
21 | if (message && message.value) {
22 | return bundle.formatPattern(message.value, args);
23 | } else {
24 | console.warn(`Localization key '${key}' not found`);
25 | return key;
26 | }
27 | }
28 |
29 | export function addFTL(ftl) {
30 | bundle.addResource(new FluentResource(ftl), { allowOverrides: true });
31 | }
32 |
--------------------------------------------------------------------------------
/src/stylesheets/components/core/_math.scss:
--------------------------------------------------------------------------------
1 | @import "./prosemirror-math";
2 | @import "~katex/dist/katex.min.css";
3 |
4 | .math-node.empty-math .math-render::before {
5 | content: "Click to insert LaTex ...";
6 | color: black;
7 | }
8 |
9 | .math-node .math-render.parse-error::before {
10 | content: "Cannot parse LaTeX.";
11 | cursor: help;
12 | }
13 |
14 | .math-node .ProseMirror-focused {
15 | outline: none;
16 | }
17 |
18 | math-display {
19 | position: relative;
20 |
21 | &:before {
22 | position: absolute;
23 | width: 64px;
24 | height: 85%;
25 | margin-left: -64px;
26 | content: "";
27 | }
28 | }
29 |
30 | math-inline {
31 | .math-src {
32 | div[contenteditable] {
33 | white-space: normal;
34 | word-break: break-word;
35 | }
36 | }
37 | }
38 |
39 | math-display {
40 | margin-block-start: 1em;
41 | margin-block-end: 1em;
42 | }
43 |
44 | .math-src {
45 | font-size: 0.90em;
46 | }
47 |
--------------------------------------------------------------------------------
/html/editor.dev.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Zotero Note Editor
6 |
7 |
8 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/core/provider.js:
--------------------------------------------------------------------------------
1 | import { randomString } from './utils';
2 |
3 | class Provider {
4 | constructor(options) {
5 | this.subscriptions = [];
6 | this.onSubscribe = options.onSubscribe;
7 | this.onUnsubscribe = options.onUnsubscribe;
8 | }
9 |
10 | subscribe(subscription) {
11 | subscription.id = randomString();
12 | this.subscriptions.push(subscription);
13 | this.onSubscribe(subscription);
14 | }
15 |
16 | unsubscribe(listener) {
17 | let subscription = this.subscriptions.find(s => s.listener === listener);
18 | this.subscriptions.splice(this.subscriptions.indexOf(subscription), 1);
19 | this.onUnsubscribe(subscription);
20 | }
21 |
22 | notify(id, data) {
23 | this.subscriptions.forEach((subscription) => {
24 | if (subscription.id === id) {
25 | subscription.listener(data);
26 | subscription.cachedData = data;
27 | }
28 | });
29 | }
30 |
31 | getCachedData(nodeID, type) {
32 | let subscription = this.subscriptions.find(s => s.nodeID === nodeID);
33 | return subscription && subscription.cachedData || null;
34 | }
35 | }
36 |
37 | export default Provider;
38 |
--------------------------------------------------------------------------------
/src/stylesheets/components/core/_image.scss:
--------------------------------------------------------------------------------
1 | .regular-image {
2 | width: 100%;
3 | display: inline-block;
4 |
5 | .resized-wrapper {
6 | max-width: 100%;
7 | margin-left: auto;
8 | margin-right: auto;
9 |
10 | .image {
11 | outline: 1px solid var(--fill-quarternary);
12 |
13 | &.annotation:hover {
14 | border-color: var(--fill-tertiary);
15 | }
16 | }
17 | }
18 | }
19 |
20 | .external-image {
21 | width: 100%;
22 | display: inline-block;
23 |
24 | .resized-wrapper {
25 | padding: 10px;
26 | border: 1px solid #d9d9d9;
27 | display: flex;
28 | align-items: center;
29 | flex-direction: column;
30 | max-width: 100%;
31 | margin-left: auto;
32 | margin-right: auto;
33 |
34 | .image {
35 | max-width: 100px;
36 | max-height: 100px;
37 | overflow-y: hidden;
38 | }
39 | }
40 | }
41 |
42 | .import-placeholder-image {
43 | width: 100%;
44 | display: flex;
45 | align-items: center;
46 | flex-direction: column;
47 |
48 | .image {
49 | max-width: 100%;
50 | width: 200px;
51 | height: 200px;
52 | background-color: var(--fill-quarternary);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/res/icons/16/hide.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/res/icons/20/strikethrough.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/ui/toolbar-elements/align-dropdown.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import { useLocalization } from '@fluent/react';
5 |
6 | import Dropdown from './dropdown';
7 | import { StateButton } from './button';
8 |
9 | import IconAlignLeft from '../../../res/icons/20/align-left.svg';
10 | import IconAlignCenter from '../../../res/icons/20/align-center.svg';
11 | import IconAlignRight from '../../../res/icons/20/align-right.svg';
12 |
13 | export default function AlignDropdown({ menuState }) {
14 | const { l10n } = useLocalization();
15 |
16 | let icon = menuState.alignCenter.isActive &&
17 | || menuState.alignRight.isActive &&
18 | ||
19 |
20 | return (
21 |
26 | }
29 | title={l10n.getString('note-editor-align-left')}
30 | />
31 | }
34 | title={l10n.getString('note-editor-align-center')}
35 | />
36 | }
39 | title={l10n.getString('note-editor-align-right')}
40 | />
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/core/node-views/citation.js:
--------------------------------------------------------------------------------
1 | import { formatCitation } from '../utils';
2 |
3 | // Note: Node view is only updated when document or decoration is updated at specific position
4 | // https://discuss.prosemirror.net/t/force-nodes-of-specific-type-to-re-render/2480/2
5 |
6 | class CitationView {
7 | constructor(node, view, getPos, options) {
8 | this.dom = document.createElement('span');
9 | this.dom.className = 'citation';
10 |
11 | let formattedCitation = '{citation}';
12 | try {
13 | let citation = JSON.parse(JSON.stringify(node.attrs.citation));
14 | options.metadata.fillCitationItemsWithData(citation.citationItems);
15 | let missingItemData = citation.citationItems.find(x => !x.itemData);
16 | if (missingItemData) {
17 | formattedCitation = node.textContent;
18 | }
19 | else {
20 | let text = formatCitation(citation);
21 | if (text) {
22 | formattedCitation = '(' + text + ')';
23 | }
24 | }
25 | }
26 | catch (e) {
27 | console.log(e);
28 | }
29 | this.dom.innerHTML = formattedCitation;
30 | }
31 |
32 | selectNode() {
33 | this.dom.classList.add('selected');
34 | }
35 |
36 | deselectNode() {
37 | this.dom.classList.remove('selected');
38 | }
39 |
40 | destroy() {
41 | }
42 | }
43 |
44 | export default function (options) {
45 | return function (node, view, getPos) {
46 | return new CitationView(node, view, getPos, options);
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/src/stylesheets/components/ui/_popup.scss:
--------------------------------------------------------------------------------
1 | .popup-container .popup {
2 | position: absolute;
3 | display: flex;
4 | z-index: 100;
5 | font-size: 12px;
6 | background: var(--material-toolbar);
7 | border: var(--material-panedivider);
8 | padding: 5px;
9 | border-radius: 5px;
10 | box-shadow: 0 0 24px 0 rgba(0, 0, 0, 0.12);
11 | @include popover-pointer($width: 10px, $height: 5px);
12 |
13 | button {
14 | user-select: none;
15 | padding: 4px;
16 | border-radius: 5px;
17 | cursor: default;
18 | display: flex;
19 | align-items: center;
20 | text-align: center;
21 | color: var(--fill-secondary);
22 |
23 | &:not(:first-child) {
24 | margin-left: 5px;
25 | }
26 |
27 | &:hover {
28 | background: var(--fill-quinary);
29 | }
30 |
31 | .icon {
32 | display: flex;
33 | align-items: center;
34 | justify-content: space-around;
35 | margin-inline-end: 7px;
36 | }
37 |
38 | .title {
39 | color: var(--fill-primary);
40 | }
41 | }
42 | }
43 |
44 | .link-popup {
45 | .link {
46 | width: 200px;
47 | display: flex;
48 | flex: 1;
49 | overflow: hidden;
50 | align-items: center;
51 |
52 | a {
53 | text-overflow: ellipsis;
54 | overflow: hidden;
55 | white-space: nowrap;
56 | margin-left: 2px;
57 | }
58 |
59 | input {
60 | width: 100%;
61 | border: none;
62 | outline: none;
63 | color: inherit;
64 | background-color: transparent;
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/ui/toolbar-elements/button.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { forwardRef } from 'react';
4 | import cx from 'classnames';
5 |
6 | export const Button = forwardRef(({ icon, title, active, enableFocus, className, triggerOnMouseDown, onClick, ...rest }, ref) => {
7 | return (
8 |
36 | );
37 | });
38 |
39 | export function StateButton({ icon, title, state, ...rest }) {
40 | return (
41 |