aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--src/client/util/RichTextSchema.tsx499
-rw-r--r--src/client/util/TooltipTextMenu.scss240
-rw-r--r--src/client/util/TooltipTextMenu.tsx170
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss1
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx12
-rw-r--r--src/server/public/files/upload_1b4818f39ea324b5a687bb1ade3dca6c.jpgbin0 -> 26946 bytes
-rw-r--r--src/server/public/files/upload_1f1c6cfef33e5992fa860802e8c466a7.jpgbin0 -> 45309 bytes
-rw-r--r--src/server/public/files/upload_2045f363aa9cf281407703ca242aad1a.jpgbin0 -> 9009 bytes
-rw-r--r--src/server/public/files/upload_25bffd90c080c27f5ac822984406b958.jpgbin0 -> 43534 bytes
-rw-r--r--src/server/public/files/upload_261f11dc39ad568212b5c7e39d1e6d13.jpgbin0 -> 27259 bytes
-rw-r--r--src/server/public/files/upload_26bcc62639141ba64e603daebb5bf5d3.pngbin0 -> 2757327 bytes
-rw-r--r--src/server/public/files/upload_2d77d0773612e4723b78118ac50a2929.jpgbin0 -> 1805512 bytes
-rw-r--r--src/server/public/files/upload_2de9ad4dc687c53760c39f724c9a08a5.jpgbin0 -> 77462 bytes
-rw-r--r--src/server/public/files/upload_4abb568aa7cce9d291532c3d0da97102.jpgbin0 -> 22445 bytes
-rw-r--r--src/server/public/files/upload_54c34aaca5a7bf510cebad461ec39512.pngbin0 -> 2757327 bytes
-rw-r--r--src/server/public/files/upload_562b1e527300df8b350eeab094b3e1f1.jpgbin0 -> 15988 bytes
-rw-r--r--src/server/public/files/upload_6a26d3f7008a8c79ee5fc8054ba69996.jpgbin0 -> 45025 bytes
-rw-r--r--src/server/public/files/upload_70fa5e0c3f393504349d5865e28f4cac.jpgbin0 -> 18041 bytes
-rw-r--r--src/server/public/files/upload_8155b5b0f57da107bb07083c04e78943.jpgbin0 -> 31103 bytes
-rw-r--r--src/server/public/files/upload_88f588574e0efc415186af935114af9a.jpgbin0 -> 40249 bytes
-rw-r--r--src/server/public/files/upload_8d1c253f93f77c69c0c04ae3efb7d714.pngbin0 -> 2757327 bytes
-rw-r--r--src/server/public/files/upload_9ef80158609f5ff739087aecad367b9d.jpgbin0 -> 28523 bytes
-rw-r--r--src/server/public/files/upload_c39a7e0d7e8d35bb18461a5a0aa063bf.jpgbin0 -> 13811 bytes
-rw-r--r--src/server/public/files/upload_c6b81ab4eb70465a7e9b45d5c8f3ecaa.jpgbin0 -> 28566 bytes
-rw-r--r--src/server/public/files/upload_c99ec7a8a2df0b2f90479fde7d70c2eb.jpgbin0 -> 21995 bytes
-rw-r--r--src/server/public/files/upload_cec1cfcc67cfe5889de4098a49fec45e.jpgbin0 -> 22125 bytes
-rw-r--r--src/server/public/files/upload_f27688fe92dc7de398e957e5d96e1a22.jpgbin0 -> 18964 bytes
29 files changed, 691 insertions, 237 deletions
diff --git a/package.json b/package.json
index 8c9c865e0..eb4d9ac98 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"@types/prosemirror-history": "^1.0.1",
"@types/prosemirror-inputrules": "^1.0.2",
"@types/prosemirror-keymap": "^1.0.1",
+ "@types/prosemirror-menu": "^1.0.1",
"@types/prosemirror-model": "^1.7.0",
"@types/prosemirror-schema-basic": "^1.0.1",
"@types/prosemirror-schema-list": "^1.0.1",
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 92944bec0..dac4be3d6 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -7,135 +7,135 @@ import { EditorState, Transaction, NodeSelection, } from "prosemirror-state";
import { EditorView, } from "prosemirror-view";
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
- preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
+ preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
// :: Object
// [Specs](#model.NodeSpec) for the nodes defined in this schema.
export const nodes: { [index: string]: NodeSpec } = {
- // :: NodeSpec The top level document node.
- doc: {
- content: "block+"
- },
-
- // :: NodeSpec A plain paragraph textblock. Represented in the DOM
- // as a `<p>` element.
- paragraph: {
- content: "inline*",
- group: "block",
- parseDOM: [{ tag: "p" }],
- toDOM() { return pDOM; }
- },
-
- // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
- blockquote: {
- content: "block+",
- group: "block",
- defining: true,
- parseDOM: [{ tag: "blockquote" }],
- toDOM() { return blockquoteDOM; }
- },
-
- // :: NodeSpec A horizontal rule (`<hr>`).
- horizontal_rule: {
- group: "block",
- parseDOM: [{ tag: "hr" }],
- toDOM() { return hrDOM; }
- },
-
- // :: NodeSpec A heading textblock, with a `level` attribute that
- // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
- // `<h6>` elements.
- heading: {
- attrs: { level: { default: 1 } },
- content: "inline*",
- group: "block",
- defining: true,
- parseDOM: [{ tag: "h1", attrs: { level: 1 } },
- { tag: "h2", attrs: { level: 2 } },
- { tag: "h3", attrs: { level: 3 } },
- { tag: "h4", attrs: { level: 4 } },
- { tag: "h5", attrs: { level: 5 } },
- { tag: "h6", attrs: { level: 6 } }],
- toDOM(node: any) { return ["h" + node.attrs.level, 0]; }
- },
-
- // :: NodeSpec A code listing. Disallows marks or non-text inline
- // nodes by default. Represented as a `<pre>` element with a
- // `<code>` element inside of it.
- code_block: {
- content: "text*",
- marks: "",
- group: "block",
- code: true,
- defining: true,
- parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
- toDOM() { return preDOM; }
- },
-
- // :: NodeSpec The text node.
- text: {
- group: "inline"
- },
-
- // :: NodeSpec An inline image (`<img>`) node. Supports `src`,
- // `alt`, and `href` attributes. The latter two default to the empty
- // string.
- image: {
- inline: true,
- attrs: {
- src: {},
- alt: { default: null },
- title: { default: null }
- },
- group: "inline",
- draggable: true,
- parseDOM: [{
- tag: "img[src]", getAttrs(dom: any) {
- return {
- src: dom.getAttribute("src"),
- title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt")
- };
- }
- }],
- toDOM(node: any) { return ["img", node.attrs]; }
- },
-
- // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
- hard_break: {
- inline: true,
- group: "inline",
- selectable: false,
- parseDOM: [{ tag: "br" }],
- toDOM() { return brDOM; }
- },
-
- ordered_list: {
- ...orderedList,
- content: 'list_item+',
- group: 'block'
- },
- //this doesn't currently work for some reason
- bullet_list: {
- ...bulletList,
- content: 'list_item+',
- group: 'block',
- // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
- // toDOM() { return ulDOM }
- },
- //bullet_list: {
- // content: 'list_item+',
- // group: 'block',
- //active: blockActive(schema.nodes.bullet_list),
- //enable: wrapInList(schema.nodes.bullet_list),
- //run: wrapInList(schema.nodes.bullet_list),
- //select: state => true,
- // },
- list_item: {
- ...listItem,
- content: 'paragraph block*'
- }
+ // :: NodeSpec The top level document node.
+ doc: {
+ content: "block+"
+ },
+
+ // :: NodeSpec A plain paragraph textblock. Represented in the DOM
+ // as a `<p>` element.
+ paragraph: {
+ content: "inline*",
+ group: "block",
+ parseDOM: [{ tag: "p" }],
+ toDOM() { return pDOM; }
+ },
+
+ // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
+ blockquote: {
+ content: "block+",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "blockquote" }],
+ toDOM() { return blockquoteDOM; }
+ },
+
+ // :: NodeSpec A horizontal rule (`<hr>`).
+ horizontal_rule: {
+ group: "block",
+ parseDOM: [{ tag: "hr" }],
+ toDOM() { return hrDOM; }
+ },
+
+ // :: NodeSpec A heading textblock, with a `level` attribute that
+ // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
+ // `<h6>` elements.
+ heading: {
+ attrs: { level: { default: 1 } },
+ content: "inline*",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "h1", attrs: { level: 1 } },
+ { tag: "h2", attrs: { level: 2 } },
+ { tag: "h3", attrs: { level: 3 } },
+ { tag: "h4", attrs: { level: 4 } },
+ { tag: "h5", attrs: { level: 5 } },
+ { tag: "h6", attrs: { level: 6 } }],
+ toDOM(node: any) { return ["h" + node.attrs.level, 0]; }
+ },
+
+ // :: NodeSpec A code listing. Disallows marks or non-text inline
+ // nodes by default. Represented as a `<pre>` element with a
+ // `<code>` element inside of it.
+ code_block: {
+ content: "text*",
+ marks: "",
+ group: "block",
+ code: true,
+ defining: true,
+ parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
+ toDOM() { return preDOM; }
+ },
+
+ // :: NodeSpec The text node.
+ text: {
+ group: "inline"
+ },
+
+ // :: NodeSpec An inline image (`<img>`) node. Supports `src`,
+ // `alt`, and `href` attributes. The latter two default to the empty
+ // string.
+ image: {
+ inline: true,
+ attrs: {
+ src: {},
+ alt: { default: null },
+ title: { default: null }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "img[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt")
+ };
+ }
+ }],
+ toDOM(node: any) { return ["img", node.attrs]; }
+ },
+
+ // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
+ hard_break: {
+ inline: true,
+ group: "inline",
+ selectable: false,
+ parseDOM: [{ tag: "br" }],
+ toDOM() { return brDOM; }
+ },
+
+ ordered_list: {
+ ...orderedList,
+ content: 'list_item+',
+ group: 'block'
+ },
+ //this doesn't currently work for some reason
+ bullet_list: {
+ ...bulletList,
+ content: 'list_item+',
+ group: 'block',
+ // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
+ // toDOM() { return ulDOM }
+ },
+ //bullet_list: {
+ // content: 'list_item+',
+ // group: 'block',
+ //active: blockActive(schema.nodes.bullet_list),
+ //enable: wrapInList(schema.nodes.bullet_list),
+ //run: wrapInList(schema.nodes.bullet_list),
+ //select: state => true,
+ // },
+ list_item: {
+ ...listItem,
+ content: 'paragraph block*'
+ }
};
const emDOM: DOMOutputSpecArray = ["em", 0];
@@ -145,84 +145,179 @@ const underlineDOM: DOMOutputSpecArray = ["underline", 0];
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
export const marks: { [index: string]: MarkSpec } = {
- // :: MarkSpec A link. Has `href` and `title` attributes. `title`
- // defaults to the empty string. Rendered and parsed as an `<a>`
- // element.
- link: {
- attrs: {
- href: {},
- title: { default: null }
- },
- inclusive: false,
- parseDOM: [{
- tag: "a[href]", getAttrs(dom: any) {
- return { href: dom.getAttribute("href"), title: dom.getAttribute("title") };
- }
- }],
- toDOM(node: any) { return ["a", node.attrs, 0]; }
- },
-
- // :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
- // Has parse rules that also match `<i>` and `font-style: italic`.
- em: {
- parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }],
- toDOM() { return emDOM; }
- },
-
- // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
- // also match `<b>` and `font-weight: bold`.
- strong: {
- parseDOM: [{ tag: "strong" },
- { tag: "b" },
- { style: "font-weight" }],
- toDOM() { return strongDOM; }
- },
-
- underline: {
- parseDOM: [
- { tag: 'u' },
- { style: 'text-decoration=underline' }
- ],
- toDOM: () => ['span', {
- style: 'text-decoration:underline'
- }]
- },
-
- strikethrough: {
- parseDOM: [
- { tag: 'strike' },
- { style: 'text-decoration=line-through' },
- { style: 'text-decoration-line=line-through' }
- ],
- toDOM: () => ['span', {
- style: 'text-decoration-line:line-through'
- }]
- },
-
- subscript: {
- excludes: 'superscript',
- parseDOM: [
- { tag: 'sub' },
- { style: 'vertical-align=sub' }
- ],
- toDOM: () => ['sub']
- },
-
- superscript: {
- excludes: 'subscript',
- parseDOM: [
- { tag: 'sup' },
- { style: 'vertical-align=super' }
- ],
- toDOM: () => ['sup']
- },
-
-
- // :: MarkSpec Code font mark. Represented as a `<code>` element.
- code: {
- parseDOM: [{ tag: "code" }],
- toDOM() { return codeDOM; }
- }
+ // :: MarkSpec A link. Has `href` and `title` attributes. `title`
+ // defaults to the empty string. Rendered and parsed as an `<a>`
+ // element.
+ link: {
+ attrs: {
+ href: {},
+ title: { default: null }
+ },
+ inclusive: false,
+ parseDOM: [{
+ tag: "a[href]", getAttrs(dom: any) {
+ return { href: dom.getAttribute("href"), title: dom.getAttribute("title") };
+ }
+ }],
+ toDOM(node: any) { return ["a", node.attrs, 0]; }
+ },
+
+ // :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
+ // Has parse rules that also match `<i>` and `font-style: italic`.
+ em: {
+ parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }],
+ toDOM() { return emDOM; }
+ },
+
+ // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
+ // also match `<b>` and `font-weight: bold`.
+ strong: {
+ parseDOM: [{ tag: "strong" },
+ { tag: "b" },
+ { style: "font-weight" }],
+ toDOM() { return strongDOM; }
+ },
+
+ underline: {
+ parseDOM: [
+ { tag: 'u' },
+ { style: 'text-decoration=underline' }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration:underline'
+ }]
+ },
+
+ strikethrough: {
+ parseDOM: [
+ { tag: 'strike' },
+ { style: 'text-decoration=line-through' },
+ { style: 'text-decoration-line=line-through' }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration-line:line-through'
+ }]
+ },
+
+ subscript: {
+ excludes: 'superscript',
+ parseDOM: [
+ { tag: 'sub' },
+ { style: 'vertical-align=sub' }
+ ],
+ toDOM: () => ['sub']
+ },
+
+ superscript: {
+ excludes: 'subscript',
+ parseDOM: [
+ { tag: 'sup' },
+ { style: 'vertical-align=super' }
+ ],
+ toDOM: () => ['sup']
+ },
+
+
+ // :: MarkSpec Code font mark. Represented as a `<code>` element.
+ code: {
+ parseDOM: [{ tag: "code" }],
+ toDOM() { return codeDOM; }
+ },
+
+
+ /* FONTS */
+ timesNewRoman: {
+ parseDOM: [{ style: 'font-family: "Times New Roman", Times, serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: "Times New Roman", Times, serif;'
+ }]
+ },
+
+ arial: {
+ parseDOM: [{ style: 'font-family: Arial, Helvetica, sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: Arial, Helvetica, sans-serif;'
+ }]
+ },
+
+ georgia: {
+ parseDOM: [{ style: 'font-family: Georgia, serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: Georgia, serif;'
+ }]
+ },
+
+ comicSans: {
+ parseDOM: [{ style: 'font-family: "Comic Sans MS", cursive, sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: "Comic Sans MS", cursive, sans-serif;'
+ }]
+ },
+
+ tahoma: {
+ parseDOM: [{ style: 'font-family: Tahoma, Geneva, sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: Tahoma, Geneva, sans-serif;'
+ }]
+ },
+
+ impact: {
+ parseDOM: [{ style: 'font-family: Impact, Charcoal, sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: Impact, Charcoal, sans-serif;'
+ }]
+ },
+
+ /** FONT SIZES */
+
+ p10: {
+ parseDOM: [{ style: 'font-size: 10px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 10px;'
+ }]
+ },
+
+ p12: {
+ parseDOM: [{ style: 'font-size: 12px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 12px;'
+ }]
+ },
+
+ p16: {
+ parseDOM: [{ style: 'font-size: 16px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 16px;'
+ }]
+ },
+
+ p24: {
+ parseDOM: [{ style: 'font-size: 24px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 24px;'
+ }]
+ },
+
+ p32: {
+ parseDOM: [{ style: 'font-size: 32px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 32px;'
+ }]
+ },
+
+ p48: {
+ parseDOM: [{ style: 'font-size: 48px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 48px;'
+ }]
+ },
+
+ p72: {
+ parseDOM: [{ style: 'font-size: 72px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 72px;'
+ }]
+ },
};
// :: Schema
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
index ea580d104..9111d60f1 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/util/TooltipTextMenu.scss
@@ -1,8 +1,244 @@
@import "../views/global_variables";
+.ProseMirror-textblock-dropdown {
+ min-width: 3em;
+ }
+
+ .ProseMirror-menu {
+ margin: 0 -4px;
+ line-height: 1;
+ }
+
+ .ProseMirror-tooltip .ProseMirror-menu {
+ width: -webkit-fit-content;
+ width: fit-content;
+ white-space: pre;
+ }
+
+ .ProseMirror-menuitem {
+ margin-right: 3px;
+ display: inline-block;
+ }
+
+ .ProseMirror-menuseparator {
+ // border-right: 1px solid #ddd;
+ margin-right: 3px;
+ }
+
+ .ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
+ font-size: 90%;
+ white-space: nowrap;
+ }
+
+ .ProseMirror-menu-dropdown {
+ vertical-align: 1px;
+ cursor: pointer;
+ position: relative;
+ padding-right: 15px;
+ }
+
+ .ProseMirror-menu-dropdown-wrap {
+ padding: 1px 0 1px 4px;
+ display: inline-block;
+ position: relative;
+ }
+
+ .ProseMirror-menu-dropdown:after {
+ content: "";
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 2px);
+ }
+
+ .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
+ position: absolute;
+ background: $dark-color;
+ color:white;
+ border: 1px solid #aaa;
+ padding: 2px;
+ }
+
+ .ProseMirror-menu-dropdown-menu {
+ z-index: 15;
+ min-width: 6em;
+ }
+
+ .ProseMirror-menu-dropdown-item {
+ cursor: pointer;
+ padding: 2px 8px 2px 4px;
+ width: auto;
+ }
+
+ .ProseMirror-menu-dropdown-item:hover {
+ background: #2e2b2b;
+ }
+
+ .ProseMirror-menu-submenu-wrap {
+ position: relative;
+ margin-right: -4px;
+ }
+
+ .ProseMirror-menu-submenu-label:after {
+ content: "";
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ border-left: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 4px);
+ }
+
+ .ProseMirror-menu-submenu {
+ display: none;
+ min-width: 4em;
+ left: 100%;
+ top: -3px;
+ }
+
+ .ProseMirror-menu-active {
+ background: #eee;
+ border-radius: 4px;
+ }
+
+ .ProseMirror-menu-active {
+ background: #eee;
+ border-radius: 4px;
+ }
+
+ .ProseMirror-menu-disabled {
+ opacity: .3;
+ }
+
+ .ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
+ display: block;
+ }
+
+ .ProseMirror-menubar {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+ position: relative;
+ min-height: 1em;
+ color: white;
+ padding: 1px 6px;
+ top: 0; left: 0; right: 0;
+ border-bottom: 1px solid silver;
+ background:$dark-color;
+ z-index: 10;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ overflow: visible;
+ }
+
+ .ProseMirror-icon {
+ display: inline-block;
+ line-height: .8;
+ vertical-align: -2px; /* Compensate for padding */
+ padding: 2px 8px;
+ cursor: pointer;
+ }
+
+ .ProseMirror-menu-disabled.ProseMirror-icon {
+ cursor: default;
+ }
+
+ .ProseMirror-icon svg {
+ fill: currentColor;
+ height: 1em;
+ }
+
+ .ProseMirror-icon span {
+ vertical-align: text-top;
+ }
+.ProseMirror-example-setup-style hr {
+ padding: 2px 10px;
+ border: none;
+ margin: 1em 0;
+ }
+
+ .ProseMirror-example-setup-style hr:after {
+ content: "";
+ display: block;
+ height: 1px;
+ background-color: silver;
+ line-height: 2px;
+ }
+
+ .ProseMirror ul, .ProseMirror ol {
+ padding-left: 30px;
+ }
+
+ .ProseMirror blockquote {
+ padding-left: 1em;
+ border-left: 3px solid #eee;
+ margin-left: 0; margin-right: 0;
+ }
+
+ .ProseMirror-example-setup-style img {
+ cursor: default;
+ }
+
+ .ProseMirror-prompt {
+ background: white;
+ padding: 5px 10px 5px 15px;
+ border: 1px solid silver;
+ position: fixed;
+ border-radius: 3px;
+ z-index: 11;
+ box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
+ }
+
+ .ProseMirror-prompt h5 {
+ margin: 0;
+ font-weight: normal;
+ font-size: 100%;
+ color: #444;
+ }
+
+ .ProseMirror-prompt input[type="text"],
+ .ProseMirror-prompt textarea {
+ background: #eee;
+ border: none;
+ outline: none;
+ }
+
+ .ProseMirror-prompt input[type="text"] {
+ padding: 0 4px;
+ }
+
+ .ProseMirror-prompt-close {
+ position: absolute;
+ left: 2px; top: 1px;
+ color: #666;
+ border: none; background: transparent; padding: 0;
+ }
+
+ .ProseMirror-prompt-close:after {
+ content: "✕";
+ font-size: 12px;
+ }
+
+ .ProseMirror-invalid {
+ background: #ffc;
+ border: 1px solid #cc7;
+ border-radius: 4px;
+ padding: 5px 10px;
+ position: absolute;
+ min-width: 10em;
+ }
+
+ .ProseMirror-prompt-buttons {
+ margin-top: 5px;
+ display: none;
+ }
+
.tooltipMenu {
position: absolute;
- z-index: 20;
+ z-index: 200;
background: $dark-color;
border: 1px solid silver;
border-radius: 4px;
@@ -39,7 +275,7 @@
display: inline-block;
border-right: 1px solid rgba(0, 0, 0, 0.2);
//color: rgb(19, 18, 18);
- color: $light-color;
+ color: white;
line-height: 1;
padding: 0px 2px;
margin: 1px;
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 7951e5686..3869db41a 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -1,38 +1,47 @@
import { action, IReactionDisposer, reaction } from "mobx";
-import { baseKeymap } from "prosemirror-commands";
+import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
+import { baseKeymap, lift } from "prosemirror-commands";
import { history, redo, undo } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-import { EditorState, Transaction, NodeSelection } from "prosemirror-state";
+import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "./RichTextSchema";
-import { Schema, NodeType } from "prosemirror-model";
+import { Schema, NodeType, MarkType } from "prosemirror-model";
import React = require("react");
import "./TooltipTextMenu.scss";
const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, bulletList } from 'prosemirror-schema-list';
-import { faListUl } from '@fortawesome/free-solid-svg-icons';
+import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list';
+import {
+ faListUl,
+} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FieldViewProps } from "../views/nodes/FieldView";
import { throwStatement } from "babel-types";
+const SVG = "http://www.w3.org/2000/svg";
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
private tooltip: HTMLElement;
+ private num_icons = 0;
+ private view: EditorView;
+ private fontStyles: MarkType[];
+ private fontSizes: MarkType[];
private editorProps: FieldViewProps;
constructor(view: EditorView, editorProps: FieldViewProps) {
+ this.view = view;
this.editorProps = editorProps;
this.tooltip = document.createElement("div");
this.tooltip.className = "tooltipMenu";
//add the div which is the tooltip
- view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
+ view.dom.parentNode!.appendChild(this.tooltip);
//add additional icons
library.add(faListUl);
-
//add the buttons to the tooltip
let items = [
{ command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") },
@@ -41,36 +50,143 @@ export class TooltipTextMenu {
{ command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") },
{ command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") },
{ command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") },
- //this doesn't work currently - look into notion of active block
{ command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") },
+ { command: lift, dom: this.icon("<", "lift") },
];
- items.forEach(({ dom }) => this.tooltip.appendChild(dom));
-
- //pointer down handler to activate button effects
- this.tooltip.addEventListener("pointerdown", e => {
- e.preventDefault();
- view.focus();
- items.forEach(({ command, dom }) => {
- if (dom.contains(e.target as Node)) {
- let active = command(view.state, view.dispatch, view);
- //uncomment this if we want the bullet button to disappear if current selection is bulleted
- // dom.style.display = active ? "" : "none"
- }
+ //add menu items
+ items.forEach(({ dom, command }) => {
+ this.tooltip.appendChild(dom);
+
+ //pointer down handler to activate button effects
+ dom.addEventListener("pointerdown", e => {
+ e.preventDefault();
+ view.focus();
+ command(view.state, view.dispatch, view);
});
+
});
+ //dropdowns
+ //list of font btns to add
+ this.fontStyles = [
+ schema.marks.timesNewRoman,
+ schema.marks.arial,
+ schema.marks.georgia,
+ schema.marks.comicSans,
+ schema.marks.tahoma,
+ schema.marks.impact,
+ ];
+ this.fontSizes = [
+ schema.marks.p10,
+ schema.marks.p12,
+ schema.marks.p16,
+ schema.marks.p24,
+ schema.marks.p32,
+ schema.marks.p48,
+ schema.marks.p72,
+ ];
+ this.addFontDropdowns();
+
this.update(view, undefined);
}
+ //adds font size and font style dropdowns
+ addFontDropdowns() {
+ //filtering function - might be unecessary
+ let cut = (arr: MenuItem[]) => arr.filter(x => x);
+ let fontBtns = [
+ this.dropdownBtn("Times New Roman", "font-family: Times New Roman, Times, serif; width: 120px;", schema.marks.timesNewRoman, this.view, this.changeToMarkInGroup, this.fontStyles),
+ this.dropdownBtn("Arial", "font-family: Arial, Helvetica, sans-serif; width: 120px;", schema.marks.arial, this.view, this.changeToMarkInGroup, this.fontStyles),
+ this.dropdownBtn("Georgia", "font-family: Georgia, serif; width: 120px; width: 120px;", schema.marks.georgia, this.view, this.changeToMarkInGroup, this.fontStyles),
+ this.dropdownBtn("ComicSans", "font-family: Comic Sans MS, cursive, sans-serif; width: 120px;", schema.marks.comicSans, this.view, this.changeToMarkInGroup, this.fontStyles),
+ this.dropdownBtn("Tahoma", "font-family: Tahoma, Geneva, sans-serif; width: 120px;", schema.marks.tahoma, this.view, this.changeToMarkInGroup, this.fontStyles),
+ this.dropdownBtn("Impact", "font-family: Impact, Charcoal, sans-serif; width: 120px;", schema.marks.impact, this.view, this.changeToMarkInGroup, this.fontStyles),
+ ];
+
+ let fontSizeBtns = [
+ this.dropdownBtn("10", "width: 50px;", schema.marks.p10, this.view, this.changeToMarkInGroup, this.fontSizes),
+ this.dropdownBtn("12", "width: 50px;", schema.marks.p12, this.view, this.changeToMarkInGroup, this.fontSizes),
+ this.dropdownBtn("16", "width: 50px;", schema.marks.p16, this.view, this.changeToMarkInGroup, this.fontSizes),
+ this.dropdownBtn("24", "width: 50px;", schema.marks.p24, this.view, this.changeToMarkInGroup, this.fontSizes),
+ this.dropdownBtn("32", "width: 50px;", schema.marks.p32, this.view, this.changeToMarkInGroup, this.fontSizes),
+ this.dropdownBtn("48", "width: 50px;", schema.marks.p48, this.view, this.changeToMarkInGroup, this.fontSizes),
+ this.dropdownBtn("72", "width: 50px;", schema.marks.p72, this.view, this.changeToMarkInGroup, this.fontSizes),
+ ];
+
+ //dropdown to hold font btns
+ let dd_fontStyle = new Dropdown(cut(fontBtns), { label: "Font Style", css: "color:white;" }) as MenuItem;
+ let dd_fontSize = new Dropdown(cut(fontSizeBtns), { label: "Font Size", css: "color:white;" }) as MenuItem;
+ this.tooltip.appendChild(dd_fontStyle.render(this.view).dom);
+ this.tooltip.appendChild(dd_fontSize.render(this.view).dom);
+ }
+
+ //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text
+ changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) {
+ let { empty, $cursor, ranges } = view.state.selection as TextSelection;
+ let state = view.state;
+ let dispatch = view.dispatch;
+
+ //remove all other active font marks
+ fontMarks.forEach((type) => {
+ if (dispatch) {
+ if ($cursor) {
+ if (type.isInSet(state.storedMarks || $cursor.marks())) {
+ dispatch(state.tr.removeStoredMark(type));
+ }
+ } else {
+ let has = false, tr = state.tr;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ let { $from, $to } = ranges[i];
+ has = state.doc.rangeHasMark($from.pos, $to.pos, type);
+ }
+ for (let i of ranges) {
+ let { $from, $to } = i;
+ if (has) {
+ toggleMark(type)(view.state, view.dispatch, view);
+ }
+ }
+ }
+ }
+ }); //actually apply font
+ return toggleMark(markType)(view.state, view.dispatch, view);
+ }
+
+ //makes a button for the drop down
+ //css is the style you want applied to the button
+ dropdownBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) {
+ return new MenuItem({
+ title: "",
+ label: label,
+ execEvent: "",
+ class: "menuicon",
+ css: css,
+ enable(state) { return true; },
+ run() {
+ changeToMarkInGroup(markType, view, groupMarks);
+ }
+ });
+ }
// Helper function to create menu icons
icon(text: string, name: string) {
let span = document.createElement("span");
span.className = "menuicon " + name;
span.title = name;
span.textContent = text;
+ span.style.color = "white";
return span;
}
+ //method for checking whether node can be inserted
+ canInsert(state: EditorState, nodeType: NodeType<Schema<string, string>>) {
+ let $from = state.selection.$from;
+ for (let d = $from.depth; d >= 0; d--) {
+ let index = $from.index(d);
+ if ($from.node(d).canReplaceWith(index, index, nodeType)) return true;
+ }
+ return false;
+ }
+
+
//adapted this method - use it to check if block has a tag (ie bulleting)
blockActive(type: NodeType<Schema<string, string>>, state: EditorState) {
let attrs = {};
@@ -89,15 +205,6 @@ export class TooltipTextMenu {
}
}
- //this doesn't currently work but could be used to use icons for buttons
- unorderedListIcon(): HTMLSpanElement {
- let span = document.createElement("span");
- let icon = document.createElement("FontAwesomeIcon");
- icon.className = "menuicon fa fa-smile-o";
- span.appendChild(icon);
- return span;
- }
-
// Create an icon for a heading at the given level
heading(level: number) {
return {
@@ -132,10 +239,9 @@ export class TooltipTextMenu {
let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale;
let mid = Math.min(start.left, end.left) + width;
- //THIS WIDTH IS 15 * NUMBER OF ICONS + 15
- this.tooltip.style.width = 122 + "px";
+ this.tooltip.style.width = 220 + "px";
this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
}
destroy() { this.tooltip.remove(); }
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index c3a2e88ac..9cd6c8a39 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -15,6 +15,11 @@
padding: 0px;
font-size: 100%;
}
+
+ul {
+ list-style-type: disc;
+}
+
#schema-options-header {
text-align: center;
padding: 0px;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 81f2146e4..31809f30b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -46,6 +46,7 @@
}
.formattedTextBox-cont {
background: $light-color-secondary;
+ overflow: visible;
}
opacity: 0.99;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8ea747b1c..08f94c820 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -76,7 +76,12 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
history(),
keymap({ "Mod-z": undo, "Mod-y": redo }),
keymap(baseKeymap),
- this.tooltipMenuPlugin()
+ this.tooltipMenuPlugin(),
+ new Plugin({
+ props: {
+ attributes: { class: "ProseMirror-example-setup-style" }
+ }
+ })
] : [
history(),
keymap({ "Mod-z": undo, "Mod-y": redo }),
@@ -162,14 +167,18 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
// doc.SetData(fieldKey, e.target.value, RichTextField);
}
onPointerDown = (e: React.PointerEvent): void => {
+ console.log("pointer down");
if (e.button === 1 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ console.log("first");
e.stopPropagation();
}
if (e.button === 2) {
+ console.log("second");
e.preventDefault();
}
}
onPointerUp = (e: React.PointerEvent): void => {
+ console.log("pointer up");
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
@@ -230,6 +239,7 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
return (
<div
className={`formattedTextBox-cont`}
+ style={{ overflow: "visible" }}
onKeyDown={this.onKeyPress}
onKeyPress={this.onKeyPress}
onFocus={this.onFocused}
diff --git a/src/server/public/files/upload_1b4818f39ea324b5a687bb1ade3dca6c.jpg b/src/server/public/files/upload_1b4818f39ea324b5a687bb1ade3dca6c.jpg
new file mode 100644
index 000000000..aeb10c4b0
--- /dev/null
+++ b/src/server/public/files/upload_1b4818f39ea324b5a687bb1ade3dca6c.jpg
Binary files differ
diff --git a/src/server/public/files/upload_1f1c6cfef33e5992fa860802e8c466a7.jpg b/src/server/public/files/upload_1f1c6cfef33e5992fa860802e8c466a7.jpg
new file mode 100644
index 000000000..a2c1d8a46
--- /dev/null
+++ b/src/server/public/files/upload_1f1c6cfef33e5992fa860802e8c466a7.jpg
Binary files differ
diff --git a/src/server/public/files/upload_2045f363aa9cf281407703ca242aad1a.jpg b/src/server/public/files/upload_2045f363aa9cf281407703ca242aad1a.jpg
new file mode 100644
index 000000000..c19b31a38
--- /dev/null
+++ b/src/server/public/files/upload_2045f363aa9cf281407703ca242aad1a.jpg
Binary files differ
diff --git a/src/server/public/files/upload_25bffd90c080c27f5ac822984406b958.jpg b/src/server/public/files/upload_25bffd90c080c27f5ac822984406b958.jpg
new file mode 100644
index 000000000..3614b42eb
--- /dev/null
+++ b/src/server/public/files/upload_25bffd90c080c27f5ac822984406b958.jpg
Binary files differ
diff --git a/src/server/public/files/upload_261f11dc39ad568212b5c7e39d1e6d13.jpg b/src/server/public/files/upload_261f11dc39ad568212b5c7e39d1e6d13.jpg
new file mode 100644
index 000000000..ecd12d9cb
--- /dev/null
+++ b/src/server/public/files/upload_261f11dc39ad568212b5c7e39d1e6d13.jpg
Binary files differ
diff --git a/src/server/public/files/upload_26bcc62639141ba64e603daebb5bf5d3.png b/src/server/public/files/upload_26bcc62639141ba64e603daebb5bf5d3.png
new file mode 100644
index 000000000..e2297cb3c
--- /dev/null
+++ b/src/server/public/files/upload_26bcc62639141ba64e603daebb5bf5d3.png
Binary files differ
diff --git a/src/server/public/files/upload_2d77d0773612e4723b78118ac50a2929.jpg b/src/server/public/files/upload_2d77d0773612e4723b78118ac50a2929.jpg
new file mode 100644
index 000000000..261a0ceff
--- /dev/null
+++ b/src/server/public/files/upload_2d77d0773612e4723b78118ac50a2929.jpg
Binary files differ
diff --git a/src/server/public/files/upload_2de9ad4dc687c53760c39f724c9a08a5.jpg b/src/server/public/files/upload_2de9ad4dc687c53760c39f724c9a08a5.jpg
new file mode 100644
index 000000000..6b6ec3c3f
--- /dev/null
+++ b/src/server/public/files/upload_2de9ad4dc687c53760c39f724c9a08a5.jpg
Binary files differ
diff --git a/src/server/public/files/upload_4abb568aa7cce9d291532c3d0da97102.jpg b/src/server/public/files/upload_4abb568aa7cce9d291532c3d0da97102.jpg
new file mode 100644
index 000000000..f6332670c
--- /dev/null
+++ b/src/server/public/files/upload_4abb568aa7cce9d291532c3d0da97102.jpg
Binary files differ
diff --git a/src/server/public/files/upload_54c34aaca5a7bf510cebad461ec39512.png b/src/server/public/files/upload_54c34aaca5a7bf510cebad461ec39512.png
new file mode 100644
index 000000000..e2297cb3c
--- /dev/null
+++ b/src/server/public/files/upload_54c34aaca5a7bf510cebad461ec39512.png
Binary files differ
diff --git a/src/server/public/files/upload_562b1e527300df8b350eeab094b3e1f1.jpg b/src/server/public/files/upload_562b1e527300df8b350eeab094b3e1f1.jpg
new file mode 100644
index 000000000..db40705dd
--- /dev/null
+++ b/src/server/public/files/upload_562b1e527300df8b350eeab094b3e1f1.jpg
Binary files differ
diff --git a/src/server/public/files/upload_6a26d3f7008a8c79ee5fc8054ba69996.jpg b/src/server/public/files/upload_6a26d3f7008a8c79ee5fc8054ba69996.jpg
new file mode 100644
index 000000000..f0417a752
--- /dev/null
+++ b/src/server/public/files/upload_6a26d3f7008a8c79ee5fc8054ba69996.jpg
Binary files differ
diff --git a/src/server/public/files/upload_70fa5e0c3f393504349d5865e28f4cac.jpg b/src/server/public/files/upload_70fa5e0c3f393504349d5865e28f4cac.jpg
new file mode 100644
index 000000000..395f8ec21
--- /dev/null
+++ b/src/server/public/files/upload_70fa5e0c3f393504349d5865e28f4cac.jpg
Binary files differ
diff --git a/src/server/public/files/upload_8155b5b0f57da107bb07083c04e78943.jpg b/src/server/public/files/upload_8155b5b0f57da107bb07083c04e78943.jpg
new file mode 100644
index 000000000..53d9315a9
--- /dev/null
+++ b/src/server/public/files/upload_8155b5b0f57da107bb07083c04e78943.jpg
Binary files differ
diff --git a/src/server/public/files/upload_88f588574e0efc415186af935114af9a.jpg b/src/server/public/files/upload_88f588574e0efc415186af935114af9a.jpg
new file mode 100644
index 000000000..b72dbc482
--- /dev/null
+++ b/src/server/public/files/upload_88f588574e0efc415186af935114af9a.jpg
Binary files differ
diff --git a/src/server/public/files/upload_8d1c253f93f77c69c0c04ae3efb7d714.png b/src/server/public/files/upload_8d1c253f93f77c69c0c04ae3efb7d714.png
new file mode 100644
index 000000000..e2297cb3c
--- /dev/null
+++ b/src/server/public/files/upload_8d1c253f93f77c69c0c04ae3efb7d714.png
Binary files differ
diff --git a/src/server/public/files/upload_9ef80158609f5ff739087aecad367b9d.jpg b/src/server/public/files/upload_9ef80158609f5ff739087aecad367b9d.jpg
new file mode 100644
index 000000000..84423538c
--- /dev/null
+++ b/src/server/public/files/upload_9ef80158609f5ff739087aecad367b9d.jpg
Binary files differ
diff --git a/src/server/public/files/upload_c39a7e0d7e8d35bb18461a5a0aa063bf.jpg b/src/server/public/files/upload_c39a7e0d7e8d35bb18461a5a0aa063bf.jpg
new file mode 100644
index 000000000..dc7ec2f33
--- /dev/null
+++ b/src/server/public/files/upload_c39a7e0d7e8d35bb18461a5a0aa063bf.jpg
Binary files differ
diff --git a/src/server/public/files/upload_c6b81ab4eb70465a7e9b45d5c8f3ecaa.jpg b/src/server/public/files/upload_c6b81ab4eb70465a7e9b45d5c8f3ecaa.jpg
new file mode 100644
index 000000000..4422124a1
--- /dev/null
+++ b/src/server/public/files/upload_c6b81ab4eb70465a7e9b45d5c8f3ecaa.jpg
Binary files differ
diff --git a/src/server/public/files/upload_c99ec7a8a2df0b2f90479fde7d70c2eb.jpg b/src/server/public/files/upload_c99ec7a8a2df0b2f90479fde7d70c2eb.jpg
new file mode 100644
index 000000000..3747ca985
--- /dev/null
+++ b/src/server/public/files/upload_c99ec7a8a2df0b2f90479fde7d70c2eb.jpg
Binary files differ
diff --git a/src/server/public/files/upload_cec1cfcc67cfe5889de4098a49fec45e.jpg b/src/server/public/files/upload_cec1cfcc67cfe5889de4098a49fec45e.jpg
new file mode 100644
index 000000000..95053d772
--- /dev/null
+++ b/src/server/public/files/upload_cec1cfcc67cfe5889de4098a49fec45e.jpg
Binary files differ
diff --git a/src/server/public/files/upload_f27688fe92dc7de398e957e5d96e1a22.jpg b/src/server/public/files/upload_f27688fe92dc7de398e957e5d96e1a22.jpg
new file mode 100644
index 000000000..9841bad3f
--- /dev/null
+++ b/src/server/public/files/upload_f27688fe92dc7de398e957e5d96e1a22.jpg
Binary files differ