aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEleanor Eng <eleanor_eng@brown.edu>2019-06-04 10:29:02 -0400
committerEleanor Eng <eleanor_eng@brown.edu>2019-06-04 10:29:02 -0400
commit376ebd44a16dfa04aacd3582e87767aed1a01f36 (patch)
tree3a9e623cf6689e1ea6975954596bf5bda6303249
parent8f14e688220096ccecfd1aa0dd54b00e48f92270 (diff)
parent6f49d067b58caf6297f7ae7687cf05b627c27a1d (diff)
merge with master
-rw-r--r--.vscode/launch.json51
-rw-r--r--deploy/debug/repl.html14
-rw-r--r--deploy/debug/viewer.html1
-rw-r--r--package.json13
-rw-r--r--solr/conf/lang/contractions_ca.txt8
-rw-r--r--solr/conf/lang/contractions_fr.txt15
-rw-r--r--solr/conf/lang/contractions_ga.txt5
-rw-r--r--solr/conf/lang/contractions_it.txt23
-rw-r--r--solr/conf/lang/hyphenations_ga.txt5
-rw-r--r--solr/conf/lang/stemdict_nl.txt6
-rw-r--r--solr/conf/lang/stoptags_ja.txt420
-rw-r--r--solr/conf/lang/stopwords_ar.txt125
-rw-r--r--solr/conf/lang/stopwords_bg.txt193
-rw-r--r--solr/conf/lang/stopwords_ca.txt220
-rw-r--r--solr/conf/lang/stopwords_cz.txt172
-rw-r--r--solr/conf/lang/stopwords_da.txt110
-rw-r--r--solr/conf/lang/stopwords_de.txt294
-rw-r--r--solr/conf/lang/stopwords_el.txt78
-rw-r--r--solr/conf/lang/stopwords_en.txt54
-rw-r--r--solr/conf/lang/stopwords_es.txt356
-rw-r--r--solr/conf/lang/stopwords_eu.txt99
-rw-r--r--solr/conf/lang/stopwords_fa.txt313
-rw-r--r--solr/conf/lang/stopwords_fi.txt97
-rw-r--r--solr/conf/lang/stopwords_fr.txt186
-rw-r--r--solr/conf/lang/stopwords_ga.txt110
-rw-r--r--solr/conf/lang/stopwords_gl.txt161
-rw-r--r--solr/conf/lang/stopwords_hi.txt235
-rw-r--r--solr/conf/lang/stopwords_hu.txt211
-rw-r--r--solr/conf/lang/stopwords_hy.txt46
-rw-r--r--solr/conf/lang/stopwords_id.txt359
-rw-r--r--solr/conf/lang/stopwords_it.txt303
-rw-r--r--solr/conf/lang/stopwords_ja.txt127
-rw-r--r--solr/conf/lang/stopwords_lv.txt172
-rw-r--r--solr/conf/lang/stopwords_nl.txt119
-rw-r--r--solr/conf/lang/stopwords_no.txt194
-rw-r--r--solr/conf/lang/stopwords_pt.txt253
-rw-r--r--solr/conf/lang/stopwords_ro.txt233
-rw-r--r--solr/conf/lang/stopwords_ru.txt243
-rw-r--r--solr/conf/lang/stopwords_sv.txt133
-rw-r--r--solr/conf/lang/stopwords_th.txt119
-rw-r--r--solr/conf/lang/stopwords_tr.txt212
-rw-r--r--solr/conf/lang/userdict_ja.txt29
-rw-r--r--solr/conf/params.json20
-rw-r--r--solr/conf/protwords.txt21
-rw-r--r--solr/conf/schema.xml61
-rw-r--r--solr/conf/schema.xml~23
-rw-r--r--solr/conf/solrconfig.xml1328
-rw-r--r--solr/conf/solrconfig.xml~1358
-rw-r--r--solr/conf/stopwords.txt14
-rw-r--r--solr/conf/synonyms.txt29
-rw-r--r--src/Utils.ts14
-rw-r--r--src/client/DocServer.ts41
-rw-r--r--src/client/documents/Documents.ts95
-rw-r--r--src/client/goldenLayout.d.ts3
-rw-r--r--src/client/goldenLayout.js5360
-rw-r--r--src/client/northstar/dash-fields/HistogramField.ts18
-rw-r--r--src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts4
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx28
-rw-r--r--src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx3
-rw-r--r--src/client/northstar/manager/Gateway.ts12
-rw-r--r--src/client/northstar/model/ModelHelpers.ts7
-rw-r--r--src/client/northstar/model/idea/idea.ts6
-rw-r--r--src/client/northstar/operations/BaseOperation.ts17
-rw-r--r--src/client/northstar/operations/HistogramOperation.ts17
-rw-r--r--src/client/util/DocumentManager.ts90
-rw-r--r--src/client/util/DragManager.ts65
-rw-r--r--src/client/util/History.ts122
-rw-r--r--src/client/util/RichTextSchema.tsx85
-rw-r--r--src/client/util/Scripting.ts7
-rw-r--r--src/client/util/SearchUtil.ts26
-rw-r--r--src/client/util/SelectionManager.ts21
-rw-r--r--src/client/util/TooltipTextMenu.scss26
-rw-r--r--src/client/util/TooltipTextMenu.tsx110
-rw-r--r--src/client/util/UndoManager.ts5
-rw-r--r--src/client/util/type_decls.d145
-rw-r--r--src/client/views/ContextMenu.scss86
-rw-r--r--src/client/views/ContextMenu.tsx26
-rw-r--r--src/client/views/ContextMenuItem.tsx59
-rw-r--r--src/client/views/DocumentDecorations.scss9
-rw-r--r--src/client/views/DocumentDecorations.tsx115
-rw-r--r--src/client/views/EditableView.scss17
-rw-r--r--src/client/views/EditableView.tsx17
-rw-r--r--src/client/views/InkingCanvas.scss41
-rw-r--r--src/client/views/InkingCanvas.tsx10
-rw-r--r--src/client/views/InkingControl.tsx7
-rw-r--r--src/client/views/Main.scss43
-rw-r--r--src/client/views/Main.tsx341
-rw-r--r--src/client/views/MainOverlayTextBox.tsx43
-rw-r--r--src/client/views/MainView.tsx316
-rw-r--r--src/client/views/PresentationView.scss71
-rw-r--r--src/client/views/PresentationView.tsx142
-rw-r--r--src/client/views/PreviewCursor.tsx15
-rw-r--r--src/client/views/SearchBox.scss102
-rw-r--r--src/client/views/SearchBox.tsx207
-rw-r--r--src/client/views/SearchItem.tsx73
-rw-r--r--src/client/views/TemplateMenu.tsx29
-rw-r--r--src/client/views/Templates.tsx70
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx43
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx221
-rw-r--r--src/client/views/collections/CollectionPDFView.scss39
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx4
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss90
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx327
-rw-r--r--src/client/views/collections/CollectionStackingView.scss51
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx179
-rw-r--r--src/client/views/collections/CollectionSubView.tsx38
-rw-r--r--src/client/views/collections/CollectionTreeView.scss65
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx142
-rw-r--r--src/client/views/collections/CollectionVideoView.scss4
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx98
-rw-r--r--src/client/views/collections/CollectionView.tsx45
-rw-r--r--src/client/views/collections/ParentDocumentSelector.scss22
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx94
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx16
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx124
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx67
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss16
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx173
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx235
-rw-r--r--src/client/views/globalCssVariables.scss8
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx166
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx52
-rw-r--r--src/client/views/nodes/DocumentView.tsx301
-rw-r--r--src/client/views/nodes/FieldView.tsx23
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss6
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx191
-rw-r--r--src/client/views/nodes/IconBox.scss9
-rw-r--r--src/client/views/nodes/IconBox.tsx30
-rw-r--r--src/client/views/nodes/ImageBox.scss6
-rw-r--r--src/client/views/nodes/ImageBox.tsx126
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx12
-rw-r--r--src/client/views/nodes/KeyValuePair.scss8
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx23
-rw-r--r--src/client/views/nodes/LinkBox.tsx23
-rw-r--r--src/client/views/nodes/LinkEditor.tsx5
-rw-r--r--src/client/views/nodes/LinkMenu.tsx15
-rw-r--r--src/client/views/nodes/PDFBox.scss4
-rw-r--r--src/client/views/nodes/PDFBox.tsx170
-rw-r--r--src/client/views/nodes/VideoBox.scss6
-rw-r--r--src/client/views/nodes/VideoBox.tsx149
-rw-r--r--src/debug/Repl.tsx63
-rw-r--r--src/debug/Test.tsx7
-rw-r--r--src/debug/Viewer.tsx353
-rw-r--r--src/fields/TemplateField.ts43
-rw-r--r--src/mobile/ImageUpload.scss25
-rw-r--r--src/mobile/ImageUpload.tsx123
-rw-r--r--src/new_fields/CursorField.ts63
-rw-r--r--src/new_fields/DateField.ts23
-rw-r--r--src/new_fields/Doc.ts157
-rw-r--r--src/new_fields/FieldSymbols.ts10
-rw-r--r--src/new_fields/HtmlField.ts7
-rw-r--r--src/new_fields/IconField.ts7
-rw-r--r--src/new_fields/InkField.ts12
-rw-r--r--src/new_fields/List.ts75
-rw-r--r--src/new_fields/ObjectField.ts12
-rw-r--r--src/new_fields/Proxy.ts9
-rw-r--r--src/new_fields/RefField.ts5
-rw-r--r--src/new_fields/RichTextField.ts7
-rw-r--r--src/new_fields/Schema.ts18
-rw-r--r--src/new_fields/Types.ts17
-rw-r--r--src/new_fields/URLField.ts16
-rw-r--r--src/new_fields/util.ts38
-rw-r--r--src/server/RouteStore.ts1
-rw-r--r--src/server/Search.ts49
-rw-r--r--src/server/authentication/models/current_user_utils.ts69
-rw-r--r--src/server/database.ts115
-rw-r--r--src/server/downsize.ts40
-rw-r--r--src/server/index.ts176
-rw-r--r--src/server/public/files/.gitignore3
-rw-r--r--src/server/remapUrl.ts59
-rw-r--r--src/server/updateSearch.ts101
-rw-r--r--webpack.config.js1
174 files changed, 19903 insertions, 2374 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index fb91a1080..3b6549767 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -14,6 +14,47 @@
"webRoot": "${workspaceFolder}",
},
{
+ "type": "firefox",
+ "request": "launch",
+ "name": "Launch Firefox against localhost",
+ "sourceMaps": "client",
+ "reAttach": true,
+ "url": "http://localhost:1050/login",
+ "webRoot": "${workspaceFolder}",
+ },
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Launch Chrome against Dash server",
+ "sourceMaps": true,
+ "breakOnLoad": true,
+ "url": "http://dash-web.eastus2.cloudapp.azure.com:1050/login",
+ "webRoot": "${workspaceFolder}",
+ },
+ {
+ "type": "node",
+ "request": "attach",
+ "name": "Typescript Server",
+ "protocol": "inspector",
+ "port": 9229,
+ "localRoot": "${workspaceFolder}",
+ "remoteRoot": "."
+ },
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Current TS File",
+ "runtimeExecutable": "npx",
+ "runtimeArgs": [
+ "ts-node-dev",
+ "--nolazy",
+ "--inspect",
+ "--",
+ "${relativeFile}"
+ ],
+ "port": 9229
+ },
+ {
"type": "node",
"request": "launch",
"name": "Mocha Tests",
@@ -47,15 +88,5 @@
"internalConsoleOptions": "openOnSessionStart",
"protocol": "inspector"
},
- {
- "type": "node",
- "request": "launch",
- "name": "Launch via NPM",
- "runtimeExecutable": "npm",
- "runtimeArgs": [
- "start"
- ],
- "port": 9229
- }
]
} \ No newline at end of file
diff --git a/deploy/debug/repl.html b/deploy/debug/repl.html
new file mode 100644
index 000000000..8ab07ec49
--- /dev/null
+++ b/deploy/debug/repl.html
@@ -0,0 +1,14 @@
+<html>
+
+<head>
+ <title>Debug REPL</title>
+ <link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script>
+</head>
+
+<body>
+ <div id="root"></div>
+ <script src="../repl.js"></script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/deploy/debug/viewer.html b/deploy/debug/viewer.html
index 3785a6602..8c265ccb8 100644
--- a/deploy/debug/viewer.html
+++ b/deploy/debug/viewer.html
@@ -3,6 +3,7 @@
<head>
<title>Document Debugger</title>
<link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script>
</head>
<body>
diff --git a/package.json b/package.json
index 1eb546a80..df0767b50 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"main": "index.js",
"scripts": {
"start": "ts-node-dev -- src/server/index.ts",
+ "debug": "ts-node-dev --inspect -- src/server/index.ts",
"build": "webpack --env production",
"test": "mocha -r ts-node/register test/**/*.ts",
"tsc": "tsc"
@@ -34,7 +35,7 @@
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3",
"webpack-dev-middleware": "^3.6.1",
- "webpack-dev-server": "^3.2.1",
+ "webpack-dev-server": "^3.3.1",
"webpack-hot-middleware": "^2.24.3"
},
"dependencies": {
@@ -87,6 +88,7 @@
"@types/react-table": "^6.7.22",
"@types/request": "^2.48.1",
"@types/request-promise": "^4.1.42",
+ "@types/sharp": "^0.22.2",
"@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.32",
"@types/typescript": "^2.0.0",
@@ -116,6 +118,8 @@
"golden-layout": "^1.5.9",
"html-to-image": "^0.1.0",
"i": "^0.3.6",
+ "image-data-uri": "^2.0.0",
+ "imagesloaded": "^4.1.4",
"jsonwebtoken": "^8.5.0",
"jsx-to-string": "^1.4.0",
"lodash": "^4.17.11",
@@ -123,13 +127,15 @@
"mobx": "^5.9.0",
"mobx-react": "^5.3.5",
"mobx-react-devtools": "^6.1.1",
+ "mobx-utils": "^5.4.0",
"mongodb": "^3.1.13",
"mongoose": "^5.4.18",
- "node-sass": "^4.11.0",
+ "node-sass": "^4.12.0",
"nodemailer": "^5.1.1",
"nodemon": "^1.18.10",
"normalize.css": "^8.0.1",
"npm": "^6.9.0",
+ "p-limit": "^2.2.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"prosemirror-commands": "^1.0.7",
@@ -162,10 +168,13 @@
"react-split-pane": "^0.1.85",
"react-table": "^6.9.2",
"request": "^2.88.0",
+ "request-image-size": "^2.1.0",
"request-promise": "^4.2.4",
"serializr": "^1.5.1",
+ "sharp": "^0.22.1",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
+ "solr-node": "^1.1.3",
"typescript-collections": "^1.3.2",
"url-loader": "^1.1.2",
"uuid": "^3.3.2",
diff --git a/solr/conf/lang/contractions_ca.txt b/solr/conf/lang/contractions_ca.txt
new file mode 100644
index 000000000..307a85f91
--- /dev/null
+++ b/solr/conf/lang/contractions_ca.txt
@@ -0,0 +1,8 @@
+# Set of Catalan contractions for ElisionFilter
+# TODO: load this as a resource from the analyzer and sync it in build.xml
+d
+l
+m
+n
+s
+t
diff --git a/solr/conf/lang/contractions_fr.txt b/solr/conf/lang/contractions_fr.txt
new file mode 100644
index 000000000..f1bba51b2
--- /dev/null
+++ b/solr/conf/lang/contractions_fr.txt
@@ -0,0 +1,15 @@
+# Set of French contractions for ElisionFilter
+# TODO: load this as a resource from the analyzer and sync it in build.xml
+l
+m
+t
+qu
+n
+s
+j
+d
+c
+jusqu
+quoiqu
+lorsqu
+puisqu
diff --git a/solr/conf/lang/contractions_ga.txt b/solr/conf/lang/contractions_ga.txt
new file mode 100644
index 000000000..9ebe7fa34
--- /dev/null
+++ b/solr/conf/lang/contractions_ga.txt
@@ -0,0 +1,5 @@
+# Set of Irish contractions for ElisionFilter
+# TODO: load this as a resource from the analyzer and sync it in build.xml
+d
+m
+b
diff --git a/solr/conf/lang/contractions_it.txt b/solr/conf/lang/contractions_it.txt
new file mode 100644
index 000000000..cac040953
--- /dev/null
+++ b/solr/conf/lang/contractions_it.txt
@@ -0,0 +1,23 @@
+# Set of Italian contractions for ElisionFilter
+# TODO: load this as a resource from the analyzer and sync it in build.xml
+c
+l
+all
+dall
+dell
+nell
+sull
+coll
+pell
+gl
+agl
+dagl
+degl
+negl
+sugl
+un
+m
+t
+s
+v
+d
diff --git a/solr/conf/lang/hyphenations_ga.txt b/solr/conf/lang/hyphenations_ga.txt
new file mode 100644
index 000000000..4d2642cc5
--- /dev/null
+++ b/solr/conf/lang/hyphenations_ga.txt
@@ -0,0 +1,5 @@
+# Set of Irish hyphenations for StopFilter
+# TODO: load this as a resource from the analyzer and sync it in build.xml
+h
+n
+t
diff --git a/solr/conf/lang/stemdict_nl.txt b/solr/conf/lang/stemdict_nl.txt
new file mode 100644
index 000000000..441072971
--- /dev/null
+++ b/solr/conf/lang/stemdict_nl.txt
@@ -0,0 +1,6 @@
+# Set of overrides for the dutch stemmer
+# TODO: load this as a resource from the analyzer and sync it in build.xml
+fiets fiets
+bromfiets bromfiets
+ei eier
+kind kinder
diff --git a/solr/conf/lang/stoptags_ja.txt b/solr/conf/lang/stoptags_ja.txt
new file mode 100644
index 000000000..71b750845
--- /dev/null
+++ b/solr/conf/lang/stoptags_ja.txt
@@ -0,0 +1,420 @@
+#
+# This file defines a Japanese stoptag set for JapanesePartOfSpeechStopFilter.
+#
+# Any token with a part-of-speech tag that exactly matches those defined in this
+# file are removed from the token stream.
+#
+# Set your own stoptags by uncommenting the lines below. Note that comments are
+# not allowed on the same line as a stoptag. See LUCENE-3745 for frequency lists,
+# etc. that can be useful for building you own stoptag set.
+#
+# The entire possible tagset is provided below for convenience.
+#
+#####
+# noun: unclassified nouns
+#名詞
+#
+# noun-common: Common nouns or nouns where the sub-classification is undefined
+#名詞-一般
+#
+# noun-proper: Proper nouns where the sub-classification is undefined
+#名詞-固有名詞
+#
+# noun-proper-misc: miscellaneous proper nouns
+#名詞-固有名詞-一般
+#
+# noun-proper-person: Personal names where the sub-classification is undefined
+#名詞-固有名詞-人名
+#
+# noun-proper-person-misc: names that cannot be divided into surname and
+# given name; foreign names; names where the surname or given name is unknown.
+# e.g. お市の方
+#名詞-固有名詞-人名-一般
+#
+# noun-proper-person-surname: Mainly Japanese surnames.
+# e.g. 山田
+#名詞-固有名詞-人名-姓
+#
+# noun-proper-person-given_name: Mainly Japanese given names.
+# e.g. 太郎
+#名詞-固有名詞-人名-名
+#
+# noun-proper-organization: Names representing organizations.
+# e.g. 通産省, NHK
+#名詞-固有名詞-組織
+#
+# noun-proper-place: Place names where the sub-classification is undefined
+#名詞-固有名詞-地域
+#
+# noun-proper-place-misc: Place names excluding countries.
+# e.g. アジア, バルセロナ, 京都
+#名詞-固有名詞-地域-一般
+#
+# noun-proper-place-country: Country names.
+# e.g. 日本, オーストラリア
+#名詞-固有名詞-地域-国
+#
+# noun-pronoun: Pronouns where the sub-classification is undefined
+#名詞-代名詞
+#
+# noun-pronoun-misc: miscellaneous pronouns:
+# e.g. それ, ここ, あいつ, あなた, あちこち, いくつ, どこか, なに, みなさん, みんな, わたくし, われわれ
+#名詞-代名詞-一般
+#
+# noun-pronoun-contraction: Spoken language contraction made by combining a
+# pronoun and the particle 'wa'.
+# e.g. ありゃ, こりゃ, こりゃあ, そりゃ, そりゃあ
+#名詞-代名詞-縮約
+#
+# noun-adverbial: Temporal nouns such as names of days or months that behave
+# like adverbs. Nouns that represent amount or ratios and can be used adverbially,
+# e.g. 金曜, 一月, 午後, 少量
+#名詞-副詞可能
+#
+# noun-verbal: Nouns that take arguments with case and can appear followed by
+# 'suru' and related verbs (する, できる, なさる, くださる)
+# e.g. インプット, 愛着, 悪化, 悪戦苦闘, 一安心, 下取り
+#名詞-サ変接続
+#
+# noun-adjective-base: The base form of adjectives, words that appear before な ("na")
+# e.g. 健康, 安易, 駄目, だめ
+#名詞-形容動詞語幹
+#
+# noun-numeric: Arabic numbers, Chinese numerals, and counters like 何 (回), 数.
+# e.g. 0, 1, 2, 何, 数, 幾
+#名詞-数
+#
+# noun-affix: noun affixes where the sub-classification is undefined
+#名詞-非自立
+#
+# noun-affix-misc: Of adnominalizers, the case-marker の ("no"), and words that
+# attach to the base form of inflectional words, words that cannot be classified
+# into any of the other categories below. This category includes indefinite nouns.
+# e.g. あかつき, 暁, かい, 甲斐, 気, きらい, 嫌い, くせ, 癖, こと, 事, ごと, 毎, しだい, 次第,
+# 順, せい, 所為, ついで, 序で, つもり, 積もり, 点, どころ, の, はず, 筈, はずみ, 弾み,
+# 拍子, ふう, ふり, 振り, ほう, 方, 旨, もの, 物, 者, ゆえ, 故, ゆえん, 所以, わけ, 訳,
+# わり, 割り, 割, ん-口語/, もん-口語/
+#名詞-非自立-一般
+#
+# noun-affix-adverbial: noun affixes that that can behave as adverbs.
+# e.g. あいだ, 間, あげく, 挙げ句, あと, 後, 余り, 以外, 以降, 以後, 以上, 以前, 一方, うえ,
+# 上, うち, 内, おり, 折り, かぎり, 限り, きり, っきり, 結果, ころ, 頃, さい, 際, 最中, さなか,
+# 最中, じたい, 自体, たび, 度, ため, 為, つど, 都度, とおり, 通り, とき, 時, ところ, 所,
+# とたん, 途端, なか, 中, のち, 後, ばあい, 場合, 日, ぶん, 分, ほか, 他, まえ, 前, まま,
+# 儘, 侭, みぎり, 矢先
+#名詞-非自立-副詞可能
+#
+# noun-affix-aux: noun affixes treated as 助動詞 ("auxiliary verb") in school grammars
+# with the stem よう(だ) ("you(da)").
+# e.g. よう, やう, 様 (よう)
+#名詞-非自立-助動詞語幹
+#
+# noun-affix-adjective-base: noun affixes that can connect to the indeclinable
+# connection form な (aux "da").
+# e.g. みたい, ふう
+#名詞-非自立-形容動詞語幹
+#
+# noun-special: special nouns where the sub-classification is undefined.
+#名詞-特殊
+#
+# noun-special-aux: The そうだ ("souda") stem form that is used for reporting news, is
+# treated as 助動詞 ("auxiliary verb") in school grammars, and attach to the base
+# form of inflectional words.
+# e.g. そう
+#名詞-特殊-助動詞語幹
+#
+# noun-suffix: noun suffixes where the sub-classification is undefined.
+#名詞-接尾
+#
+# noun-suffix-misc: Of the nouns or stem forms of other parts of speech that connect
+# to ガル or タイ and can combine into compound nouns, words that cannot be classified into
+# any of the other categories below. In general, this category is more inclusive than
+# 接尾語 ("suffix") and is usually the last element in a compound noun.
+# e.g. おき, かた, 方, 甲斐 (がい), がかり, ぎみ, 気味, ぐるみ, (~した) さ, 次第, 済 (ず) み,
+# よう, (でき)っこ, 感, 観, 性, 学, 類, 面, 用
+#名詞-接尾-一般
+#
+# noun-suffix-person: Suffixes that form nouns and attach to person names more often
+# than other nouns.
+# e.g. 君, 様, 著
+#名詞-接尾-人名
+#
+# noun-suffix-place: Suffixes that form nouns and attach to place names more often
+# than other nouns.
+# e.g. 町, 市, 県
+#名詞-接尾-地域
+#
+# noun-suffix-verbal: Of the suffixes that attach to nouns and form nouns, those that
+# can appear before スル ("suru").
+# e.g. 化, 視, 分け, 入り, 落ち, 買い
+#名詞-接尾-サ変接続
+#
+# noun-suffix-aux: The stem form of そうだ (様態) that is used to indicate conditions,
+# is treated as 助動詞 ("auxiliary verb") in school grammars, and attach to the
+# conjunctive form of inflectional words.
+# e.g. そう
+#名詞-接尾-助動詞語幹
+#
+# noun-suffix-adjective-base: Suffixes that attach to other nouns or the conjunctive
+# form of inflectional words and appear before the copula だ ("da").
+# e.g. 的, げ, がち
+#名詞-接尾-形容動詞語幹
+#
+# noun-suffix-adverbial: Suffixes that attach to other nouns and can behave as adverbs.
+# e.g. 後 (ご), 以後, 以降, 以前, 前後, 中, 末, 上, 時 (じ)
+#名詞-接尾-副詞可能
+#
+# noun-suffix-classifier: Suffixes that attach to numbers and form nouns. This category
+# is more inclusive than 助数詞 ("classifier") and includes common nouns that attach
+# to numbers.
+# e.g. 個, つ, 本, 冊, パーセント, cm, kg, カ月, か国, 区画, 時間, 時半
+#名詞-接尾-助数詞
+#
+# noun-suffix-special: Special suffixes that mainly attach to inflecting words.
+# e.g. (楽し) さ, (考え) 方
+#名詞-接尾-特殊
+#
+# noun-suffix-conjunctive: Nouns that behave like conjunctions and join two words
+# together.
+# e.g. (日本) 対 (アメリカ), 対 (アメリカ), (3) 対 (5), (女優) 兼 (主婦)
+#名詞-接続詞的
+#
+# noun-verbal_aux: Nouns that attach to the conjunctive particle て ("te") and are
+# semantically verb-like.
+# e.g. ごらん, ご覧, 御覧, 頂戴
+#名詞-動詞非自立的
+#
+# noun-quotation: text that cannot be segmented into words, proverbs, Chinese poetry,
+# dialects, English, etc. Currently, the only entry for 名詞 引用文字列 ("noun quotation")
+# is いわく ("iwaku").
+#名詞-引用文字列
+#
+# noun-nai_adjective: Words that appear before the auxiliary verb ない ("nai") and
+# behave like an adjective.
+# e.g. 申し訳, 仕方, とんでも, 違い
+#名詞-ナイ形容詞語幹
+#
+#####
+# prefix: unclassified prefixes
+#接頭詞
+#
+# prefix-nominal: Prefixes that attach to nouns (including adjective stem forms)
+# excluding numerical expressions.
+# e.g. お (水), 某 (氏), 同 (社), 故 (~氏), 高 (品質), お (見事), ご (立派)
+#接頭詞-名詞接続
+#
+# prefix-verbal: Prefixes that attach to the imperative form of a verb or a verb
+# in conjunctive form followed by なる/なさる/くださる.
+# e.g. お (読みなさい), お (座り)
+#接頭詞-動詞接続
+#
+# prefix-adjectival: Prefixes that attach to adjectives.
+# e.g. お (寒いですねえ), バカ (でかい)
+#接頭詞-形容詞接続
+#
+# prefix-numerical: Prefixes that attach to numerical expressions.
+# e.g. 約, およそ, 毎時
+#接頭詞-数接続
+#
+#####
+# verb: unclassified verbs
+#動詞
+#
+# verb-main:
+#動詞-自立
+#
+# verb-auxiliary:
+#動詞-非自立
+#
+# verb-suffix:
+#動詞-接尾
+#
+#####
+# adjective: unclassified adjectives
+#形容詞
+#
+# adjective-main:
+#形容詞-自立
+#
+# adjective-auxiliary:
+#形容詞-非自立
+#
+# adjective-suffix:
+#形容詞-接尾
+#
+#####
+# adverb: unclassified adverbs
+#副詞
+#
+# adverb-misc: Words that can be segmented into one unit and where adnominal
+# modification is not possible.
+# e.g. あいかわらず, 多分
+#副詞-一般
+#
+# adverb-particle_conjunction: Adverbs that can be followed by の, は, に,
+# な, する, だ, etc.
+# e.g. こんなに, そんなに, あんなに, なにか, なんでも
+#副詞-助詞類接続
+#
+#####
+# adnominal: Words that only have noun-modifying forms.
+# e.g. この, その, あの, どの, いわゆる, なんらかの, 何らかの, いろんな, こういう, そういう, ああいう,
+# どういう, こんな, そんな, あんな, どんな, 大きな, 小さな, おかしな, ほんの, たいした,
+# 「(, も) さる (ことながら)」, 微々たる, 堂々たる, 単なる, いかなる, 我が」「同じ, 亡き
+#連体詞
+#
+#####
+# conjunction: Conjunctions that can occur independently.
+# e.g. が, けれども, そして, じゃあ, それどころか
+接続詞
+#
+#####
+# particle: unclassified particles.
+助詞
+#
+# particle-case: case particles where the subclassification is undefined.
+助詞-格助詞
+#
+# particle-case-misc: Case particles.
+# e.g. から, が, で, と, に, へ, より, を, の, にて
+助詞-格助詞-一般
+#
+# particle-case-quote: the "to" that appears after nouns, a person’s speech,
+# quotation marks, expressions of decisions from a meeting, reasons, judgements,
+# conjectures, etc.
+# e.g. ( だ) と (述べた.), ( である) と (して執行猶予...)
+助詞-格助詞-引用
+#
+# particle-case-compound: Compounds of particles and verbs that mainly behave
+# like case particles.
+# e.g. という, といった, とかいう, として, とともに, と共に, でもって, にあたって, に当たって, に当って,
+# にあたり, に当たり, に当り, に当たる, にあたる, において, に於いて,に於て, における, に於ける,
+# にかけ, にかけて, にかんし, に関し, にかんして, に関して, にかんする, に関する, に際し,
+# に際して, にしたがい, に従い, に従う, にしたがって, に従って, にたいし, に対し, にたいして,
+# に対して, にたいする, に対する, について, につき, につけ, につけて, につれ, につれて, にとって,
+# にとり, にまつわる, によって, に依って, に因って, により, に依り, に因り, による, に依る, に因る,
+# にわたって, にわたる, をもって, を以って, を通じ, を通じて, を通して, をめぐって, をめぐり, をめぐる,
+# って-口語/, ちゅう-関西弁「という」/, (何) ていう (人)-口語/, っていう-口語/, といふ, とかいふ
+助詞-格助詞-連語
+#
+# particle-conjunctive:
+# e.g. から, からには, が, けれど, けれども, けど, し, つつ, て, で, と, ところが, どころか, とも, ども,
+# ながら, なり, ので, のに, ば, ものの, や ( した), やいなや, (ころん) じゃ(いけない)-口語/,
+# (行っ) ちゃ(いけない)-口語/, (言っ) たって (しかたがない)-口語/, (それがなく)ったって (平気)-口語/
+助詞-接続助詞
+#
+# particle-dependency:
+# e.g. こそ, さえ, しか, すら, は, も, ぞ
+助詞-係助詞
+#
+# particle-adverbial:
+# e.g. がてら, かも, くらい, 位, ぐらい, しも, (学校) じゃ(これが流行っている)-口語/,
+# (それ)じゃあ (よくない)-口語/, ずつ, (私) なぞ, など, (私) なり (に), (先生) なんか (大嫌い)-口語/,
+# (私) なんぞ, (先生) なんて (大嫌い)-口語/, のみ, だけ, (私) だって-口語/, だに,
+# (彼)ったら-口語/, (お茶) でも (いかが), 等 (とう), (今後) とも, ばかり, ばっか-口語/, ばっかり-口語/,
+# ほど, 程, まで, 迄, (誰) も (が)([助詞-格助詞] および [助詞-係助詞] の前に位置する「も」)
+助詞-副助詞
+#
+# particle-interjective: particles with interjective grammatical roles.
+# e.g. (松島) や
+助詞-間投助詞
+#
+# particle-coordinate:
+# e.g. と, たり, だの, だり, とか, なり, や, やら
+助詞-並立助詞
+#
+# particle-final:
+# e.g. かい, かしら, さ, ぜ, (だ)っけ-口語/, (とまってる) で-方言/, な, ナ, なあ-口語/, ぞ, ね, ネ,
+# ねぇ-口語/, ねえ-口語/, ねん-方言/, の, のう-口語/, や, よ, ヨ, よぉ-口語/, わ, わい-口語/
+助詞-終助詞
+#
+# particle-adverbial/conjunctive/final: The particle "ka" when unknown whether it is
+# adverbial, conjunctive, or sentence final. For example:
+# (a) 「A か B か」. Ex:「(国内で運用する) か,(海外で運用する) か (.)」
+# (b) Inside an adverb phrase. Ex:「(幸いという) か (, 死者はいなかった.)」
+# 「(祈りが届いたせい) か (, 試験に合格した.)」
+# (c) 「かのように」. Ex:「(何もなかった) か (のように振る舞った.)」
+# e.g. か
+助詞-副助詞/並立助詞/終助詞
+#
+# particle-adnominalizer: The "no" that attaches to nouns and modifies
+# non-inflectional words.
+助詞-連体化
+#
+# particle-adnominalizer: The "ni" and "to" that appear following nouns and adverbs
+# that are giongo, giseigo, or gitaigo.
+# e.g. に, と
+助詞-副詞化
+#
+# particle-special: A particle that does not fit into one of the above classifications.
+# This includes particles that are used in Tanka, Haiku, and other poetry.
+# e.g. かな, けむ, ( しただろう) に, (あんた) にゃ(わからん), (俺) ん (家)
+助詞-特殊
+#
+#####
+# auxiliary-verb:
+助動詞
+#
+#####
+# interjection: Greetings and other exclamations.
+# e.g. おはよう, おはようございます, こんにちは, こんばんは, ありがとう, どうもありがとう, ありがとうございます,
+# いただきます, ごちそうさま, さよなら, さようなら, はい, いいえ, ごめん, ごめんなさい
+#感動詞
+#
+#####
+# symbol: unclassified Symbols.
+記号
+#
+# symbol-misc: A general symbol not in one of the categories below.
+# e.g. [○◎@$〒→+]
+記号-一般
+#
+# symbol-comma: Commas
+# e.g. [,、]
+記号-読点
+#
+# symbol-period: Periods and full stops.
+# e.g. [..。]
+記号-句点
+#
+# symbol-space: Full-width whitespace.
+記号-空白
+#
+# symbol-open_bracket:
+# e.g. [({‘“『【]
+記号-括弧開
+#
+# symbol-close_bracket:
+# e.g. [)}’”』」】]
+記号-括弧閉
+#
+# symbol-alphabetic:
+#記号-アルファベット
+#
+#####
+# other: unclassified other
+#その他
+#
+# other-interjection: Words that are hard to classify as noun-suffixes or
+# sentence-final particles.
+# e.g. (だ)ァ
+その他-間投
+#
+#####
+# filler: Aizuchi that occurs during a conversation or sounds inserted as filler.
+# e.g. あの, うんと, えと
+フィラー
+#
+#####
+# non-verbal: non-verbal sound.
+非言語音
+#
+#####
+# fragment:
+#語断片
+#
+#####
+# unknown: unknown part of speech.
+#未知語
+#
+##### End of file
diff --git a/solr/conf/lang/stopwords_ar.txt b/solr/conf/lang/stopwords_ar.txt
new file mode 100644
index 000000000..046829db6
--- /dev/null
+++ b/solr/conf/lang/stopwords_ar.txt
@@ -0,0 +1,125 @@
+# This file was created by Jacques Savoy and is distributed under the BSD license.
+# See http://members.unine.ch/jacques.savoy/clef/index.html.
+# Also see http://www.opensource.org/licenses/bsd-license.html
+# Cleaned on October 11, 2009 (not normalized, so use before normalization)
+# This means that when modifying this list, you might need to add some
+# redundant entries, for example containing forms with both أ and ا
+من
+ومن
+منها
+منه
+في
+وفي
+فيها
+فيه
+ثم
+او
+أو
+بها
+به
+اى
+اي
+أي
+أى
+لا
+ولا
+الا
+ألا
+إلا
+لكن
+ما
+وما
+كما
+فما
+عن
+مع
+اذا
+إذا
+ان
+أن
+إن
+انها
+أنها
+إنها
+انه
+أنه
+إنه
+بان
+بأن
+فان
+فأن
+وان
+وأن
+وإن
+التى
+التي
+الذى
+الذي
+الذين
+الى
+الي
+إلى
+إلي
+على
+عليها
+عليه
+اما
+أما
+إما
+ايضا
+أيضا
+كل
+وكل
+لم
+ولم
+لن
+ولن
+هى
+هي
+هو
+وهى
+وهي
+وهو
+فهى
+فهي
+فهو
+انت
+أنت
+لك
+لها
+له
+هذه
+هذا
+تلك
+ذلك
+هناك
+كانت
+كان
+يكون
+تكون
+وكانت
+وكان
+غير
+بعض
+قد
+نحو
+بين
+بينما
+منذ
+ضمن
+حيث
+الان
+الآن
+خلال
+بعد
+قبل
+حتى
+عند
+عندما
+لدى
+جميع
diff --git a/solr/conf/lang/stopwords_bg.txt b/solr/conf/lang/stopwords_bg.txt
new file mode 100644
index 000000000..1ae4ba2ae
--- /dev/null
+++ b/solr/conf/lang/stopwords_bg.txt
@@ -0,0 +1,193 @@
+# This file was created by Jacques Savoy and is distributed under the BSD license.
+# See http://members.unine.ch/jacques.savoy/clef/index.html.
+# Also see http://www.opensource.org/licenses/bsd-license.html
+аз
+ако
+ала
+бе
+без
+беше
+би
+бил
+била
+били
+било
+близо
+бъдат
+бъде
+бяха
+вас
+ваш
+ваша
+вероятно
+вече
+взема
+ви
+вие
+винаги
+все
+всеки
+всички
+всичко
+всяка
+във
+въпреки
+върху
+ги
+главно
+го
+да
+дали
+до
+докато
+докога
+дори
+досега
+доста
+едва
+един
+ето
+за
+зад
+заедно
+заради
+засега
+затова
+защо
+защото
+из
+или
+им
+има
+имат
+иска
+каза
+как
+каква
+какво
+както
+какъв
+като
+кога
+когато
+което
+които
+кой
+който
+колко
+която
+къде
+където
+към
+ли
+ме
+между
+мен
+ми
+мнозина
+мога
+могат
+може
+моля
+момента
+му
+на
+над
+назад
+най
+направи
+напред
+например
+нас
+не
+него
+нея
+ни
+ние
+никой
+нито
+но
+някои
+някой
+няма
+обаче
+около
+освен
+особено
+от
+отгоре
+отново
+още
+пак
+по
+повече
+повечето
+под
+поне
+поради
+после
+почти
+прави
+пред
+преди
+през
+при
+пък
+първо
+са
+само
+се
+сега
+си
+скоро
+след
+сме
+според
+сред
+срещу
+сте
+съм
+със
+също
+тази
+така
+такива
+такъв
+там
+твой
+те
+тези
+ти
+тн
+то
+това
+тогава
+този
+той
+толкова
+точно
+трябва
+тук
+тъй
+тя
+тях
+харесва
+че
+често
+чрез
+ще
+щом
diff --git a/solr/conf/lang/stopwords_ca.txt b/solr/conf/lang/stopwords_ca.txt
new file mode 100644
index 000000000..3da65deaf
--- /dev/null
+++ b/solr/conf/lang/stopwords_ca.txt
@@ -0,0 +1,220 @@
+# Catalan stopwords from http://github.com/vcl/cue.language (Apache 2 Licensed)
+a
+abans
+ací
+ah
+així
+això
+al
+als
+aleshores
+algun
+alguna
+algunes
+alguns
+alhora
+allà
+allí
+allò
+altra
+altre
+altres
+amb
+ambdós
+ambdues
+apa
+aquell
+aquella
+aquelles
+aquells
+aquest
+aquesta
+aquestes
+aquests
+aquí
+baix
+cada
+cadascú
+cadascuna
+cadascunes
+cadascuns
+com
+contra
+d'un
+d'una
+d'unes
+d'uns
+dalt
+de
+del
+dels
+des
+després
+dins
+dintre
+donat
+doncs
+durant
+e
+eh
+el
+els
+em
+en
+encara
+ens
+entre
+érem
+eren
+éreu
+es
+és
+esta
+està
+estàvem
+estaven
+estàveu
+esteu
+et
+etc
+ets
+fins
+fora
+gairebé
+ha
+han
+has
+havia
+he
+hem
+heu
+hi
+ho
+i
+igual
+iguals
+ja
+l'hi
+la
+les
+li
+li'n
+llavors
+m'he
+ma
+mal
+malgrat
+mateix
+mateixa
+mateixes
+mateixos
+me
+mentre
+més
+meu
+meus
+meva
+meves
+molt
+molta
+moltes
+molts
+mon
+mons
+n'he
+n'hi
+ne
+ni
+no
+nogensmenys
+només
+nosaltres
+nostra
+nostre
+nostres
+o
+oh
+oi
+on
+pas
+pel
+pels
+per
+però
+perquè
+poc
+poca
+pocs
+poques
+potser
+propi
+qual
+quals
+quan
+quant
+que
+què
+quelcom
+qui
+quin
+quina
+quines
+quins
+s'ha
+s'han
+sa
+semblant
+semblants
+ses
+seu
+seus
+seva
+seva
+seves
+si
+sobre
+sobretot
+sóc
+solament
+sols
+son
+són
+sons
+sota
+sou
+t'ha
+t'han
+t'he
+ta
+tal
+també
+tampoc
+tan
+tant
+tanta
+tantes
+teu
+teus
+teva
+teves
+ton
+tons
+tot
+tota
+totes
+tots
+un
+una
+unes
+uns
+us
+va
+vaig
+vam
+van
+vas
+veu
+vosaltres
+vostra
+vostre
+vostres
diff --git a/solr/conf/lang/stopwords_cz.txt b/solr/conf/lang/stopwords_cz.txt
new file mode 100644
index 000000000..53c6097da
--- /dev/null
+++ b/solr/conf/lang/stopwords_cz.txt
@@ -0,0 +1,172 @@
+a
+s
+k
+o
+i
+u
+v
+z
+dnes
+cz
+tímto
+budeš
+budem
+byli
+jseš
+můj
+svým
+ta
+tomto
+tohle
+tuto
+tyto
+jej
+zda
+proč
+máte
+tato
+kam
+tohoto
+kdo
+kteří
+mi
+nám
+tom
+tomuto
+mít
+nic
+proto
+kterou
+byla
+toho
+protože
+asi
+ho
+naši
+napište
+re
+což
+tím
+takže
+svých
+její
+svými
+jste
+aj
+tu
+tedy
+teto
+bylo
+kde
+ke
+pravé
+ji
+nad
+nejsou
+či
+pod
+téma
+mezi
+přes
+ty
+pak
+vám
+ani
+když
+však
+neg
+jsem
+tento
+článku
+články
+aby
+jsme
+před
+pta
+jejich
+byl
+ještě
+až
+bez
+také
+pouze
+první
+vaše
+která
+nás
+nový
+tipy
+pokud
+může
+strana
+jeho
+své
+jiné
+zprávy
+nové
+není
+vás
+jen
+podle
+zde
+už
+být
+více
+bude
+již
+než
+který
+by
+které
+co
+nebo
+ten
+tak
+má
+při
+od
+po
+jsou
+jak
+další
+ale
+si
+se
+ve
+to
+jako
+za
+zpět
+ze
+do
+pro
+je
+na
+atd
+atp
+jakmile
+přičemž
+já
+on
+ona
+ono
+oni
+ony
+my
+vy
+jí
+ji
+mě
+mne
+jemu
+tomu
+těm
+těmu
+němu
+němuž
+jehož
+jíž
+jelikož
+jež
+jakož
+načež
diff --git a/solr/conf/lang/stopwords_da.txt b/solr/conf/lang/stopwords_da.txt
new file mode 100644
index 000000000..42e6145b9
--- /dev/null
+++ b/solr/conf/lang/stopwords_da.txt
@@ -0,0 +1,110 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/danish/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | A Danish stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | This is a ranked list (commonest to rarest) of stopwords derived from
+ | a large text sample.
+
+
+og | and
+i | in
+jeg | I
+det | that (dem. pronoun)/it (pers. pronoun)
+at | that (in front of a sentence)/to (with infinitive)
+en | a/an
+den | it (pers. pronoun)/that (dem. pronoun)
+til | to/at/for/until/against/by/of/into, more
+er | present tense of "to be"
+som | who, as
+på | on/upon/in/on/at/to/after/of/with/for, on
+de | they
+med | with/by/in, along
+han | he
+af | of/by/from/off/for/in/with/on, off
+for | at/for/to/from/by/of/ago, in front/before, because
+ikke | not
+der | who/which, there/those
+var | past tense of "to be"
+mig | me/myself
+sig | oneself/himself/herself/itself/themselves
+men | but
+et | a/an/one, one (number), someone/somebody/one
+har | present tense of "to have"
+om | round/about/for/in/a, about/around/down, if
+vi | we
+min | my
+havde | past tense of "to have"
+ham | him
+hun | she
+nu | now
+over | over/above/across/by/beyond/past/on/about, over/past
+da | then, when/as/since
+fra | from/off/since, off, since
+du | you
+ud | out
+sin | his/her/its/one's
+dem | them
+os | us/ourselves
+op | up
+man | you/one
+hans | his
+hvor | where
+eller | or
+hvad | what
+skal | must/shall etc.
+selv | myself/youself/herself/ourselves etc., even
+her | here
+alle | all/everyone/everybody etc.
+vil | will (verb)
+blev | past tense of "to stay/to remain/to get/to become"
+kunne | could
+ind | in
+når | when
+være | present tense of "to be"
+dog | however/yet/after all
+noget | something
+ville | would
+jo | you know/you see (adv), yes
+deres | their/theirs
+efter | after/behind/according to/for/by/from, later/afterwards
+ned | down
+skulle | should
+denne | this
+end | than
+dette | this
+mit | my/mine
+også | also
+under | under/beneath/below/during, below/underneath
+have | have
+dig | you
+anden | other
+hende | her
+mine | my
+alt | everything
+meget | much/very, plenty of
+sit | his, her, its, one's
+sine | his, her, its, one's
+vor | our
+mod | against
+disse | these
+hvis | if
+din | your/yours
+nogle | some
+hos | by/at
+blive | be/become
+mange | many
+ad | by/through
+bliver | present tense of "to be/to become"
+hendes | her/hers
+været | be
+thi | for (conj)
+jer | you
+sådan | such, like this/like that
diff --git a/solr/conf/lang/stopwords_de.txt b/solr/conf/lang/stopwords_de.txt
new file mode 100644
index 000000000..86525e7ae
--- /dev/null
+++ b/solr/conf/lang/stopwords_de.txt
@@ -0,0 +1,294 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/german/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | A German stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | The number of forms in this list is reduced significantly by passing it
+ | through the German stemmer.
+
+
+aber | but
+
+alle | all
+allem
+allen
+aller
+alles
+
+als | than, as
+also | so
+am | an + dem
+an | at
+
+ander | other
+andere
+anderem
+anderen
+anderer
+anderes
+anderm
+andern
+anderr
+anders
+
+auch | also
+auf | on
+aus | out of
+bei | by
+bin | am
+bis | until
+bist | art
+da | there
+damit | with it
+dann | then
+
+der | the
+den
+des
+dem
+die
+das
+
+daß | that
+
+derselbe | the same
+derselben
+denselben
+desselben
+demselben
+dieselbe
+dieselben
+dasselbe
+
+dazu | to that
+
+dein | thy
+deine
+deinem
+deinen
+deiner
+deines
+
+denn | because
+
+derer | of those
+dessen | of him
+
+dich | thee
+dir | to thee
+du | thou
+
+dies | this
+diese
+diesem
+diesen
+dieser
+dieses
+
+
+doch | (several meanings)
+dort | (over) there
+
+
+durch | through
+
+ein | a
+eine
+einem
+einen
+einer
+eines
+
+einig | some
+einige
+einigem
+einigen
+einiger
+einiges
+
+einmal | once
+
+er | he
+ihn | him
+ihm | to him
+
+es | it
+etwas | something
+
+euer | your
+eure
+eurem
+euren
+eurer
+eures
+
+für | for
+gegen | towards
+gewesen | p.p. of sein
+hab | have
+habe | have
+haben | have
+hat | has
+hatte | had
+hatten | had
+hier | here
+hin | there
+hinter | behind
+
+ich | I
+mich | me
+mir | to me
+
+
+ihr | you, to her
+ihre
+ihrem
+ihren
+ihrer
+ihres
+euch | to you
+
+im | in + dem
+in | in
+indem | while
+ins | in + das
+ist | is
+
+jede | each, every
+jedem
+jeden
+jeder
+jedes
+
+jene | that
+jenem
+jenen
+jener
+jenes
+
+jetzt | now
+kann | can
+
+kein | no
+keine
+keinem
+keinen
+keiner
+keines
+
+können | can
+könnte | could
+machen | do
+man | one
+
+manche | some, many a
+manchem
+manchen
+mancher
+manches
+
+mein | my
+meine
+meinem
+meinen
+meiner
+meines
+
+mit | with
+muss | must
+musste | had to
+nach | to(wards)
+nicht | not
+nichts | nothing
+noch | still, yet
+nun | now
+nur | only
+ob | whether
+oder | or
+ohne | without
+sehr | very
+
+sein | his
+seine
+seinem
+seinen
+seiner
+seines
+
+selbst | self
+sich | herself
+
+sie | they, she
+ihnen | to them
+
+sind | are
+so | so
+
+solche | such
+solchem
+solchen
+solcher
+solches
+
+soll | shall
+sollte | should
+sondern | but
+sonst | else
+über | over
+um | about, around
+und | and
+
+uns | us
+unse
+unsem
+unsen
+unser
+unses
+
+unter | under
+viel | much
+vom | von + dem
+von | from
+vor | before
+während | while
+war | was
+waren | were
+warst | wast
+was | what
+weg | away, off
+weil | because
+weiter | further
+
+welche | which
+welchem
+welchen
+welcher
+welches
+
+wenn | when
+werde | will
+werden | will
+wie | how
+wieder | again
+will | want
+wir | we
+wird | will
+wirst | willst
+wo | where
+wollen | want
+wollte | wanted
+würde | would
+würden | would
+zu | to
+zum | zu + dem
+zur | zu + der
+zwar | indeed
+zwischen | between
+
diff --git a/solr/conf/lang/stopwords_el.txt b/solr/conf/lang/stopwords_el.txt
new file mode 100644
index 000000000..232681f5b
--- /dev/null
+++ b/solr/conf/lang/stopwords_el.txt
@@ -0,0 +1,78 @@
+# Lucene Greek Stopwords list
+# Note: by default this file is used after GreekLowerCaseFilter,
+# so when modifying this file use 'σ' instead of 'ς'
+ο
+το
+οι
+τα
+του
+τησ
+των
+τον
+την
+και
+κι
+ειμαι
+εισαι
+ειναι
+ειμαστε
+ειστε
+στο
+στον
+στη
+στην
+μα
+αλλα
+απο
+για
+προσ
+με
+σε
+ωσ
+παρα
+αντι
+κατα
+μετα
+θα
+να
+δε
+δεν
+μη
+μην
+επι
+ενω
+εαν
+αν
+τοτε
+που
+πωσ
+ποιοσ
+ποια
+ποιο
+ποιοι
+ποιεσ
+ποιων
+ποιουσ
+αυτοσ
+αυτη
+αυτο
+αυτοι
+αυτων
+αυτουσ
+αυτεσ
+αυτα
+εκεινοσ
+εκεινη
+εκεινο
+εκεινοι
+εκεινεσ
+εκεινα
+εκεινων
+εκεινουσ
+οπωσ
+ομωσ
+ισωσ
+οσο
+οτι
diff --git a/solr/conf/lang/stopwords_en.txt b/solr/conf/lang/stopwords_en.txt
new file mode 100644
index 000000000..2c164c0b2
--- /dev/null
+++ b/solr/conf/lang/stopwords_en.txt
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# a couple of test stopwords to test that the words are really being
+# configured from this file:
+stopworda
+stopwordb
+
+# Standard english stop words taken from Lucene's StopAnalyzer
+a
+an
+and
+are
+as
+at
+be
+but
+by
+for
+if
+in
+into
+is
+it
+no
+not
+of
+on
+or
+such
+that
+the
+their
+then
+there
+these
+they
+this
+to
+was
+will
+with
diff --git a/solr/conf/lang/stopwords_es.txt b/solr/conf/lang/stopwords_es.txt
new file mode 100644
index 000000000..487d78c8d
--- /dev/null
+++ b/solr/conf/lang/stopwords_es.txt
@@ -0,0 +1,356 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/spanish/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | A Spanish stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+
+ | The following is a ranked list (commonest to rarest) of stopwords
+ | deriving from a large sample of text.
+
+ | Extra words have been added at the end.
+
+de | from, of
+la | the, her
+que | who, that
+el | the
+en | in
+y | and
+a | to
+los | the, them
+del | de + el
+se | himself, from him etc
+las | the, them
+por | for, by, etc
+un | a
+para | for
+con | with
+no | no
+una | a
+su | his, her
+al | a + el
+ | es from SER
+lo | him
+como | how
+más | more
+pero | pero
+sus | su plural
+le | to him, her
+ya | already
+o | or
+ | fue from SER
+este | this
+ | ha from HABER
+sí | himself etc
+porque | because
+esta | this
+ | son from SER
+entre | between
+ | está from ESTAR
+cuando | when
+muy | very
+sin | without
+sobre | on
+ | ser from SER
+ | tiene from TENER
+también | also
+me | me
+hasta | until
+hay | there is/are
+donde | where
+ | han from HABER
+quien | whom, that
+ | están from ESTAR
+ | estado from ESTAR
+desde | from
+todo | all
+nos | us
+durante | during
+ | estados from ESTAR
+todos | all
+uno | a
+les | to them
+ni | nor
+contra | against
+otros | other
+ | fueron from SER
+ese | that
+eso | that
+ | había from HABER
+ante | before
+ellos | they
+e | and (variant of y)
+esto | this
+mí | me
+antes | before
+algunos | some
+qué | what?
+unos | a
+yo | I
+otro | other
+otras | other
+otra | other
+él | he
+tanto | so much, many
+esa | that
+estos | these
+mucho | much, many
+quienes | who
+nada | nothing
+muchos | many
+cual | who
+ | sea from SER
+poco | few
+ella | she
+estar | to be
+ | haber from HABER
+estas | these
+ | estaba from ESTAR
+ | estamos from ESTAR
+algunas | some
+algo | something
+nosotros | we
+
+ | other forms
+
+mi | me
+mis | mi plural
+tú | thou
+te | thee
+ti | thee
+tu | thy
+tus | tu plural
+ellas | they
+nosotras | we
+vosotros | you
+vosotras | you
+os | you
+mío | mine
+mía |
+míos |
+mías |
+tuyo | thine
+tuya |
+tuyos |
+tuyas |
+suyo | his, hers, theirs
+suya |
+suyos |
+suyas |
+nuestro | ours
+nuestra |
+nuestros |
+nuestras |
+vuestro | yours
+vuestra |
+vuestros |
+vuestras |
+esos | those
+esas | those
+
+ | forms of estar, to be (not including the infinitive):
+estoy
+estás
+está
+estamos
+estáis
+están
+esté
+estés
+estemos
+estéis
+estén
+estaré
+estarás
+estará
+estaremos
+estaréis
+estarán
+estaría
+estarías
+estaríamos
+estaríais
+estarían
+estaba
+estabas
+estábamos
+estabais
+estaban
+estuve
+estuviste
+estuvo
+estuvimos
+estuvisteis
+estuvieron
+estuviera
+estuvieras
+estuviéramos
+estuvierais
+estuvieran
+estuviese
+estuvieses
+estuviésemos
+estuvieseis
+estuviesen
+estando
+estado
+estada
+estados
+estadas
+estad
+
+ | forms of haber, to have (not including the infinitive):
+he
+has
+ha
+hemos
+habéis
+han
+haya
+hayas
+hayamos
+hayáis
+hayan
+habré
+habrás
+habrá
+habremos
+habréis
+habrán
+habría
+habrías
+habríamos
+habríais
+habrían
+había
+habías
+habíamos
+habíais
+habían
+hube
+hubiste
+hubo
+hubimos
+hubisteis
+hubieron
+hubiera
+hubieras
+hubiéramos
+hubierais
+hubieran
+hubiese
+hubieses
+hubiésemos
+hubieseis
+hubiesen
+habiendo
+habido
+habida
+habidos
+habidas
+
+ | forms of ser, to be (not including the infinitive):
+soy
+eres
+es
+somos
+sois
+son
+sea
+seas
+seamos
+seáis
+sean
+seré
+serás
+será
+seremos
+seréis
+serán
+sería
+serías
+seríamos
+seríais
+serían
+era
+eras
+éramos
+erais
+eran
+fui
+fuiste
+fue
+fuimos
+fuisteis
+fueron
+fuera
+fueras
+fuéramos
+fuerais
+fueran
+fuese
+fueses
+fuésemos
+fueseis
+fuesen
+siendo
+sido
+ | sed also means 'thirst'
+
+ | forms of tener, to have (not including the infinitive):
+tengo
+tienes
+tiene
+tenemos
+tenéis
+tienen
+tenga
+tengas
+tengamos
+tengáis
+tengan
+tendré
+tendrás
+tendrá
+tendremos
+tendréis
+tendrán
+tendría
+tendrías
+tendríamos
+tendríais
+tendrían
+tenía
+tenías
+teníamos
+teníais
+tenían
+tuve
+tuviste
+tuvo
+tuvimos
+tuvisteis
+tuvieron
+tuviera
+tuvieras
+tuviéramos
+tuvierais
+tuvieran
+tuviese
+tuvieses
+tuviésemos
+tuvieseis
+tuviesen
+teniendo
+tenido
+tenida
+tenidos
+tenidas
+tened
+
diff --git a/solr/conf/lang/stopwords_eu.txt b/solr/conf/lang/stopwords_eu.txt
new file mode 100644
index 000000000..25f1db934
--- /dev/null
+++ b/solr/conf/lang/stopwords_eu.txt
@@ -0,0 +1,99 @@
+# example set of basque stopwords
+al
+anitz
+arabera
+asko
+baina
+bat
+batean
+batek
+bati
+batzuei
+batzuek
+batzuetan
+batzuk
+bera
+beraiek
+berau
+berauek
+bere
+berori
+beroriek
+beste
+bezala
+da
+dago
+dira
+ditu
+du
+dute
+edo
+egin
+ere
+eta
+eurak
+ez
+gainera
+gu
+gutxi
+guzti
+haiei
+haiek
+haietan
+hainbeste
+hala
+han
+handik
+hango
+hara
+hari
+hark
+hartan
+hau
+hauei
+hauek
+hauetan
+hemen
+hemendik
+hemengo
+hi
+hona
+honek
+honela
+honetan
+honi
+hor
+hori
+horiei
+horiek
+horietan
+horko
+horra
+horrek
+horrela
+horretan
+horri
+hortik
+hura
+izan
+ni
+noiz
+nola
+non
+nondik
+nongo
+nor
+nora
+ze
+zein
+zen
+zenbait
+zenbat
+zer
+zergatik
+ziren
+zituen
+zu
+zuek
+zuen
+zuten
diff --git a/solr/conf/lang/stopwords_fa.txt b/solr/conf/lang/stopwords_fa.txt
new file mode 100644
index 000000000..723641c6d
--- /dev/null
+++ b/solr/conf/lang/stopwords_fa.txt
@@ -0,0 +1,313 @@
+# This file was created by Jacques Savoy and is distributed under the BSD license.
+# See http://members.unine.ch/jacques.savoy/clef/index.html.
+# Also see http://www.opensource.org/licenses/bsd-license.html
+# Note: by default this file is used after normalization, so when adding entries
+# to this file, use the arabic 'ي' instead of 'ی'
+انان
+نداشته
+سراسر
+خياه
+ايشان
+وي
+تاكنون
+بيشتري
+دوم
+پس
+ناشي
+وگو
+يا
+داشتند
+سپس
+هنگام
+هرگز
+پنج
+نشان
+امسال
+ديگر
+گروهي
+شدند
+چطور
+ده
+دو
+نخستين
+ولي
+چرا
+چه
+وسط
+كدام
+قابل
+يك
+رفت
+هفت
+همچنين
+در
+هزار
+بله
+بلي
+شايد
+اما
+شناسي
+گرفته
+دهد
+داشته
+دانست
+داشتن
+خواهيم
+ميليارد
+وقتيكه
+امد
+خواهد
+جز
+اورده
+شده
+بلكه
+خدمات
+شدن
+برخي
+نبود
+بسياري
+جلوگيري
+حق
+كردند
+نوعي
+بعري
+نكرده
+نظير
+نبايد
+بوده
+بودن
+داد
+اورد
+هست
+جايي
+شود
+دنبال
+داده
+بايد
+سابق
+هيچ
+همان
+انجا
+كمتر
+كجاست
+گردد
+كسي
+تر
+مردم
+تان
+دادن
+بودند
+سري
+جدا
+ندارند
+مگر
+يكديگر
+دارد
+دهند
+بنابراين
+هنگامي
+سمت
+جا
+انچه
+خود
+دادند
+زياد
+دارند
+اثر
+بدون
+بهترين
+بيشتر
+البته
+به
+براساس
+بيرون
+كرد
+بعضي
+گرفت
+توي
+اي
+ميليون
+او
+جريان
+تول
+بر
+مانند
+برابر
+باشيم
+مدتي
+گويند
+اكنون
+تا
+تنها
+جديد
+چند
+بي
+نشده
+كردن
+كردم
+گويد
+كرده
+كنيم
+نمي
+نزد
+روي
+قصد
+فقط
+بالاي
+ديگران
+اين
+ديروز
+توسط
+سوم
+ايم
+دانند
+سوي
+استفاده
+شما
+كنار
+داريم
+ساخته
+طور
+امده
+رفته
+نخست
+بيست
+نزديك
+طي
+كنيد
+از
+انها
+تمامي
+داشت
+يكي
+طريق
+اش
+چيست
+روب
+نمايد
+گفت
+چندين
+چيزي
+تواند
+ام
+ايا
+با
+ان
+ايد
+ترين
+اينكه
+ديگري
+راه
+هايي
+بروز
+همچنان
+پاعين
+كس
+حدود
+مختلف
+مقابل
+چيز
+گيرد
+ندارد
+ضد
+همچون
+سازي
+شان
+مورد
+باره
+مرسي
+خويش
+برخوردار
+چون
+خارج
+شش
+هنوز
+تحت
+ضمن
+هستيم
+گفته
+فكر
+بسيار
+پيش
+براي
+روزهاي
+انكه
+نخواهد
+بالا
+كل
+وقتي
+كي
+چنين
+كه
+گيري
+نيست
+است
+كجا
+كند
+نيز
+يابد
+بندي
+حتي
+توانند
+عقب
+خواست
+كنند
+بين
+تمام
+همه
+ما
+باشند
+مثل
+شد
+اري
+باشد
+اره
+طبق
+بعد
+اگر
+صورت
+غير
+جاي
+بيش
+ريزي
+اند
+زيرا
+چگونه
+بار
+لطفا
+مي
+درباره
+من
+ديده
+همين
+گذاري
+برداري
+علت
+گذاشته
+هم
+فوق
+نه
+ها
+شوند
+اباد
+همواره
+هر
+اول
+خواهند
+چهار
+نام
+امروز
+مان
+هاي
+قبل
+كنم
+سعي
+تازه
+را
+هستند
+زير
+جلوي
+عنوان
+بود
diff --git a/solr/conf/lang/stopwords_fi.txt b/solr/conf/lang/stopwords_fi.txt
new file mode 100644
index 000000000..4372c9a05
--- /dev/null
+++ b/solr/conf/lang/stopwords_fi.txt
@@ -0,0 +1,97 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/finnish/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+| forms of BE
+
+olla
+olen
+olet
+on
+olemme
+olette
+ovat
+ole | negative form
+
+oli
+olisi
+olisit
+olisin
+olisimme
+olisitte
+olisivat
+olit
+olin
+olimme
+olitte
+olivat
+ollut
+olleet
+
+en | negation
+et
+ei
+emme
+ette
+eivät
+
+|Nom Gen Acc Part Iness Elat Illat Adess Ablat Allat Ess Trans
+minä minun minut minua minussa minusta minuun minulla minulta minulle | I
+sinä sinun sinut sinua sinussa sinusta sinuun sinulla sinulta sinulle | you
+hän hänen hänet häntä hänessä hänestä häneen hänellä häneltä hänelle | he she
+me meidän meidät meitä meissä meistä meihin meillä meiltä meille | we
+te teidän teidät teitä teissä teistä teihin teillä teiltä teille | you
+he heidän heidät heitä heissä heistä heihin heillä heiltä heille | they
+
+tämä tämän tätä tässä tästä tähän tallä tältä tälle tänä täksi | this
+tuo tuon tuotä tuossa tuosta tuohon tuolla tuolta tuolle tuona tuoksi | that
+se sen sitä siinä siitä siihen sillä siltä sille sinä siksi | it
+nämä näiden näitä näissä näistä näihin näillä näiltä näille näinä näiksi | these
+nuo noiden noita noissa noista noihin noilla noilta noille noina noiksi | those
+ne niiden niitä niissä niistä niihin niillä niiltä niille niinä niiksi | they
+
+kuka kenen kenet ketä kenessä kenestä keneen kenellä keneltä kenelle kenenä keneksi| who
+ketkä keiden ketkä keitä keissä keistä keihin keillä keiltä keille keinä keiksi | (pl)
+mikä minkä minkä mitä missä mistä mihin millä miltä mille minä miksi | which what
+mitkä | (pl)
+
+joka jonka jota jossa josta johon jolla jolta jolle jona joksi | who which
+jotka joiden joita joissa joista joihin joilla joilta joille joina joiksi | (pl)
+
+| conjunctions
+
+että | that
+ja | and
+jos | if
+koska | because
+kuin | than
+mutta | but
+niin | so
+sekä | and
+sillä | for
+tai | or
+vaan | but
+vai | or
+vaikka | although
+
+
+| prepositions
+
+kanssa | with
+mukaan | according to
+noin | about
+poikki | across
+yli | over, across
+
+| other
+
+kun | when
+niin | so
+nyt | now
+itse | self
+
diff --git a/solr/conf/lang/stopwords_fr.txt b/solr/conf/lang/stopwords_fr.txt
new file mode 100644
index 000000000..749abae68
--- /dev/null
+++ b/solr/conf/lang/stopwords_fr.txt
@@ -0,0 +1,186 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/french/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | A French stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+au | a + le
+aux | a + les
+avec | with
+ce | this
+ces | these
+dans | with
+de | of
+des | de + les
+du | de + le
+elle | she
+en | `of them' etc
+et | and
+eux | them
+il | he
+je | I
+la | the
+le | the
+leur | their
+lui | him
+ma | my (fem)
+mais | but
+me | me
+même | same; as in moi-même (myself) etc
+mes | me (pl)
+moi | me
+mon | my (masc)
+ne | not
+nos | our (pl)
+notre | our
+nous | we
+on | one
+ou | where
+par | by
+pas | not
+pour | for
+qu | que before vowel
+que | that
+qui | who
+sa | his, her (fem)
+se | oneself
+ses | his (pl)
+son | his, her (masc)
+sur | on
+ta | thy (fem)
+te | thee
+tes | thy (pl)
+toi | thee
+ton | thy (masc)
+tu | thou
+un | a
+une | a
+vos | your (pl)
+votre | your
+vous | you
+
+ | single letter forms
+
+c | c'
+d | d'
+j | j'
+l | l'
+à | to, at
+m | m'
+n | n'
+s | s'
+t | t'
+y | there
+
+ | forms of être (not including the infinitive):
+été
+étée
+étées
+étés
+étant
+suis
+es
+est
+sommes
+êtes
+sont
+serai
+seras
+sera
+serons
+serez
+seront
+serais
+serait
+serions
+seriez
+seraient
+étais
+était
+étions
+étiez
+étaient
+fus
+fut
+fûmes
+fûtes
+furent
+sois
+soit
+soyons
+soyez
+soient
+fusse
+fusses
+fût
+fussions
+fussiez
+fussent
+
+ | forms of avoir (not including the infinitive):
+ayant
+eu
+eue
+eues
+eus
+ai
+as
+avons
+avez
+ont
+aurai
+auras
+aura
+aurons
+aurez
+auront
+aurais
+aurait
+aurions
+auriez
+auraient
+avais
+avait
+avions
+aviez
+avaient
+eut
+eûmes
+eûtes
+eurent
+aie
+aies
+ait
+ayons
+ayez
+aient
+eusse
+eusses
+eût
+eussions
+eussiez
+eussent
+
+ | Later additions (from Jean-Christophe Deschamps)
+ceci | this
+cela | that
+celà | that
+cet | this
+cette | this
+ici | here
+ils | they
+les | the (pl)
+leurs | their (pl)
+quel | which
+quels | which
+quelle | which
+quelles | which
+sans | without
+soi | oneself
+
diff --git a/solr/conf/lang/stopwords_ga.txt b/solr/conf/lang/stopwords_ga.txt
new file mode 100644
index 000000000..9ff88d747
--- /dev/null
+++ b/solr/conf/lang/stopwords_ga.txt
@@ -0,0 +1,110 @@
+
+a
+ach
+ag
+agus
+an
+aon
+ar
+arna
+as
+b'
+ba
+beirt
+bhúr
+caoga
+ceathair
+ceathrar
+chomh
+chtó
+chuig
+chun
+cois
+céad
+cúig
+cúigear
+d'
+daichead
+dar
+de
+deich
+deichniúr
+den
+dhá
+do
+don
+dtí
+dá
+dár
+dó
+faoi
+faoin
+faoina
+faoinár
+fara
+fiche
+gach
+gan
+go
+gur
+haon
+hocht
+i
+iad
+idir
+in
+ina
+ins
+inár
+is
+le
+leis
+lena
+lenár
+m'
+mar
+mo
+mé
+na
+nach
+naoi
+naonúr
+ná
+ní
+níor
+nó
+nócha
+ocht
+ochtar
+os
+roimh
+sa
+seacht
+seachtar
+seachtó
+seasca
+seisear
+siad
+sibh
+sinn
+sna
+sé
+sí
+tar
+thar
+thú
+triúr
+trí
+trína
+trínár
+tríocha
+tú
+um
+ár
+éis
+ón
+óna
+ónár
diff --git a/solr/conf/lang/stopwords_gl.txt b/solr/conf/lang/stopwords_gl.txt
new file mode 100644
index 000000000..d8760b12c
--- /dev/null
+++ b/solr/conf/lang/stopwords_gl.txt
@@ -0,0 +1,161 @@
+# galican stopwords
+a
+aínda
+alí
+aquel
+aquela
+aquelas
+aqueles
+aquilo
+aquí
+ao
+aos
+as
+así
+ben
+cando
+che
+co
+coa
+comigo
+con
+connosco
+contigo
+convosco
+coas
+cos
+cun
+cuns
+cunha
+cunhas
+da
+dalgunha
+dalgunhas
+dalgún
+dalgúns
+das
+de
+del
+dela
+delas
+deles
+desde
+deste
+do
+dos
+dun
+duns
+dunha
+dunhas
+e
+el
+ela
+elas
+eles
+en
+era
+eran
+esa
+esas
+ese
+eses
+esta
+estar
+estaba
+está
+están
+este
+estes
+estiven
+estou
+eu
+facer
+foi
+foron
+fun
+había
+hai
+iso
+isto
+la
+las
+lle
+lles
+lo
+los
+mais
+me
+meu
+meus
+min
+miña
+miñas
+moi
+na
+nas
+neste
+nin
+no
+non
+nos
+nosa
+nosas
+noso
+nosos
+nós
+nun
+nunha
+nuns
+nunhas
+o
+os
+ou
+ós
+para
+pero
+pode
+pois
+pola
+polas
+polo
+polos
+por
+que
+se
+senón
+ser
+seu
+seus
+sexa
+sido
+sobre
+súa
+súas
+tamén
+tan
+te
+ten
+teñen
+teño
+ter
+teu
+teus
+ti
+tido
+tiña
+tiven
+túa
+túas
+un
+unha
+unhas
+uns
+vos
+vosa
+vosas
+voso
+vosos
+vós
diff --git a/solr/conf/lang/stopwords_hi.txt b/solr/conf/lang/stopwords_hi.txt
new file mode 100644
index 000000000..86286bb08
--- /dev/null
+++ b/solr/conf/lang/stopwords_hi.txt
@@ -0,0 +1,235 @@
+# Also see http://www.opensource.org/licenses/bsd-license.html
+# See http://members.unine.ch/jacques.savoy/clef/index.html.
+# This file was created by Jacques Savoy and is distributed under the BSD license.
+# Note: by default this file also contains forms normalized by HindiNormalizer
+# for spelling variation (see section below), such that it can be used whether or
+# not you enable that feature. When adding additional entries to this list,
+# please add the normalized form as well.
+अंदर
+अत
+अपना
+अपनी
+अपने
+अभी
+आदि
+आप
+इत्यादि
+इन
+इनका
+इन्हीं
+इन्हें
+इन्हों
+इस
+इसका
+इसकी
+इसके
+इसमें
+इसी
+इसे
+उन
+उनका
+उनकी
+उनके
+उनको
+उन्हीं
+उन्हें
+उन्हों
+उस
+उसके
+उसी
+उसे
+एक
+एवं
+एस
+ऐसे
+और
+कई
+कर
+करता
+करते
+करना
+करने
+करें
+कहते
+कहा
+का
+काफ़ी
+कि
+कितना
+किन्हें
+किन्हों
+किया
+किर
+किस
+किसी
+किसे
+की
+कुछ
+कुल
+के
+को
+कोई
+कौन
+कौनसा
+गया
+घर
+जब
+जहाँ
+जा
+जितना
+जिन
+जिन्हें
+जिन्हों
+जिस
+जिसे
+जीधर
+जैसा
+जैसे
+जो
+तक
+तब
+तरह
+तिन
+तिन्हें
+तिन्हों
+तिस
+तिसे
+तो
+था
+थी
+थे
+दबारा
+दिया
+दुसरा
+दूसरे
+दो
+द्वारा
+न
+नहीं
+ना
+निहायत
+नीचे
+ने
+पर
+पर
+पहले
+पूरा
+पे
+फिर
+बनी
+बही
+बहुत
+बाद
+बाला
+बिलकुल
+भी
+भीतर
+मगर
+मानो
+मे
+में
+यदि
+यह
+यहाँ
+यही
+या
+यिह
+ये
+रखें
+रहा
+रहे
+ऱ्वासा
+लिए
+लिये
+लेकिन
+व
+वर्ग
+वह
+वह
+वहाँ
+वहीं
+वाले
+वुह
+वे
+वग़ैरह
+संग
+सकता
+सकते
+सबसे
+सभी
+साथ
+साबुत
+साभ
+सारा
+से
+सो
+ही
+हुआ
+हुई
+हुए
+है
+हैं
+हो
+होता
+होती
+होते
+होना
+होने
+# additional normalized forms of the above
+अपनि
+जेसे
+होति
+सभि
+तिंहों
+इंहों
+दवारा
+इसि
+किंहें
+थि
+उंहों
+ओर
+जिंहें
+वहिं
+अभि
+बनि
+हि
+उंहिं
+उंहें
+हें
+वगेरह
+एसे
+रवासा
+कोन
+निचे
+काफि
+उसि
+पुरा
+भितर
+हे
+बहि
+वहां
+कोइ
+यहां
+जिंहों
+तिंहें
+किसि
+कइ
+यहि
+इंहिं
+जिधर
+इंहें
+अदि
+इतयादि
+हुइ
+कोनसा
+इसकि
+दुसरे
+जहां
+अप
+किंहों
+उनकि
+भि
+वरग
+हुअ
+जेसा
+नहिं
diff --git a/solr/conf/lang/stopwords_hu.txt b/solr/conf/lang/stopwords_hu.txt
new file mode 100644
index 000000000..37526da8a
--- /dev/null
+++ b/solr/conf/lang/stopwords_hu.txt
@@ -0,0 +1,211 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/hungarian/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+| Hungarian stop word list
+| prepared by Anna Tordai
+
+a
+ahogy
+ahol
+aki
+akik
+akkor
+alatt
+által
+általában
+amely
+amelyek
+amelyekben
+amelyeket
+amelyet
+amelynek
+ami
+amit
+amolyan
+amíg
+amikor
+át
+abban
+ahhoz
+annak
+arra
+arról
+az
+azok
+azon
+azt
+azzal
+azért
+aztán
+azután
+azonban
+bár
+be
+belül
+benne
+cikk
+cikkek
+cikkeket
+csak
+de
+e
+eddig
+egész
+egy
+egyes
+egyetlen
+egyéb
+egyik
+egyre
+ekkor
+el
+elég
+ellen
+elő
+először
+előtt
+első
+én
+éppen
+ebben
+ehhez
+emilyen
+ennek
+erre
+ez
+ezt
+ezek
+ezen
+ezzel
+ezért
+és
+fel
+felé
+hanem
+hiszen
+hogy
+hogyan
+igen
+így
+illetve
+ill.
+ill
+ilyen
+ilyenkor
+ison
+ismét
+itt
+jó
+jól
+jobban
+kell
+kellett
+keresztül
+keressünk
+ki
+kívül
+között
+közül
+legalább
+lehet
+lehetett
+legyen
+lenne
+lenni
+lesz
+lett
+maga
+magát
+majd
+majd
+már
+más
+másik
+meg
+még
+mellett
+mert
+mely
+melyek
+mi
+mit
+míg
+miért
+milyen
+mikor
+minden
+mindent
+mindenki
+mindig
+mint
+mintha
+mivel
+most
+nagy
+nagyobb
+nagyon
+ne
+néha
+nekem
+neki
+nem
+néhány
+nélkül
+nincs
+olyan
+ott
+össze
+ők
+őket
+pedig
+persze
+rá
+s
+saját
+sem
+semmi
+sok
+sokat
+sokkal
+számára
+szemben
+szerint
+szinte
+talán
+tehát
+teljes
+tovább
+továbbá
+több
+úgy
+ugyanis
+új
+újabb
+újra
+után
+utána
+utolsó
+vagy
+vagyis
+valaki
+valami
+valamint
+való
+vagyok
+van
+vannak
+volt
+voltam
+voltak
+voltunk
+vissza
+vele
+viszont
+volna
diff --git a/solr/conf/lang/stopwords_hy.txt b/solr/conf/lang/stopwords_hy.txt
new file mode 100644
index 000000000..60c1c50fb
--- /dev/null
+++ b/solr/conf/lang/stopwords_hy.txt
@@ -0,0 +1,46 @@
+# example set of Armenian stopwords.
+այդ
+այլ
+այն
+այս
+դու
+դուք
+եմ
+են
+ենք
+ես
+եք
+էի
+էին
+էինք
+էիր
+էիք
+էր
+ըստ
+ին
+իսկ
+իր
+կամ
+համար
+հետ
+հետո
+մենք
+մեջ
+մի
+նա
+նաև
+նրա
+նրանք
+որ
+որը
+որոնք
+որպես
+ու
+ում
+պիտի
+վրա
diff --git a/solr/conf/lang/stopwords_id.txt b/solr/conf/lang/stopwords_id.txt
new file mode 100644
index 000000000..4617f83a5
--- /dev/null
+++ b/solr/conf/lang/stopwords_id.txt
@@ -0,0 +1,359 @@
+# from appendix D of: A Study of Stemming Effects on Information
+# Retrieval in Bahasa Indonesia
+ada
+adanya
+adalah
+adapun
+agak
+agaknya
+agar
+akan
+akankah
+akhirnya
+aku
+akulah
+amat
+amatlah
+anda
+andalah
+antar
+diantaranya
+antara
+antaranya
+diantara
+apa
+apaan
+mengapa
+apabila
+apakah
+apalagi
+apatah
+atau
+ataukah
+ataupun
+bagai
+bagaikan
+sebagai
+sebagainya
+bagaimana
+bagaimanapun
+sebagaimana
+bagaimanakah
+bagi
+bahkan
+bahwa
+bahwasanya
+sebaliknya
+banyak
+sebanyak
+beberapa
+seberapa
+begini
+beginian
+beginikah
+beginilah
+sebegini
+begitu
+begitukah
+begitulah
+begitupun
+sebegitu
+belum
+belumlah
+sebelum
+sebelumnya
+sebenarnya
+berapa
+berapakah
+berapalah
+berapapun
+betulkah
+sebetulnya
+biasa
+biasanya
+bila
+bilakah
+bisa
+bisakah
+sebisanya
+boleh
+bolehkah
+bolehlah
+buat
+bukan
+bukankah
+bukanlah
+bukannya
+cuma
+percuma
+dahulu
+dalam
+dan
+dapat
+dari
+daripada
+dekat
+demi
+demikian
+demikianlah
+sedemikian
+dengan
+depan
+di
+dia
+dialah
+dini
+diri
+dirinya
+terdiri
+dong
+dulu
+enggak
+enggaknya
+entah
+entahlah
+terhadap
+terhadapnya
+hal
+hampir
+hanya
+hanyalah
+harus
+haruslah
+harusnya
+seharusnya
+hendak
+hendaklah
+hendaknya
+hingga
+sehingga
+ia
+ialah
+ibarat
+ingin
+inginkah
+inginkan
+ini
+inikah
+inilah
+itu
+itukah
+itulah
+jangan
+jangankan
+janganlah
+jika
+jikalau
+juga
+justru
+kala
+kalau
+kalaulah
+kalaupun
+kalian
+kami
+kamilah
+kamu
+kamulah
+kan
+kapan
+kapankah
+kapanpun
+dikarenakan
+karena
+karenanya
+ke
+kecil
+kemudian
+kenapa
+kepada
+kepadanya
+ketika
+seketika
+khususnya
+kini
+kinilah
+kiranya
+sekiranya
+kita
+kitalah
+kok
+lagi
+lagian
+selagi
+lah
+lain
+lainnya
+melainkan
+selaku
+lalu
+melalui
+terlalu
+lama
+lamanya
+selama
+selama
+selamanya
+lebih
+terlebih
+bermacam
+macam
+semacam
+maka
+makanya
+makin
+malah
+malahan
+mampu
+mampukah
+mana
+manakala
+manalagi
+masih
+masihkah
+semasih
+masing
+mau
+maupun
+semaunya
+memang
+mereka
+merekalah
+meski
+meskipun
+semula
+mungkin
+mungkinkah
+nah
+namun
+nanti
+nantinya
+nyaris
+oleh
+olehnya
+seorang
+seseorang
+pada
+padanya
+padahal
+paling
+sepanjang
+pantas
+sepantasnya
+sepantasnyalah
+para
+pasti
+pastilah
+per
+pernah
+pula
+pun
+merupakan
+rupanya
+serupa
+saat
+saatnya
+sesaat
+saja
+sajalah
+saling
+bersama
+sama
+sesama
+sambil
+sampai
+sana
+sangat
+sangatlah
+saya
+sayalah
+se
+sebab
+sebabnya
+sebuah
+tersebut
+tersebutlah
+sedang
+sedangkan
+sedikit
+sedikitnya
+segala
+segalanya
+segera
+sesegera
+sejak
+sejenak
+sekali
+sekalian
+sekalipun
+sesekali
+sekaligus
+sekarang
+sekarang
+sekitar
+sekitarnya
+sela
+selain
+selalu
+seluruh
+seluruhnya
+semakin
+sementara
+sempat
+semua
+semuanya
+sendiri
+sendirinya
+seolah
+seperti
+sepertinya
+sering
+seringnya
+serta
+siapa
+siapakah
+siapapun
+disini
+disinilah
+sini
+sinilah
+sesuatu
+sesuatunya
+suatu
+sesudah
+sesudahnya
+sudah
+sudahkah
+sudahlah
+supaya
+tadi
+tadinya
+tak
+tanpa
+setelah
+telah
+tentang
+tentu
+tentulah
+tentunya
+tertentu
+seterusnya
+tapi
+tetapi
+setiap
+tiap
+setidaknya
+tidak
+tidakkah
+tidaklah
+toh
+waduh
+wah
+wahai
+sewaktu
+walau
+walaupun
+wong
+yaitu
+yakni
+yang
diff --git a/solr/conf/lang/stopwords_it.txt b/solr/conf/lang/stopwords_it.txt
new file mode 100644
index 000000000..1219cc773
--- /dev/null
+++ b/solr/conf/lang/stopwords_it.txt
@@ -0,0 +1,303 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/italian/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | An Italian stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ad | a (to) before vowel
+al | a + il
+allo | a + lo
+ai | a + i
+agli | a + gli
+all | a + l'
+agl | a + gl'
+alla | a + la
+alle | a + le
+con | with
+col | con + il
+coi | con + i (forms collo, cogli etc are now very rare)
+da | from
+dal | da + il
+dallo | da + lo
+dai | da + i
+dagli | da + gli
+dall | da + l'
+dagl | da + gll'
+dalla | da + la
+dalle | da + le
+di | of
+del | di + il
+dello | di + lo
+dei | di + i
+degli | di + gli
+dell | di + l'
+degl | di + gl'
+della | di + la
+delle | di + le
+in | in
+nel | in + el
+nello | in + lo
+nei | in + i
+negli | in + gli
+nell | in + l'
+negl | in + gl'
+nella | in + la
+nelle | in + le
+su | on
+sul | su + il
+sullo | su + lo
+sui | su + i
+sugli | su + gli
+sull | su + l'
+sugl | su + gl'
+sulla | su + la
+sulle | su + le
+per | through, by
+tra | among
+contro | against
+io | I
+tu | thou
+lui | he
+lei | she
+noi | we
+voi | you
+loro | they
+mio | my
+mia |
+miei |
+mie |
+tuo |
+tua |
+tuoi | thy
+tue |
+suo |
+sua |
+suoi | his, her
+sue |
+nostro | our
+nostra |
+nostri |
+nostre |
+vostro | your
+vostra |
+vostri |
+vostre |
+mi | me
+ti | thee
+ci | us, there
+vi | you, there
+lo | him, the
+la | her, the
+li | them
+le | them, the
+gli | to him, the
+ne | from there etc
+il | the
+un | a
+uno | a
+una | a
+ma | but
+ed | and
+se | if
+perché | why, because
+anche | also
+come | how
+dov | where (as dov')
+dove | where
+che | who, that
+chi | who
+cui | whom
+non | not
+più | more
+quale | who, that
+quanto | how much
+quanti |
+quanta |
+quante |
+quello | that
+quelli |
+quella |
+quelle |
+questo | this
+questi |
+questa |
+queste |
+si | yes
+tutto | all
+tutti | all
+
+ | single letter forms:
+
+a | at
+c | as c' for ce or ci
+e | and
+i | the
+l | as l'
+o | or
+
+ | forms of avere, to have (not including the infinitive):
+
+ho
+hai
+ha
+abbiamo
+avete
+hanno
+abbia
+abbiate
+abbiano
+avrò
+avrai
+avrà
+avremo
+avrete
+avranno
+avrei
+avresti
+avrebbe
+avremmo
+avreste
+avrebbero
+avevo
+avevi
+aveva
+avevamo
+avevate
+avevano
+ebbi
+avesti
+ebbe
+avemmo
+aveste
+ebbero
+avessi
+avesse
+avessimo
+avessero
+avendo
+avuto
+avuta
+avuti
+avute
+
+ | forms of essere, to be (not including the infinitive):
+sono
+sei
+siamo
+siete
+sia
+siate
+siano
+sarò
+sarai
+sarà
+saremo
+sarete
+saranno
+sarei
+saresti
+sarebbe
+saremmo
+sareste
+sarebbero
+ero
+eri
+era
+eravamo
+eravate
+erano
+fui
+fosti
+fu
+fummo
+foste
+furono
+fossi
+fosse
+fossimo
+fossero
+essendo
+
+ | forms of fare, to do (not including the infinitive, fa, fat-):
+faccio
+fai
+facciamo
+fanno
+faccia
+facciate
+facciano
+farò
+farai
+farà
+faremo
+farete
+faranno
+farei
+faresti
+farebbe
+faremmo
+fareste
+farebbero
+facevo
+facevi
+faceva
+facevamo
+facevate
+facevano
+feci
+facesti
+fece
+facemmo
+faceste
+fecero
+facessi
+facesse
+facessimo
+facessero
+facendo
+
+ | forms of stare, to be (not including the infinitive):
+sto
+stai
+sta
+stiamo
+stanno
+stia
+stiate
+stiano
+starò
+starai
+starà
+staremo
+starete
+staranno
+starei
+staresti
+starebbe
+staremmo
+stareste
+starebbero
+stavo
+stavi
+stava
+stavamo
+stavate
+stavano
+stetti
+stesti
+stette
+stemmo
+steste
+stettero
+stessi
+stesse
+stessimo
+stessero
+stando
diff --git a/solr/conf/lang/stopwords_ja.txt b/solr/conf/lang/stopwords_ja.txt
new file mode 100644
index 000000000..d4321be6b
--- /dev/null
+++ b/solr/conf/lang/stopwords_ja.txt
@@ -0,0 +1,127 @@
+#
+# This file defines a stopword set for Japanese.
+#
+# This set is made up of hand-picked frequent terms from segmented Japanese Wikipedia.
+# Punctuation characters and frequent kanji have mostly been left out. See LUCENE-3745
+# for frequency lists, etc. that can be useful for making your own set (if desired)
+#
+# Note that there is an overlap between these stopwords and the terms stopped when used
+# in combination with the JapanesePartOfSpeechStopFilter. When editing this file, note
+# that comments are not allowed on the same line as stopwords.
+#
+# Also note that stopping is done in a case-insensitive manner. Change your StopFilter
+# configuration if you need case-sensitive stopping. Lastly, note that stopping is done
+# using the same character width as the entries in this file. Since this StopFilter is
+# normally done after a CJKWidthFilter in your chain, you would usually want your romaji
+# entries to be in half-width and your kana entries to be in full-width.
+#
+の
+に
+は
+を
+た
+が
+で
+て
+と
+し
+れ
+さ
+ある
+いる
+も
+する
+から
+な
+こと
+として
+い
+や
+れる
+など
+なっ
+ない
+この
+ため
+その
+あっ
+よう
+また
+もの
+という
+あり
+まで
+られ
+なる
+へ
+か
+だ
+これ
+によって
+により
+おり
+より
+による
+ず
+なり
+られる
+において
+ば
+なかっ
+なく
+しかし
+について
+せ
+だっ
+その後
+できる
+それ
+う
+ので
+なお
+のみ
+でき
+き
+つ
+における
+および
+いう
+さらに
+でも
+ら
+たり
+その他
+に関する
+たち
+ます
+ん
+なら
+に対して
+特に
+せる
+及び
+これら
+とき
+では
+にて
+ほか
+ながら
+うち
+そして
+とともに
+ただし
+かつて
+それぞれ
+または
+お
+ほど
+ものの
+に対する
+ほとんど
+と共に
+といった
+です
+とも
+ところ
+ここ
+##### End of file
diff --git a/solr/conf/lang/stopwords_lv.txt b/solr/conf/lang/stopwords_lv.txt
new file mode 100644
index 000000000..e21a23c06
--- /dev/null
+++ b/solr/conf/lang/stopwords_lv.txt
@@ -0,0 +1,172 @@
+# Set of Latvian stopwords from A Stemming Algorithm for Latvian, Karlis Kreslins
+# the original list of over 800 forms was refined:
+# pronouns, adverbs, interjections were removed
+#
+# prepositions
+aiz
+ap
+ar
+apakš
+ārpus
+augšpus
+bez
+caur
+dēļ
+gar
+iekš
+iz
+kopš
+labad
+lejpus
+līdz
+no
+otrpus
+pa
+par
+pār
+pēc
+pie
+pirms
+pret
+priekš
+starp
+šaipus
+uz
+viņpus
+virs
+virspus
+zem
+apakšpus
+# Conjunctions
+un
+bet
+jo
+ja
+ka
+lai
+tomēr
+tikko
+turpretī
+arī
+kaut
+gan
+tādēļ
+tā
+ne
+tikvien
+vien
+kā
+ir
+te
+vai
+kamēr
+# Particles
+ar
+diezin
+droši
+diemžēl
+nebūt
+ik
+it
+taču
+nu
+pat
+tiklab
+iekšpus
+nedz
+tik
+nevis
+turpretim
+jeb
+iekam
+iekām
+iekāms
+kolīdz
+līdzko
+tiklīdz
+jebšu
+tālab
+tāpēc
+nekā
+itin
+jā
+jau
+jel
+nē
+nezin
+tad
+tikai
+vis
+tak
+iekams
+vien
+# modal verbs
+būt
+biju
+biji
+bija
+bijām
+bijāt
+esmu
+esi
+esam
+esat
+būšu
+būsi
+būs
+būsim
+būsiet
+tikt
+tiku
+tiki
+tika
+tikām
+tikāt
+tieku
+tiec
+tiek
+tiekam
+tiekat
+tikšu
+tiks
+tiksim
+tiksiet
+tapt
+tapi
+tapāt
+topat
+tapšu
+tapsi
+taps
+tapsim
+tapsiet
+kļūt
+kļuvu
+kļuvi
+kļuva
+kļuvām
+kļuvāt
+kļūstu
+kļūsti
+kļūst
+kļūstam
+kļūstat
+kļūšu
+kļūsi
+kļūs
+kļūsim
+kļūsiet
+# verbs
+varēt
+varēju
+varējām
+varēšu
+varēsim
+var
+varēji
+varējāt
+varēsi
+varēsiet
+varat
+varēja
+varēs
diff --git a/solr/conf/lang/stopwords_nl.txt b/solr/conf/lang/stopwords_nl.txt
new file mode 100644
index 000000000..47a2aeacf
--- /dev/null
+++ b/solr/conf/lang/stopwords_nl.txt
@@ -0,0 +1,119 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/dutch/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | A Dutch stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | This is a ranked list (commonest to rarest) of stopwords derived from
+ | a large sample of Dutch text.
+
+ | Dutch stop words frequently exhibit homonym clashes. These are indicated
+ | clearly below.
+
+de | the
+en | and
+van | of, from
+ik | I, the ego
+te | (1) chez, at etc, (2) to, (3) too
+dat | that, which
+die | that, those, who, which
+in | in, inside
+een | a, an, one
+hij | he
+het | the, it
+niet | not, nothing, naught
+zijn | (1) to be, being, (2) his, one's, its
+is | is
+was | (1) was, past tense of all persons sing. of 'zijn' (to be) (2) wax, (3) the washing, (4) rise of river
+op | on, upon, at, in, up, used up
+aan | on, upon, to (as dative)
+met | with, by
+als | like, such as, when
+voor | (1) before, in front of, (2) furrow
+had | had, past tense all persons sing. of 'hebben' (have)
+er | there
+maar | but, only
+om | round, about, for etc
+hem | him
+dan | then
+zou | should/would, past tense all persons sing. of 'zullen'
+of | or, whether, if
+wat | what, something, anything
+mijn | possessive and noun 'mine'
+men | people, 'one'
+dit | this
+zo | so, thus, in this way
+door | through by
+over | over, across
+ze | she, her, they, them
+zich | oneself
+bij | (1) a bee, (2) by, near, at
+ook | also, too
+tot | till, until
+je | you
+mij | me
+uit | out of, from
+der | Old Dutch form of 'van der' still found in surnames
+daar | (1) there, (2) because
+haar | (1) her, their, them, (2) hair
+naar | (1) unpleasant, unwell etc, (2) towards, (3) as
+heb | present first person sing. of 'to have'
+hoe | how, why
+heeft | present third person sing. of 'to have'
+hebben | 'to have' and various parts thereof
+deze | this
+u | you
+want | (1) for, (2) mitten, (3) rigging
+nog | yet, still
+zal | 'shall', first and third person sing. of verb 'zullen' (will)
+me | me
+zij | she, they
+nu | now
+ge | 'thou', still used in Belgium and south Netherlands
+geen | none
+omdat | because
+iets | something, somewhat
+worden | to become, grow, get
+toch | yet, still
+al | all, every, each
+waren | (1) 'were' (2) to wander, (3) wares, (3)
+veel | much, many
+meer | (1) more, (2) lake
+doen | to do, to make
+toen | then, when
+moet | noun 'spot/mote' and present form of 'to must'
+ben | (1) am, (2) 'are' in interrogative second person singular of 'to be'
+zonder | without
+kan | noun 'can' and present form of 'to be able'
+hun | their, them
+dus | so, consequently
+alles | all, everything, anything
+onder | under, beneath
+ja | yes, of course
+eens | once, one day
+hier | here
+wie | who
+werd | imperfect third person sing. of 'become'
+altijd | always
+doch | yet, but etc
+wordt | present third person sing. of 'become'
+wezen | (1) to be, (2) 'been' as in 'been fishing', (3) orphans
+kunnen | to be able
+ons | us/our
+zelf | self
+tegen | against, towards, at
+na | after, near
+reeds | already
+wil | (1) present tense of 'want', (2) 'will', noun, (3) fender
+kon | could; past tense of 'to be able'
+niets | nothing
+uw | your
+iemand | somebody
+geweest | been; past participle of 'be'
+andere | other
diff --git a/solr/conf/lang/stopwords_no.txt b/solr/conf/lang/stopwords_no.txt
new file mode 100644
index 000000000..a7a2c28ba
--- /dev/null
+++ b/solr/conf/lang/stopwords_no.txt
@@ -0,0 +1,194 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/norwegian/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | A Norwegian stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | This stop word list is for the dominant bokmål dialect. Words unique
+ | to nynorsk are marked *.
+
+ | Revised by Jan Bruusgaard <Jan.Bruusgaard@ssb.no>, Jan 2005
+
+og | and
+i | in
+jeg | I
+det | it/this/that
+at | to (w. inf.)
+en | a/an
+et | a/an
+den | it/this/that
+til | to
+er | is/am/are
+som | who/that
+på | on
+de | they / you(formal)
+med | with
+han | he
+av | of
+ikke | not
+ikkje | not *
+der | there
+så | so
+var | was/were
+meg | me
+seg | you
+men | but
+ett | one
+har | have
+om | about
+vi | we
+min | my
+mitt | my
+ha | have
+hadde | had
+hun | she
+nå | now
+over | over
+da | when/as
+ved | by/know
+fra | from
+du | you
+ut | out
+sin | your
+dem | them
+oss | us
+opp | up
+man | you/one
+kan | can
+hans | his
+hvor | where
+eller | or
+hva | what
+skal | shall/must
+selv | self (reflective)
+sjøl | self (reflective)
+her | here
+alle | all
+vil | will
+bli | become
+ble | became
+blei | became *
+blitt | have become
+kunne | could
+inn | in
+når | when
+være | be
+kom | come
+noen | some
+noe | some
+ville | would
+dere | you
+som | who/which/that
+deres | their/theirs
+kun | only/just
+ja | yes
+etter | after
+ned | down
+skulle | should
+denne | this
+for | for/because
+deg | you
+si | hers/his
+sine | hers/his
+sitt | hers/his
+mot | against
+å | to
+meget | much
+hvorfor | why
+dette | this
+disse | these/those
+uten | without
+hvordan | how
+ingen | none
+din | your
+ditt | your
+blir | become
+samme | same
+hvilken | which
+hvilke | which (plural)
+sånn | such a
+inni | inside/within
+mellom | between
+vår | our
+hver | each
+hvem | who
+vors | us/ours
+hvis | whose
+både | both
+bare | only/just
+enn | than
+fordi | as/because
+før | before
+mange | many
+også | also
+slik | just
+vært | been
+være | to be
+båe | both *
+begge | both
+siden | since
+dykk | your *
+dykkar | yours *
+dei | they *
+deira | them *
+deires | theirs *
+deim | them *
+di | your (fem.) *
+då | as/when *
+eg | I *
+ein | a/an *
+eit | a/an *
+eitt | a/an *
+elles | or *
+honom | he *
+hjå | at *
+ho | she *
+hoe | she *
+henne | her
+hennar | her/hers
+hennes | hers
+hoss | how *
+hossen | how *
+ikkje | not *
+ingi | noone *
+inkje | noone *
+korleis | how *
+korso | how *
+kva | what/which *
+kvar | where *
+kvarhelst | where *
+kven | who/whom *
+kvi | why *
+kvifor | why *
+me | we *
+medan | while *
+mi | my *
+mine | my *
+mykje | much *
+no | now *
+nokon | some (masc./neut.) *
+noka | some (fem.) *
+nokor | some *
+noko | some *
+nokre | some *
+si | his/hers *
+sia | since *
+sidan | since *
+so | so *
+somt | some *
+somme | some *
+um | about*
+upp | up *
+vere | be *
+vore | was *
+verte | become *
+vort | become *
+varte | became *
+vart | became *
+
diff --git a/solr/conf/lang/stopwords_pt.txt b/solr/conf/lang/stopwords_pt.txt
new file mode 100644
index 000000000..acfeb01af
--- /dev/null
+++ b/solr/conf/lang/stopwords_pt.txt
@@ -0,0 +1,253 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/portuguese/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | A Portuguese stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+
+ | The following is a ranked list (commonest to rarest) of stopwords
+ | deriving from a large sample of text.
+
+ | Extra words have been added at the end.
+
+de | of, from
+a | the; to, at; her
+o | the; him
+que | who, that
+e | and
+do | de + o
+da | de + a
+em | in
+um | a
+para | for
+ | é from SER
+com | with
+não | not, no
+uma | a
+os | the; them
+no | em + o
+se | himself etc
+na | em + a
+por | for
+mais | more
+as | the; them
+dos | de + os
+como | as, like
+mas | but
+ | foi from SER
+ao | a + o
+ele | he
+das | de + as
+ | tem from TER
+à | a + a
+seu | his
+sua | her
+ou | or
+ | ser from SER
+quando | when
+muito | much
+ | há from HAV
+nos | em + os; us
+já | already, now
+ | está from EST
+eu | I
+também | also
+só | only, just
+pelo | per + o
+pela | per + a
+até | up to
+isso | that
+ela | he
+entre | between
+ | era from SER
+depois | after
+sem | without
+mesmo | same
+aos | a + os
+ | ter from TER
+seus | his
+quem | whom
+nas | em + as
+me | me
+esse | that
+eles | they
+ | estão from EST
+você | you
+ | tinha from TER
+ | foram from SER
+essa | that
+num | em + um
+nem | nor
+suas | her
+meu | my
+às | a + as
+minha | my
+ | têm from TER
+numa | em + uma
+pelos | per + os
+elas | they
+ | havia from HAV
+ | seja from SER
+qual | which
+ | será from SER
+nós | we
+ | tenho from TER
+lhe | to him, her
+deles | of them
+essas | those
+esses | those
+pelas | per + as
+este | this
+ | fosse from SER
+dele | of him
+
+ | other words. There are many contractions such as naquele = em+aquele,
+ | mo = me+o, but they are rare.
+ | Indefinite article plural forms are also rare.
+
+tu | thou
+te | thee
+vocês | you (plural)
+vos | you
+lhes | to them
+meus | my
+minhas
+teu | thy
+tua
+teus
+tuas
+nosso | our
+nossa
+nossos
+nossas
+
+dela | of her
+delas | of them
+
+esta | this
+estes | these
+estas | these
+aquele | that
+aquela | that
+aqueles | those
+aquelas | those
+isto | this
+aquilo | that
+
+ | forms of estar, to be (not including the infinitive):
+estou
+está
+estamos
+estão
+estive
+esteve
+estivemos
+estiveram
+estava
+estávamos
+estavam
+estivera
+estivéramos
+esteja
+estejamos
+estejam
+estivesse
+estivéssemos
+estivessem
+estiver
+estivermos
+estiverem
+
+ | forms of haver, to have (not including the infinitive):
+hei
+há
+havemos
+hão
+houve
+houvemos
+houveram
+houvera
+houvéramos
+haja
+hajamos
+hajam
+houvesse
+houvéssemos
+houvessem
+houver
+houvermos
+houverem
+houverei
+houverá
+houveremos
+houverão
+houveria
+houveríamos
+houveriam
+
+ | forms of ser, to be (not including the infinitive):
+sou
+somos
+são
+era
+éramos
+eram
+fui
+foi
+fomos
+foram
+fora
+fôramos
+seja
+sejamos
+sejam
+fosse
+fôssemos
+fossem
+for
+formos
+forem
+serei
+será
+seremos
+serão
+seria
+seríamos
+seriam
+
+ | forms of ter, to have (not including the infinitive):
+tenho
+tem
+temos
+tém
+tinha
+tínhamos
+tinham
+tive
+teve
+tivemos
+tiveram
+tivera
+tivéramos
+tenha
+tenhamos
+tenham
+tivesse
+tivéssemos
+tivessem
+tiver
+tivermos
+tiverem
+terei
+terá
+teremos
+terão
+teria
+teríamos
+teriam
diff --git a/solr/conf/lang/stopwords_ro.txt b/solr/conf/lang/stopwords_ro.txt
new file mode 100644
index 000000000..4fdee90a5
--- /dev/null
+++ b/solr/conf/lang/stopwords_ro.txt
@@ -0,0 +1,233 @@
+# This file was created by Jacques Savoy and is distributed under the BSD license.
+# See http://members.unine.ch/jacques.savoy/clef/index.html.
+# Also see http://www.opensource.org/licenses/bsd-license.html
+acea
+aceasta
+această
+aceea
+acei
+aceia
+acel
+acela
+acele
+acelea
+acest
+acesta
+aceste
+acestea
+aceşti
+aceştia
+acolo
+acum
+ai
+aia
+aibă
+aici
+al
+ăla
+ale
+alea
+ălea
+altceva
+altcineva
+am
+ar
+are
+aş
+aşadar
+asemenea
+asta
+ăsta
+astăzi
+astea
+ăstea
+ăştia
+asupra
+aţi
+au
+avea
+avem
+aveţi
+azi
+bine
+bucur
+bună
+ca
+că
+căci
+când
+care
+cărei
+căror
+cărui
+cât
+câte
+câţi
+către
+câtva
+ce
+cel
+ceva
+chiar
+cînd
+cine
+cineva
+cît
+cîte
+cîţi
+cîtva
+contra
+cu
+cum
+cumva
+curând
+curînd
+da
+dă
+dacă
+dar
+datorită
+de
+deci
+deja
+deoarece
+departe
+deşi
+din
+dinaintea
+dintr
+dintre
+drept
+după
+ea
+ei
+el
+ele
+eram
+este
+eşti
+eu
+face
+fără
+fi
+fie
+fiecare
+fii
+fim
+fiţi
+iar
+ieri
+îi
+îl
+îmi
+împotriva
+în
+înainte
+înaintea
+încât
+încît
+încotro
+între
+întrucât
+întrucît
+îţi
+la
+lângă
+le
+li
+lîngă
+lor
+lui
+mă
+mâine
+mea
+mei
+mele
+mereu
+meu
+mi
+mine
+mult
+multă
+mulţi
+ne
+nicăieri
+nici
+nimeni
+nişte
+noastră
+noastre
+noi
+noştri
+nostru
+nu
+ori
+oricând
+oricare
+oricât
+orice
+oricînd
+oricine
+oricît
+oricum
+oriunde
+până
+pe
+pentru
+peste
+pînă
+poate
+pot
+prea
+prima
+primul
+prin
+printr
+sa
+să
+săi
+sale
+sau
+său
+se
+şi
+sînt
+sîntem
+sînteţi
+spre
+sub
+sunt
+suntem
+sunteţi
+ta
+tăi
+tale
+tău
+te
+ţi
+ţie
+tine
+toată
+toate
+tot
+toţi
+totuşi
+tu
+un
+una
+unde
+undeva
+unei
+unele
+uneori
+unor
+vă
+vi
+voastră
+voastre
+voi
+voştri
+vostru
+vouă
+vreo
+vreun
diff --git a/solr/conf/lang/stopwords_ru.txt b/solr/conf/lang/stopwords_ru.txt
new file mode 100644
index 000000000..55271400c
--- /dev/null
+++ b/solr/conf/lang/stopwords_ru.txt
@@ -0,0 +1,243 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/russian/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | a russian stop word list. comments begin with vertical bar. each stop
+ | word is at the start of a line.
+
+ | this is a ranked list (commonest to rarest) of stopwords derived from
+ | a large text sample.
+
+ | letter `ё' is translated to `е'.
+
+и | and
+в | in/into
+во | alternative form
+не | not
+что | what/that
+он | he
+на | on/onto
+я | i
+с | from
+со | alternative form
+как | how
+а | milder form of `no' (but)
+то | conjunction and form of `that'
+все | all
+она | she
+так | so, thus
+его | him
+но | but
+да | yes/and
+ты | thou
+к | towards, by
+у | around, chez
+же | intensifier particle
+вы | you
+за | beyond, behind
+бы | conditional/subj. particle
+по | up to, along
+только | only
+ее | her
+мне | to me
+было | it was
+вот | here is/are, particle
+от | away from
+меня | me
+еще | still, yet, more
+нет | no, there isnt/arent
+о | about
+из | out of
+ему | to him
+теперь | now
+когда | when
+даже | even
+ну | so, well
+вдруг | suddenly
+ли | interrogative particle
+если | if
+уже | already, but homonym of `narrower'
+или | or
+ни | neither
+быть | to be
+был | he was
+него | prepositional form of его
+до | up to
+вас | you accusative
+нибудь | indef. suffix preceded by hyphen
+опять | again
+уж | already, but homonym of `adder'
+вам | to you
+сказал | he said
+ведь | particle `after all'
+там | there
+потом | then
+себя | oneself
+ничего | nothing
+ей | to her
+может | usually with `быть' as `maybe'
+они | they
+тут | here
+где | where
+есть | there is/are
+надо | got to, must
+ней | prepositional form of ей
+для | for
+мы | we
+тебя | thee
+их | them, their
+чем | than
+была | she was
+сам | self
+чтоб | in order to
+без | without
+будто | as if
+человек | man, person, one
+чего | genitive form of `what'
+раз | once
+тоже | also
+себе | to oneself
+под | beneath
+жизнь | life
+будет | will be
+ж | short form of intensifer particle `же'
+тогда | then
+кто | who
+этот | this
+говорил | was saying
+того | genitive form of `that'
+потому | for that reason
+этого | genitive form of `this'
+какой | which
+совсем | altogether
+ним | prepositional form of `его', `они'
+здесь | here
+этом | prepositional form of `этот'
+один | one
+почти | almost
+мой | my
+тем | instrumental/dative plural of `тот', `то'
+чтобы | full form of `in order that'
+нее | her (acc.)
+кажется | it seems
+сейчас | now
+были | they were
+куда | where to
+зачем | why
+сказать | to say
+всех | all (acc., gen. preposn. plural)
+никогда | never
+сегодня | today
+можно | possible, one can
+при | by
+наконец | finally
+два | two
+об | alternative form of `о', about
+другой | another
+хоть | even
+после | after
+над | above
+больше | more
+тот | that one (masc.)
+через | across, in
+эти | these
+нас | us
+про | about
+всего | in all, only, of all
+них | prepositional form of `они' (they)
+какая | which, feminine
+много | lots
+разве | interrogative particle
+сказала | she said
+три | three
+эту | this, acc. fem. sing.
+моя | my, feminine
+впрочем | moreover, besides
+хорошо | good
+свою | ones own, acc. fem. sing.
+этой | oblique form of `эта', fem. `this'
+перед | in front of
+иногда | sometimes
+лучше | better
+чуть | a little
+том | preposn. form of `that one'
+нельзя | one must not
+такой | such a one
+им | to them
+более | more
+всегда | always
+конечно | of course
+всю | acc. fem. sing of `all'
+между | between
+
+
+ | b: some paradigms
+ |
+ | personal pronouns
+ |
+ | я меня мне мной [мною]
+ | ты тебя тебе тобой [тобою]
+ | он его ему им [него, нему, ним]
+ | она ее эи ею [нее, нэи, нею]
+ | оно его ему им [него, нему, ним]
+ |
+ | мы нас нам нами
+ | вы вас вам вами
+ | они их им ими [них, ним, ними]
+ |
+ | себя себе собой [собою]
+ |
+ | demonstrative pronouns: этот (this), тот (that)
+ |
+ | этот эта это эти
+ | этого эты это эти
+ | этого этой этого этих
+ | этому этой этому этим
+ | этим этой этим [этою] этими
+ | этом этой этом этих
+ |
+ | тот та то те
+ | того ту то те
+ | того той того тех
+ | тому той тому тем
+ | тем той тем [тою] теми
+ | том той том тех
+ |
+ | determinative pronouns
+ |
+ | (a) весь (all)
+ |
+ | весь вся все все
+ | всего всю все все
+ | всего всей всего всех
+ | всему всей всему всем
+ | всем всей всем [всею] всеми
+ | всем всей всем всех
+ |
+ | (b) сам (himself etc)
+ |
+ | сам сама само сами
+ | самого саму само самих
+ | самого самой самого самих
+ | самому самой самому самим
+ | самим самой самим [самою] самими
+ | самом самой самом самих
+ |
+ | stems of verbs `to be', `to have', `to do' and modal
+ |
+ | быть бы буд быв есть суть
+ | име
+ | дел
+ | мог мож мочь
+ | уме
+ | хоч хот
+ | долж
+ | можн
+ | нужн
+ | нельзя
+
diff --git a/solr/conf/lang/stopwords_sv.txt b/solr/conf/lang/stopwords_sv.txt
new file mode 100644
index 000000000..096f87f67
--- /dev/null
+++ b/solr/conf/lang/stopwords_sv.txt
@@ -0,0 +1,133 @@
+ | From svn.tartarus.org/snowball/trunk/website/algorithms/swedish/stop.txt
+ | This file is distributed under the BSD License.
+ | See http://snowball.tartarus.org/license.php
+ | Also see http://www.opensource.org/licenses/bsd-license.html
+ | - Encoding was converted to UTF-8.
+ | - This notice was added.
+ |
+ | NOTE: To use this file with StopFilterFactory, you must specify format="snowball"
+
+ | A Swedish stop word list. Comments begin with vertical bar. Each stop
+ | word is at the start of a line.
+
+ | This is a ranked list (commonest to rarest) of stopwords derived from
+ | a large text sample.
+
+ | Swedish stop words occasionally exhibit homonym clashes. For example
+ | så = so, but also seed. These are indicated clearly below.
+
+och | and
+det | it, this/that
+att | to (with infinitive)
+i | in, at
+en | a
+jag | I
+hon | she
+som | who, that
+han | he
+på | on
+den | it, this/that
+med | with
+var | where, each
+sig | him(self) etc
+för | for
+så | so (also: seed)
+till | to
+är | is
+men | but
+ett | a
+om | if; around, about
+hade | had
+de | they, these/those
+av | of
+icke | not, no
+mig | me
+du | you
+henne | her
+då | then, when
+sin | his
+nu | now
+har | have
+inte | inte någon = no one
+hans | his
+honom | him
+skulle | 'sake'
+hennes | her
+där | there
+min | my
+man | one (pronoun)
+ej | nor
+vid | at, by, on (also: vast)
+kunde | could
+något | some etc
+från | from, off
+ut | out
+när | when
+efter | after, behind
+upp | up
+vi | we
+dem | them
+vara | be
+vad | what
+över | over
+än | than
+dig | you
+kan | can
+sina | his
+här | here
+ha | have
+mot | towards
+alla | all
+under | under (also: wonder)
+någon | some etc
+eller | or (else)
+allt | all
+mycket | much
+sedan | since
+ju | why
+denna | this/that
+själv | myself, yourself etc
+detta | this/that
+åt | to
+utan | without
+varit | was
+hur | how
+ingen | no
+mitt | my
+ni | you
+bli | to be, become
+blev | from bli
+oss | us
+din | thy
+dessa | these/those
+några | some etc
+deras | their
+blir | from bli
+mina | my
+samma | (the) same
+vilken | who, that
+er | you, your
+sådan | such a
+vår | our
+blivit | from bli
+dess | its
+inom | within
+mellan | between
+sådant | such a
+varför | why
+varje | each
+vilka | who, that
+ditt | thy
+vem | who
+vilket | who, that
+sitta | his
+sådana | such a
+vart | each
+dina | thy
+vars | whose
+vårt | our
+våra | our
+ert | your
+era | your
+vilkas | whose
+
diff --git a/solr/conf/lang/stopwords_th.txt b/solr/conf/lang/stopwords_th.txt
new file mode 100644
index 000000000..07f0fabe6
--- /dev/null
+++ b/solr/conf/lang/stopwords_th.txt
@@ -0,0 +1,119 @@
+# Thai stopwords from:
+# "Opinion Detection in Thai Political News Columns
+# Based on Subjectivity Analysis"
+# Khampol Sukhum, Supot Nitsuwat, and Choochart Haruechaiyasak
+ไว้
+ไม่
+ไป
+ได้
+ให้
+ใน
+โดย
+แห่ง
+แล้ว
+และ
+แรก
+แบบ
+แต่
+เอง
+เห็น
+เลย
+เริ่ม
+เรา
+เมื่อ
+เพื่อ
+เพราะ
+เป็นการ
+เป็น
+เปิดเผย
+เปิด
+เนื่องจาก
+เดียวกัน
+เดียว
+เช่น
+เฉพาะ
+เคย
+เข้า
+เขา
+อีก
+อาจ
+อะไร
+ออก
+อย่าง
+อยู่
+อยาก
+หาก
+หลาย
+หลังจาก
+หลัง
+หรือ
+หนึ่ง
+ส่วน
+ส่ง
+สุด
+สําหรับ
+ว่า
+วัน
+ลง
+ร่วม
+ราย
+รับ
+ระหว่าง
+รวม
+ยัง
+มี
+มาก
+มา
+พร้อม
+พบ
+ผ่าน
+ผล
+บาง
+น่า
+นี้
+นํา
+นั้น
+นัก
+นอกจาก
+ทุก
+ที่สุด
+ที่
+ทําให้
+ทํา
+ทาง
+ทั้งนี้
+ทั้ง
+ถ้า
+ถูก
+ถึง
+ต้อง
+ต่างๆ
+ต่าง
+ต่อ
+ตาม
+ตั้งแต่
+ตั้ง
+ด้าน
+ด้วย
+ดัง
+ซึ่ง
+ช่วง
+จึง
+จาก
+จัด
+จะ
+คือ
+ความ
+ครั้ง
+คง
+ขึ้น
+ของ
+ขอ
+ขณะ
+ก่อน
+ก็
+การ
+กับ
+กัน
+กว่า
+กล่าว
diff --git a/solr/conf/lang/stopwords_tr.txt b/solr/conf/lang/stopwords_tr.txt
new file mode 100644
index 000000000..84d9408d4
--- /dev/null
+++ b/solr/conf/lang/stopwords_tr.txt
@@ -0,0 +1,212 @@
+# Turkish stopwords from LUCENE-559
+# merged with the list from "Information Retrieval on Turkish Texts"
+# (http://www.users.muohio.edu/canf/papers/JASIST2008offPrint.pdf)
+acaba
+altmış
+altı
+ama
+ancak
+arada
+aslında
+ayrıca
+bana
+bazı
+belki
+ben
+benden
+beni
+benim
+beri
+beş
+bile
+bin
+bir
+birçok
+biri
+birkaç
+birkez
+birşey
+birşeyi
+biz
+bize
+bizden
+bizi
+bizim
+böyle
+böylece
+bu
+buna
+bunda
+bundan
+bunlar
+bunları
+bunların
+bunu
+bunun
+burada
+çok
+çünkü
+da
+daha
+dahi
+de
+defa
+değil
+diğer
+diye
+doksan
+dokuz
+dolayı
+dolayısıyla
+dört
+edecek
+eden
+ederek
+edilecek
+ediliyor
+edilmesi
+ediyor
+eğer
+elli
+en
+etmesi
+etti
+ettiği
+ettiğini
+gibi
+göre
+halen
+hangi
+hatta
+hem
+henüz
+hep
+hepsi
+her
+herhangi
+herkesin
+hiç
+hiçbir
+için
+iki
+ile
+ilgili
+ise
+işte
+itibaren
+itibariyle
+kadar
+karşın
+katrilyon
+kendi
+kendilerine
+kendini
+kendisi
+kendisine
+kendisini
+kez
+ki
+kim
+kimden
+kime
+kimi
+kimse
+kırk
+milyar
+milyon
+mu
+mü
+mı
+nasıl
+ne
+neden
+nedenle
+nerde
+nerede
+nereye
+niye
+niçin
+o
+olan
+olarak
+oldu
+olduğu
+olduğunu
+olduklarını
+olmadı
+olmadığı
+olmak
+olması
+olmayan
+olmaz
+olsa
+olsun
+olup
+olur
+olursa
+oluyor
+on
+ona
+ondan
+onlar
+onlardan
+onları
+onların
+onu
+onun
+otuz
+oysa
+öyle
+pek
+rağmen
+sadece
+sanki
+sekiz
+seksen
+sen
+senden
+seni
+senin
+siz
+sizden
+sizi
+sizin
+şey
+şeyden
+şeyi
+şeyler
+şöyle
+şu
+şuna
+şunda
+şundan
+şunları
+şunu
+tarafından
+trilyon
+tüm
+üç
+üzere
+var
+vardı
+ve
+veya
+ya
+yani
+yapacak
+yapılan
+yapılması
+yapıyor
+yapmak
+yaptı
+yaptığı
+yaptığını
+yaptıkları
+yedi
+yerine
+yetmiş
+yine
+yirmi
+yoksa
+yüz
+zaten
diff --git a/solr/conf/lang/userdict_ja.txt b/solr/conf/lang/userdict_ja.txt
new file mode 100644
index 000000000..6f0368e4d
--- /dev/null
+++ b/solr/conf/lang/userdict_ja.txt
@@ -0,0 +1,29 @@
+#
+# This is a sample user dictionary for Kuromoji (JapaneseTokenizer)
+#
+# Add entries to this file in order to override the statistical model in terms
+# of segmentation, readings and part-of-speech tags. Notice that entries do
+# not have weights since they are always used when found. This is by-design
+# in order to maximize ease-of-use.
+#
+# Entries are defined using the following CSV format:
+# <text>,<token 1> ... <token n>,<reading 1> ... <reading n>,<part-of-speech tag>
+#
+# Notice that a single half-width space separates tokens and readings, and
+# that the number tokens and readings must match exactly.
+#
+# Also notice that multiple entries with the same <text> is undefined.
+#
+# Whitespace only lines are ignored. Comments are not allowed on entry lines.
+#
+
+# Custom segmentation for kanji compounds
+日本経済新聞,日本 経済 新聞,ニホン ケイザイ シンブン,カスタム名詞
+関西国際空港,関西 国際 空港,カンサイ コクサイ クウコウ,カスタム名詞
+
+# Custom segmentation for compound katakana
+トートバッグ,トート バッグ,トート バッグ,かずカナ名詞
+ショルダーバッグ,ショルダー バッグ,ショルダー バッグ,かずカナ名詞
+
+# Custom reading for former sumo wrestler
+朝青龍,朝青龍,アサショウリュウ,カスタム人名
diff --git a/solr/conf/params.json b/solr/conf/params.json
new file mode 100644
index 000000000..06114ef25
--- /dev/null
+++ b/solr/conf/params.json
@@ -0,0 +1,20 @@
+{"params":{
+ "query":{
+ "defType":"edismax",
+ "q.alt":"*:*",
+ "rows":"10",
+ "fl":"*,score",
+ "":{"v":0}
+ },
+ "facets":{
+ "facet":"on",
+ "facet.mincount": "1",
+ "":{"v":0}
+ },
+ "velocity":{
+ "wt": "velocity",
+ "v.template":"browse",
+ "v.layout": "layout",
+ "":{"v":0}
+ }
+}} \ No newline at end of file
diff --git a/solr/conf/protwords.txt b/solr/conf/protwords.txt
new file mode 100644
index 000000000..1dfc0abec
--- /dev/null
+++ b/solr/conf/protwords.txt
@@ -0,0 +1,21 @@
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#-----------------------------------------------------------------------
+# Use a protected word file to protect against the stemmer reducing two
+# unrelated words to the same base word.
+
+# Some non-words that normally won't be encountered,
+# just to test that they won't be stemmed.
+dontstems
+zwhacky
+
diff --git a/solr/conf/schema.xml b/solr/conf/schema.xml
new file mode 100644
index 000000000..9217e015b
--- /dev/null
+++ b/solr/conf/schema.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema name="default-config" version="1.6">
+ <uniqueKey>id</uniqueKey>
+ <fieldType name="string" class="solr.StrField" sortMissingLast="true" docValues="true"/>
+ <fieldType name="text" class="solr.TextField">
+ <analyzer type="index">
+ <tokenizer class="solr.StandardTokenizerFactory"/>
+ <filter class="solr.StopFilterFactory" words="stopwords.txt"/>
+ <filter class="solr.LowerCaseFilterFactory"/>
+ <filter class="solr.PorterStemFilterFactory"/>
+ <filter class="solr.EdgeNGramFilterFactory" minGramSize="3" maxGramSize="12"/>
+ </analyzer>
+ <analyzer type="query">
+ <tokenizer class="solr.StandardTokenizerFactory"/>
+ <filter class="solr.StopFilterFactory" words="stopwords.txt"/>
+ <filter class="solr.LowerCaseFilterFactory"/>
+ <filter class="solr.PorterStemFilterFactory"/>
+ </analyzer>
+ </fieldType>
+ <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100" multiValued="true">
+ <analyzer type="index">
+ <tokenizer class="solr.StandardTokenizerFactory"/>
+ <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
+ <!-- in this example, we will only use synonyms at query time
+ <filter class="solr.SynonymGraphFilterFactory" synonyms="index_synonyms.txt" ignoreCase="true" expand="false"/>
+ <filter class="solr.FlattenGraphFilterFactory"/>
+ -->
+ <filter class="solr.LowerCaseFilterFactory"/>
+ </analyzer>
+ <analyzer type="query">
+ <tokenizer class="solr.StandardTokenizerFactory"/>
+ <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
+ <filter class="solr.SynonymGraphFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
+ <filter class="solr.LowerCaseFilterFactory"/>
+ </analyzer>
+ </fieldType>
+
+ <fieldType name="plong" class="solr.LongPointField"/>
+ <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/>
+ <fieldType name="pdate" class="solr.DatePointField"/>
+ <fieldType name="pdaterange" class="solr.DateRangeField"/>
+ <fieldType name="pdouble" class="solr.DoublePointField"/>
+
+ <field name="text" type="text" indexed="true" stored="false" uninvertible="false" multiValued="true"/>
+ <field name="id" type="string" indexed="true" stored="true" uninvertible="false" required="true"/>
+ <field name="_version_" type="plong" indexed="true" stored="true"/>
+
+ <dynamicField name="*_t" type="text" indexed="true" stored="true" uninvertible="false" docValues="false"/>
+ <dynamicField name="*_n" type="pdouble" indexed="true" stored="true" uninvertible="false" docValues="false"/>
+ <dynamicField name="*_d" type="pdaterange" indexed="true" stored="true" uninvertible="false" docValues="false"/>
+ <dynamicField name="*_l" type="string" indexed="true" stored="true" uninvertible="false" docValues="false" multiValued="true"/>
+ <dynamicField name="*_i" type="string" indexed="true" stored="true" uninvertible="false" docValues="false"/>
+ <dynamicField name="*_b" type="boolean" indexed="true" stored="true" uninvertible="false" docValues="false"/>
+
+ <dynamicField name="*_a" type="text" indexed="true" stored="false" uninvertible="false" docValues="false"/>
+
+ <copyField source="*_t" dest="text"/>
+
+ <copyField source="*_t" dest="*_a"/>
+ <copyField source="*_n" dest="*_a"/>
+</schema>
diff --git a/solr/conf/schema.xml~ b/solr/conf/schema.xml~
new file mode 100644
index 000000000..3b2addf78
--- /dev/null
+++ b/solr/conf/schema.xml~
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema name="default-config" version="1.6">
+ <uniqueKey>id</uniqueKey>
+ <fieldType name="string" class="solr.StrField" sortMissingLast="true" docValues="true"/>
+ <fieldType name="text" class="solr.TextField">
+ <analyzer type="index">
+ <tokenizer class="solr.StandardTokenizerFactory"/>
+ <filter class="solr.StopFilterFactory" words="stopwords.txt"/>
+ <filter class="solr.LowerCaseFilterFactory"/>
+ <filter class="solr.PorterStemFilterFactory"/>
+ <filter class="solr.EdgeNGramFilterFactory" minGramSize="3" maxGramSize="12"/>
+ </analyzer>
+ <analyzer type="query">
+ <tokenizer class="solr.StandardTokenizerFactory"/>
+ <filter class="solr.StopFilterFactory" words="stopwords.txt"/>
+ <filter class="solr.LowerCaseFilterFactory"/>
+ <filter class="solr.PorterStemFilterFactory"/>
+ </analyzer>
+ </fieldType>
+ <field name="data" type="text" indexed="true" stored="true" uninvertible="false"/>
+ <field name="id" type="string" indexed="true" stored="true" uninvertible="false" required="true"/>
+ <field name="_version_" type="long" indexed="false" stored="false" multiValued="false"/>
+</schema>
diff --git a/solr/conf/solrconfig.xml b/solr/conf/solrconfig.xml
new file mode 100644
index 000000000..90eff5363
--- /dev/null
+++ b/solr/conf/solrconfig.xml
@@ -0,0 +1,1328 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+ For more details about configurations options that may appear in
+ this file, see http://wiki.apache.org/solr/SolrConfigXml.
+-->
+<config>
+ <!-- In all configuration below, a prefix of "solr." for class names
+ is an alias that causes solr to search appropriate packages,
+ including org.apache.solr.(search|update|request|core|analysis)
+
+ You may also specify a fully qualified Java classname if you
+ have your own custom plugins.
+ -->
+
+ <!-- Controls what version of Lucene various components of Solr
+ adhere to. Generally, you want to use the latest version to
+ get all bug fixes and improvements. It is highly recommended
+ that you fully re-index after changing this setting as it can
+ affect both how text is indexed and queried.
+ -->
+ <luceneMatchVersion>8.0.0</luceneMatchVersion>
+
+ <!-- <lib/> directives can be used to instruct Solr to load any Jars
+ identified and use them to resolve any "plugins" specified in
+ your solrconfig.xml or schema.xml (ie: Analyzers, Request
+ Handlers, etc...).
+
+ All directories and paths are resolved relative to the
+ instanceDir.
+
+ Please note that <lib/> directives are processed in the order
+ that they appear in your solrconfig.xml file, and are "stacked"
+ on top of each other when building a ClassLoader - so if you have
+ plugin jars with dependencies on other jars, the "lower level"
+ dependency jars should be loaded first.
+
+ If a "./lib" directory exists in your instanceDir, all files
+ found in it are included as if you had used the following
+ syntax...
+
+ <lib dir="./lib" />
+ -->
+
+ <!-- A 'dir' option by itself adds any files found in the directory
+ to the classpath, this is useful for including all jars in a
+ directory.
+
+ When a 'regex' is specified in addition to a 'dir', only the
+ files in that directory which completely match the regex
+ (anchored on both ends) will be included.
+
+ If a 'dir' option (with or without a regex) is used and nothing
+ is found that matches, a warning will be logged.
+
+ The examples below can be used to load some solr-contribs along
+ with their external dependencies.
+ -->
+ <lib dir="${solr.install.dir:../../../..}/contrib/extraction/lib" regex=".*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-cell-\d.*\.jar" />
+
+ <lib dir="${solr.install.dir:../../../..}/contrib/clustering/lib/" regex=".*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-clustering-\d.*\.jar" />
+
+ <lib dir="${solr.install.dir:../../../..}/contrib/langid/lib/" regex=".*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-langid-\d.*\.jar" />
+
+ <lib dir="${solr.install.dir:../../../..}/contrib/velocity/lib" regex=".*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-velocity-\d.*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-ltr-\d.*\.jar" />
+
+ <!-- an exact 'path' can be used instead of a 'dir' to specify a
+ specific jar file. This will cause a serious error to be logged
+ if it can't be loaded.
+ -->
+ <!--
+ <lib path="../a-jar-that-does-not-exist.jar" />
+ -->
+
+ <!-- Data Directory
+
+ Used to specify an alternate directory to hold all index data
+ other than the default ./data under the Solr home. If
+ replication is in use, this should match the replication
+ configuration.
+ -->
+ <dataDir>${solr.data.dir:}</dataDir>
+
+
+ <!-- The DirectoryFactory to use for indexes.
+
+ solr.StandardDirectoryFactory is filesystem
+ based and tries to pick the best implementation for the current
+ JVM and platform. solr.NRTCachingDirectoryFactory, the default,
+ wraps solr.StandardDirectoryFactory and caches small files in memory
+ for better NRT performance.
+
+ One can force a particular implementation via solr.MMapDirectoryFactory,
+ solr.NIOFSDirectoryFactory, or solr.SimpleFSDirectoryFactory.
+
+ solr.RAMDirectoryFactory is memory based and not persistent.
+ -->
+ <directoryFactory name="DirectoryFactory"
+ class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+
+ <schemaFactory class="ClassicIndexSchemaFactory"/>
+ <!-- The CodecFactory for defining the format of the inverted index.
+ The default implementation is SchemaCodecFactory, which is the official Lucene
+ index format, but hooks into the schema to provide per-field customization of
+ the postings lists and per-document values in the fieldType element
+ (postingsFormat/docValuesFormat). Note that most of the alternative implementations
+ are experimental, so if you choose to customize the index format, it's a good
+ idea to convert back to the official format e.g. via IndexWriter.addIndexes(IndexReader)
+ before upgrading to a newer version to avoid unnecessary reindexing.
+ A "compressionMode" string element can be added to <codecFactory> to choose
+ between the existing compression modes in the default codec: "BEST_SPEED" (default)
+ or "BEST_COMPRESSION".
+ -->
+ <codecFactory class="solr.SchemaCodecFactory"/>
+
+ <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Index Config - These settings control low-level behavior of indexing
+ Most example settings here show the default value, but are commented
+ out, to more easily see where customizations have been made.
+
+ Note: This replaces <indexDefaults> and <mainIndex> from older versions
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
+ <indexConfig>
+ <!-- maxFieldLength was removed in 4.0. To get similar behavior, include a
+ LimitTokenCountFilterFactory in your fieldType definition. E.g.
+ <filter class="solr.LimitTokenCountFilterFactory" maxTokenCount="10000"/>
+ -->
+ <!-- Maximum time to wait for a write lock (ms) for an IndexWriter. Default: 1000 -->
+ <!-- <writeLockTimeout>1000</writeLockTimeout> -->
+
+ <!-- Expert: Enabling compound file will use less files for the index,
+ using fewer file descriptors on the expense of performance decrease.
+ Default in Lucene is "true". Default in Solr is "false" (since 3.6) -->
+ <!-- <useCompoundFile>false</useCompoundFile> -->
+
+ <!-- ramBufferSizeMB sets the amount of RAM that may be used by Lucene
+ indexing for buffering added documents and deletions before they are
+ flushed to the Directory.
+ maxBufferedDocs sets a limit on the number of documents buffered
+ before flushing.
+ If both ramBufferSizeMB and maxBufferedDocs is set, then
+ Lucene will flush based on whichever limit is hit first. -->
+ <!-- <ramBufferSizeMB>100</ramBufferSizeMB> -->
+ <!-- <maxBufferedDocs>1000</maxBufferedDocs> -->
+
+ <!-- Expert: Merge Policy
+ The Merge Policy in Lucene controls how merging of segments is done.
+ The default since Solr/Lucene 3.3 is TieredMergePolicy.
+ The default since Lucene 2.3 was the LogByteSizeMergePolicy,
+ Even older versions of Lucene used LogDocMergePolicy.
+ -->
+ <!--
+ <mergePolicyFactory class="org.apache.solr.index.TieredMergePolicyFactory">
+ <int name="maxMergeAtOnce">10</int>
+ <int name="segmentsPerTier">10</int>
+ <double name="noCFSRatio">0.1</double>
+ </mergePolicyFactory>
+ -->
+
+ <!-- Expert: Merge Scheduler
+ The Merge Scheduler in Lucene controls how merges are
+ performed. The ConcurrentMergeScheduler (Lucene 2.3 default)
+ can perform merges in the background using separate threads.
+ The SerialMergeScheduler (Lucene 2.2 default) does not.
+ -->
+ <!--
+ <mergeScheduler class="org.apache.lucene.index.ConcurrentMergeScheduler"/>
+ -->
+
+ <!-- LockFactory
+
+ This option specifies which Lucene LockFactory implementation
+ to use.
+
+ single = SingleInstanceLockFactory - suggested for a
+ read-only index or when there is no possibility of
+ another process trying to modify the index.
+ native = NativeFSLockFactory - uses OS native file locking.
+ Do not use when multiple solr webapps in the same
+ JVM are attempting to share a single index.
+ simple = SimpleFSLockFactory - uses a plain file for locking
+
+ Defaults: 'native' is default for Solr3.6 and later, otherwise
+ 'simple' is the default
+
+ More details on the nuances of each LockFactory...
+ http://wiki.apache.org/lucene-java/AvailableLockFactories
+ -->
+ <lockType>${solr.lock.type:native}</lockType>
+
+ <!-- Commit Deletion Policy
+ Custom deletion policies can be specified here. The class must
+ implement org.apache.lucene.index.IndexDeletionPolicy.
+
+ The default Solr IndexDeletionPolicy implementation supports
+ deleting index commit points on number of commits, age of
+ commit point and optimized status.
+
+ The latest commit point should always be preserved regardless
+ of the criteria.
+ -->
+ <!--
+ <deletionPolicy class="solr.SolrDeletionPolicy">
+ -->
+ <!-- The number of commit points to be kept -->
+ <!-- <str name="maxCommitsToKeep">1</str> -->
+ <!-- The number of optimized commit points to be kept -->
+ <!-- <str name="maxOptimizedCommitsToKeep">0</str> -->
+ <!--
+ Delete all commit points once they have reached the given age.
+ Supports DateMathParser syntax e.g.
+ -->
+ <!--
+ <str name="maxCommitAge">30MINUTES</str>
+ <str name="maxCommitAge">1DAY</str>
+ -->
+ <!--
+ </deletionPolicy>
+ -->
+
+ <!-- Lucene Infostream
+
+ To aid in advanced debugging, Lucene provides an "InfoStream"
+ of detailed information when indexing.
+
+ Setting The value to true will instruct the underlying Lucene
+ IndexWriter to write its debugging info the specified file
+ -->
+ <!-- <infoStream file="INFOSTREAM.txt">false</infoStream> -->
+ </indexConfig>
+
+
+ <!-- JMX
+
+ This example enables JMX if and only if an existing MBeanServer
+ is found, use this if you want to configure JMX through JVM
+ parameters. Remove this to disable exposing Solr configuration
+ and statistics to JMX.
+
+ For more details see http://wiki.apache.org/solr/SolrJmx
+ -->
+ <jmx />
+ <!-- If you want to connect to a particular server, specify the
+ agentId
+ -->
+ <!-- <jmx agentId="myAgent" /> -->
+ <!-- If you want to start a new MBeanServer, specify the serviceUrl -->
+ <!-- <jmx serviceUrl="service:jmx:rmi:///jndi/rmi://localhost:9999/solr"/>
+ -->
+
+ <!-- The default high-performance update handler -->
+ <updateHandler class="solr.DirectUpdateHandler2">
+
+ <!-- Enables a transaction log, used for real-time get, durability, and
+ and solr cloud replica recovery. The log can grow as big as
+ uncommitted changes to the index, so use of a hard autoCommit
+ is recommended (see below).
+ "dir" - the target directory for transaction logs, defaults to the
+ solr data directory.
+ "numVersionBuckets" - sets the number of buckets used to keep
+ track of max version values when checking for re-ordered
+ updates; increase this value to reduce the cost of
+ synchronizing access to version buckets during high-volume
+ indexing, this requires 8 bytes (long) * numVersionBuckets
+ of heap space per Solr core.
+ -->
+ <updateLog>
+ <str name="dir">${solr.ulog.dir:}</str>
+ <int name="numVersionBuckets">${solr.ulog.numVersionBuckets:65536}</int>
+ </updateLog>
+
+ <!-- AutoCommit
+
+ Perform a hard commit automatically under certain conditions.
+ Instead of enabling autoCommit, consider using "commitWithin"
+ when adding documents.
+
+ http://wiki.apache.org/solr/UpdateXmlMessages
+
+ maxDocs - Maximum number of documents to add since the last
+ commit before automatically triggering a new commit.
+
+ maxTime - Maximum amount of time in ms that is allowed to pass
+ since a document was added before automatically
+ triggering a new commit.
+ openSearcher - if false, the commit causes recent index changes
+ to be flushed to stable storage, but does not cause a new
+ searcher to be opened to make those changes visible.
+
+ If the updateLog is enabled, then it's highly recommended to
+ have some sort of hard autoCommit to limit the log size.
+ -->
+ <autoCommit>
+ <maxTime>600000</maxTime>
+ <openSearcher>false</openSearcher>
+ </autoCommit>
+
+ <!-- softAutoCommit is like autoCommit except it causes a
+ 'soft' commit which only ensures that changes are visible
+ but does not ensure that data is synced to disk. This is
+ faster and more near-realtime friendly than a hard commit.
+ -->
+
+ <autoSoftCommit>
+ <maxTime>1000</maxTime>
+ </autoSoftCommit>
+
+ <!-- Update Related Event Listeners
+
+ Various IndexWriter related events can trigger Listeners to
+ take actions.
+
+ postCommit - fired after every commit or optimize command
+ postOptimize - fired after every optimize command
+ -->
+
+ </updateHandler>
+
+ <!-- IndexReaderFactory
+
+ Use the following format to specify a custom IndexReaderFactory,
+ which allows for alternate IndexReader implementations.
+
+ ** Experimental Feature **
+
+ Please note - Using a custom IndexReaderFactory may prevent
+ certain other features from working. The API to
+ IndexReaderFactory may change without warning or may even be
+ removed from future releases if the problems cannot be
+ resolved.
+
+
+ ** Features that may not work with custom IndexReaderFactory **
+
+ The ReplicationHandler assumes a disk-resident index. Using a
+ custom IndexReader implementation may cause incompatibility
+ with ReplicationHandler and may cause replication to not work
+ correctly. See SOLR-1366 for details.
+
+ -->
+ <!--
+ <indexReaderFactory name="IndexReaderFactory" class="package.class">
+ <str name="someArg">Some Value</str>
+ </indexReaderFactory >
+ -->
+
+ <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Query section - these settings control query time things like caches
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
+ <query>
+
+ <!-- Maximum number of clauses in each BooleanQuery, an exception
+ is thrown if exceeded. It is safe to increase or remove this setting,
+ since it is purely an arbitrary limit to try and catch user errors where
+ large boolean queries may not be the best implementation choice.
+ -->
+ <maxBooleanClauses>${solr.max.booleanClauses:1024}</maxBooleanClauses>
+
+ <!-- Solr Internal Query Caches
+
+ There are two implementations of cache available for Solr,
+ LRUCache, based on a synchronized LinkedHashMap, and
+ FastLRUCache, based on a ConcurrentHashMap.
+
+ FastLRUCache has faster gets and slower puts in single
+ threaded operation and thus is generally faster than LRUCache
+ when the hit ratio of the cache is high (> 75%), and may be
+ faster under other scenarios on multi-cpu systems.
+ -->
+
+ <!-- Filter Cache
+
+ Cache used by SolrIndexSearcher for filters (DocSets),
+ unordered sets of *all* documents that match a query. When a
+ new searcher is opened, its caches may be prepopulated or
+ "autowarmed" using data from caches in the old searcher.
+ autowarmCount is the number of items to prepopulate. For
+ LRUCache, the autowarmed items will be the most recently
+ accessed items.
+
+ Parameters:
+ class - the SolrCache implementation LRUCache or
+ (LRUCache or FastLRUCache)
+ size - the maximum number of entries in the cache
+ initialSize - the initial capacity (number of entries) of
+ the cache. (see java.util.HashMap)
+ autowarmCount - the number of entries to prepopulate from
+ and old cache.
+ maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed
+ to occupy. Note that when this option is specified, the size
+ and initialSize parameters are ignored.
+ -->
+ <filterCache class="solr.FastLRUCache"
+ size="512"
+ initialSize="512"
+ autowarmCount="0"/>
+
+ <!-- Query Result Cache
+
+ Caches results of searches - ordered lists of document ids
+ (DocList) based on a query, a sort, and the range of documents requested.
+ Additional supported parameter by LRUCache:
+ maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed
+ to occupy
+ -->
+ <queryResultCache class="solr.LRUCache"
+ size="512"
+ initialSize="512"
+ autowarmCount="0"/>
+
+ <!-- Document Cache
+
+ Caches Lucene Document objects (the stored fields for each
+ document). Since Lucene internal document ids are transient,
+ this cache will not be autowarmed.
+ -->
+ <documentCache class="solr.LRUCache"
+ size="512"
+ initialSize="512"
+ autowarmCount="0"/>
+
+ <!-- custom cache currently used by block join -->
+ <cache name="perSegFilter"
+ class="solr.search.LRUCache"
+ size="10"
+ initialSize="0"
+ autowarmCount="10"
+ regenerator="solr.NoOpRegenerator" />
+
+ <!-- Field Value Cache
+
+ Cache used to hold field values that are quickly accessible
+ by document id. The fieldValueCache is created by default
+ even if not configured here.
+ -->
+ <!--
+ <fieldValueCache class="solr.FastLRUCache"
+ size="512"
+ autowarmCount="128"
+ showItems="32" />
+ -->
+
+ <!-- Custom Cache
+
+ Example of a generic cache. These caches may be accessed by
+ name through SolrIndexSearcher.getCache(),cacheLookup(), and
+ cacheInsert(). The purpose is to enable easy caching of
+ user/application level data. The regenerator argument should
+ be specified as an implementation of solr.CacheRegenerator
+ if autowarming is desired.
+ -->
+ <!--
+ <cache name="myUserCache"
+ class="solr.LRUCache"
+ size="4096"
+ initialSize="1024"
+ autowarmCount="1024"
+ regenerator="com.mycompany.MyRegenerator"
+ />
+ -->
+
+
+ <!-- Lazy Field Loading
+
+ If true, stored fields that are not requested will be loaded
+ lazily. This can result in a significant speed improvement
+ if the usual case is to not load all stored fields,
+ especially if the skipped fields are large compressed text
+ fields.
+ -->
+ <enableLazyFieldLoading>true</enableLazyFieldLoading>
+
+ <!-- Use Filter For Sorted Query
+
+ A possible optimization that attempts to use a filter to
+ satisfy a search. If the requested sort does not include
+ score, then the filterCache will be checked for a filter
+ matching the query. If found, the filter will be used as the
+ source of document ids, and then the sort will be applied to
+ that.
+
+ For most situations, this will not be useful unless you
+ frequently get the same search repeatedly with different sort
+ options, and none of them ever use "score"
+ -->
+ <!--
+ <useFilterForSortedQuery>true</useFilterForSortedQuery>
+ -->
+
+ <!-- Result Window Size
+
+ An optimization for use with the queryResultCache. When a search
+ is requested, a superset of the requested number of document ids
+ are collected. For example, if a search for a particular query
+ requests matching documents 10 through 19, and queryWindowSize is 50,
+ then documents 0 through 49 will be collected and cached. Any further
+ requests in that range can be satisfied via the cache.
+ -->
+ <queryResultWindowSize>20</queryResultWindowSize>
+
+ <!-- Maximum number of documents to cache for any entry in the
+ queryResultCache.
+ -->
+ <queryResultMaxDocsCached>200</queryResultMaxDocsCached>
+
+ <!-- Query Related Event Listeners
+
+ Various IndexSearcher related events can trigger Listeners to
+ take actions.
+
+ newSearcher - fired whenever a new searcher is being prepared
+ and there is a current searcher handling requests (aka
+ registered). It can be used to prime certain caches to
+ prevent long request times for certain requests.
+
+ firstSearcher - fired whenever a new searcher is being
+ prepared but there is no current registered searcher to handle
+ requests or to gain autowarming data from.
+
+
+ -->
+ <!-- QuerySenderListener takes an array of NamedList and executes a
+ local query request for each NamedList in sequence.
+ -->
+ <listener event="newSearcher" class="solr.QuerySenderListener">
+ <arr name="queries">
+ <!--
+ <lst><str name="q">solr</str><str name="sort">price asc</str></lst>
+ <lst><str name="q">rocks</str><str name="sort">weight asc</str></lst>
+ -->
+ </arr>
+ </listener>
+ <listener event="firstSearcher" class="solr.QuerySenderListener">
+ <arr name="queries">
+ <!--
+ <lst>
+ <str name="q">static firstSearcher warming in solrconfig.xml</str>
+ </lst>
+ -->
+ </arr>
+ </listener>
+
+ <!-- Use Cold Searcher
+
+ If a search request comes in and there is no current
+ registered searcher, then immediately register the still
+ warming searcher and use it. If "false" then all requests
+ will block until the first searcher is done warming.
+ -->
+ <useColdSearcher>false</useColdSearcher>
+
+ </query>
+
+
+ <!-- Request Dispatcher
+
+ This section contains instructions for how the SolrDispatchFilter
+ should behave when processing requests for this SolrCore.
+
+ -->
+ <requestDispatcher>
+ <!-- Request Parsing
+
+ These settings indicate how Solr Requests may be parsed, and
+ what restrictions may be placed on the ContentStreams from
+ those requests
+
+ enableRemoteStreaming - enables use of the stream.file
+ and stream.url parameters for specifying remote streams.
+
+ multipartUploadLimitInKB - specifies the max size (in KiB) of
+ Multipart File Uploads that Solr will allow in a Request.
+
+ formdataUploadLimitInKB - specifies the max size (in KiB) of
+ form data (application/x-www-form-urlencoded) sent via
+ POST. You can use POST to pass request parameters not
+ fitting into the URL.
+
+ addHttpRequestToContext - if set to true, it will instruct
+ the requestParsers to include the original HttpServletRequest
+ object in the context map of the SolrQueryRequest under the
+ key "httpRequest". It will not be used by any of the existing
+ Solr components, but may be useful when developing custom
+ plugins.
+
+ *** WARNING ***
+ Before enabling remote streaming, you should make sure your
+ system has authentication enabled.
+
+ <requestParsers enableRemoteStreaming="false"
+ multipartUploadLimitInKB="-1"
+ formdataUploadLimitInKB="-1"
+ addHttpRequestToContext="false"/>
+ -->
+
+ <!-- HTTP Caching
+
+ Set HTTP caching related parameters (for proxy caches and clients).
+
+ The options below instruct Solr not to output any HTTP Caching
+ related headers
+ -->
+ <httpCaching never304="true" />
+ <!-- If you include a <cacheControl> directive, it will be used to
+ generate a Cache-Control header (as well as an Expires header
+ if the value contains "max-age=")
+
+ By default, no Cache-Control header is generated.
+
+ You can use the <cacheControl> option even if you have set
+ never304="true"
+ -->
+ <!--
+ <httpCaching never304="true" >
+ <cacheControl>max-age=30, public</cacheControl>
+ </httpCaching>
+ -->
+ <!-- To enable Solr to respond with automatically generated HTTP
+ Caching headers, and to response to Cache Validation requests
+ correctly, set the value of never304="false"
+
+ This will cause Solr to generate Last-Modified and ETag
+ headers based on the properties of the Index.
+
+ The following options can also be specified to affect the
+ values of these headers...
+
+ lastModFrom - the default value is "openTime" which means the
+ Last-Modified value (and validation against If-Modified-Since
+ requests) will all be relative to when the current Searcher
+ was opened. You can change it to lastModFrom="dirLastMod" if
+ you want the value to exactly correspond to when the physical
+ index was last modified.
+
+ etagSeed="..." is an option you can change to force the ETag
+ header (and validation against If-None-Match requests) to be
+ different even if the index has not changed (ie: when making
+ significant changes to your config file)
+
+ (lastModifiedFrom and etagSeed are both ignored if you use
+ the never304="true" option)
+ -->
+ <!--
+ <httpCaching lastModifiedFrom="openTime"
+ etagSeed="Solr">
+ <cacheControl>max-age=30, public</cacheControl>
+ </httpCaching>
+ -->
+ </requestDispatcher>
+
+ <!-- Request Handlers
+
+ http://wiki.apache.org/solr/SolrRequestHandler
+
+ Incoming queries will be dispatched to a specific handler by name
+ based on the path specified in the request.
+
+ If a Request Handler is declared with startup="lazy", then it will
+ not be initialized until the first request that uses it.
+
+ -->
+ <!-- SearchHandler
+
+ http://wiki.apache.org/solr/SearchHandler
+
+ For processing Search Queries, the primary Request Handler
+ provided with Solr is "SearchHandler" It delegates to a sequent
+ of SearchComponents (see below) and supports distributed
+ queries across multiple shards
+ -->
+ <requestHandler name="/select" class="solr.SearchHandler">
+ <!-- default values for query parameters can be specified, these
+ will be overridden by parameters in the request
+ -->
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ <int name="rows">10</int>
+ <str name="df">text</str>
+ <!-- Default search field
+ <str name="df">text</str>
+ -->
+ <!-- Change from JSON to XML format (the default prior to Solr 7.0)
+ <str name="wt">xml</str>
+ -->
+ </lst>
+ <!-- In addition to defaults, "appends" params can be specified
+ to identify values which should be appended to the list of
+ multi-val params from the query (or the existing "defaults").
+ -->
+ <!-- In this example, the param "fq=instock:true" would be appended to
+ any query time fq params the user may specify, as a mechanism for
+ partitioning the index, independent of any user selected filtering
+ that may also be desired (perhaps as a result of faceted searching).
+
+ NOTE: there is *absolutely* nothing a client can do to prevent these
+ "appends" values from being used, so don't use this mechanism
+ unless you are sure you always want it.
+ -->
+ <!--
+ <lst name="appends">
+ <str name="fq">inStock:true</str>
+ </lst>
+ -->
+ <!-- "invariants" are a way of letting the Solr maintainer lock down
+ the options available to Solr clients. Any params values
+ specified here are used regardless of what values may be specified
+ in either the query, the "defaults", or the "appends" params.
+
+ In this example, the facet.field and facet.query params would
+ be fixed, limiting the facets clients can use. Faceting is
+ not turned on by default - but if the client does specify
+ facet=true in the request, these are the only facets they
+ will be able to see counts for; regardless of what other
+ facet.field or facet.query params they may specify.
+
+ NOTE: there is *absolutely* nothing a client can do to prevent these
+ "invariants" values from being used, so don't use this mechanism
+ unless you are sure you always want it.
+ -->
+ <!--
+ <lst name="invariants">
+ <str name="facet.field">cat</str>
+ <str name="facet.field">manu_exact</str>
+ <str name="facet.query">price:[* TO 500]</str>
+ <str name="facet.query">price:[500 TO *]</str>
+ </lst>
+ -->
+ <!-- If the default list of SearchComponents is not desired, that
+ list can either be overridden completely, or components can be
+ prepended or appended to the default list. (see below)
+ -->
+ <!--
+ <arr name="components">
+ <str>nameOfCustomComponent1</str>
+ <str>nameOfCustomComponent2</str>
+ </arr>
+ -->
+ </requestHandler>
+
+ <!-- A request handler that returns indented JSON by default -->
+ <requestHandler name="/query" class="solr.SearchHandler">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ <str name="wt">json</str>
+ <str name="indent">true</str>
+ </lst>
+ </requestHandler>
+
+
+ <!-- A Robust Example
+
+ This example SearchHandler declaration shows off usage of the
+ SearchHandler with many defaults declared
+
+ Note that multiple instances of the same Request Handler
+ (SearchHandler) can be registered multiple times with different
+ names (and different init parameters)
+ -->
+ <requestHandler name="/browse" class="solr.SearchHandler" useParams="query,facets,velocity,browse">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ </lst>
+ </requestHandler>
+
+ <initParams path="/update/**,/query,/select,/tvrh,/elevate,/spell,/browse">
+ <lst name="defaults">
+ <str name="df">_text_</str>
+ </lst>
+ </initParams>
+
+ <!-- Solr Cell Update Request Handler
+
+ http://wiki.apache.org/solr/ExtractingRequestHandler
+
+ -->
+ <requestHandler name="/update/extract"
+ startup="lazy"
+ class="solr.extraction.ExtractingRequestHandler" >
+ <lst name="defaults">
+ <str name="lowernames">true</str>
+ <str name="fmap.content">_text_</str>
+ </lst>
+ </requestHandler>
+
+ <!-- Search Components
+
+ Search components are registered to SolrCore and used by
+ instances of SearchHandler (which can access them by name)
+
+ By default, the following components are available:
+
+ <searchComponent name="query" class="solr.QueryComponent" />
+ <searchComponent name="facet" class="solr.FacetComponent" />
+ <searchComponent name="mlt" class="solr.MoreLikeThisComponent" />
+ <searchComponent name="highlight" class="solr.HighlightComponent" />
+ <searchComponent name="stats" class="solr.StatsComponent" />
+ <searchComponent name="debug" class="solr.DebugComponent" />
+
+ Default configuration in a requestHandler would look like:
+
+ <arr name="components">
+ <str>query</str>
+ <str>facet</str>
+ <str>mlt</str>
+ <str>highlight</str>
+ <str>stats</str>
+ <str>debug</str>
+ </arr>
+
+ If you register a searchComponent to one of the standard names,
+ that will be used instead of the default.
+
+ To insert components before or after the 'standard' components, use:
+
+ <arr name="first-components">
+ <str>myFirstComponentName</str>
+ </arr>
+
+ <arr name="last-components">
+ <str>myLastComponentName</str>
+ </arr>
+
+ NOTE: The component registered with the name "debug" will
+ always be executed after the "last-components"
+
+ -->
+
+ <!-- Spell Check
+
+ The spell check component can return a list of alternative spelling
+ suggestions.
+
+ http://wiki.apache.org/solr/SpellCheckComponent
+ -->
+ <searchComponent name="spellcheck" class="solr.SpellCheckComponent">
+
+ <str name="queryAnalyzerFieldType">text_general</str>
+
+ <!-- Multiple "Spell Checkers" can be declared and used by this
+ component
+ -->
+
+ <!-- a spellchecker built from a field of the main index -->
+ <lst name="spellchecker">
+ <str name="name">default</str>
+ <str name="field">_text_</str>
+ <str name="classname">solr.DirectSolrSpellChecker</str>
+ <!-- the spellcheck distance measure used, the default is the internal levenshtein -->
+ <str name="distanceMeasure">internal</str>
+ <!-- minimum accuracy needed to be considered a valid spellcheck suggestion -->
+ <float name="accuracy">0.5</float>
+ <!-- the maximum #edits we consider when enumerating terms: can be 1 or 2 -->
+ <int name="maxEdits">2</int>
+ <!-- the minimum shared prefix when enumerating terms -->
+ <int name="minPrefix">1</int>
+ <!-- maximum number of inspections per result. -->
+ <int name="maxInspections">5</int>
+ <!-- minimum length of a query term to be considered for correction -->
+ <int name="minQueryLength">4</int>
+ <!-- maximum threshold of documents a query term can appear to be considered for correction -->
+ <float name="maxQueryFrequency">0.01</float>
+ <!-- uncomment this to require suggestions to occur in 1% of the documents
+ <float name="thresholdTokenFrequency">.01</float>
+ -->
+ </lst>
+
+ <!-- a spellchecker that can break or combine words. See "/spell" handler below for usage -->
+ <!--
+ <lst name="spellchecker">
+ <str name="name">wordbreak</str>
+ <str name="classname">solr.WordBreakSolrSpellChecker</str>
+ <str name="field">name</str>
+ <str name="combineWords">true</str>
+ <str name="breakWords">true</str>
+ <int name="maxChanges">10</int>
+ </lst>
+ -->
+ </searchComponent>
+
+ <!-- A request handler for demonstrating the spellcheck component.
+
+ NOTE: This is purely as an example. The whole purpose of the
+ SpellCheckComponent is to hook it into the request handler that
+ handles your normal user queries so that a separate request is
+ not needed to get suggestions.
+
+ IN OTHER WORDS, THERE IS REALLY GOOD CHANCE THE SETUP BELOW IS
+ NOT WHAT YOU WANT FOR YOUR PRODUCTION SYSTEM!
+
+ See http://wiki.apache.org/solr/SpellCheckComponent for details
+ on the request parameters.
+ -->
+ <requestHandler name="/spell" class="solr.SearchHandler" startup="lazy">
+ <lst name="defaults">
+ <!-- Solr will use suggestions from both the 'default' spellchecker
+ and from the 'wordbreak' spellchecker and combine them.
+ collations (re-written queries) can include a combination of
+ corrections from both spellcheckers -->
+ <str name="spellcheck.dictionary">default</str>
+ <str name="spellcheck">on</str>
+ <str name="spellcheck.extendedResults">true</str>
+ <str name="spellcheck.count">10</str>
+ <str name="spellcheck.alternativeTermCount">5</str>
+ <str name="spellcheck.maxResultsForSuggest">5</str>
+ <str name="spellcheck.collate">true</str>
+ <str name="spellcheck.collateExtendedResults">true</str>
+ <str name="spellcheck.maxCollationTries">10</str>
+ <str name="spellcheck.maxCollations">5</str>
+ </lst>
+ <arr name="last-components">
+ <str>spellcheck</str>
+ </arr>
+ </requestHandler>
+
+ <!-- Term Vector Component
+
+ http://wiki.apache.org/solr/TermVectorComponent
+ -->
+ <searchComponent name="tvComponent" class="solr.TermVectorComponent"/>
+
+ <!-- A request handler for demonstrating the term vector component
+
+ This is purely as an example.
+
+ In reality you will likely want to add the component to your
+ already specified request handlers.
+ -->
+ <requestHandler name="/tvrh" class="solr.SearchHandler" startup="lazy">
+ <lst name="defaults">
+ <bool name="tv">true</bool>
+ </lst>
+ <arr name="last-components">
+ <str>tvComponent</str>
+ </arr>
+ </requestHandler>
+
+ <!-- Clustering Component. (Omitted here. See the default Solr example for a typical configuration.) -->
+
+ <!-- Terms Component
+
+ http://wiki.apache.org/solr/TermsComponent
+
+ A component to return terms and document frequency of those
+ terms
+ -->
+ <searchComponent name="terms" class="solr.TermsComponent"/>
+
+ <!-- A request handler for demonstrating the terms component -->
+ <requestHandler name="/terms" class="solr.SearchHandler" startup="lazy">
+ <lst name="defaults">
+ <bool name="terms">true</bool>
+ <bool name="distrib">false</bool>
+ </lst>
+ <arr name="components">
+ <str>terms</str>
+ </arr>
+ </requestHandler>
+
+
+ <!-- Query Elevation Component
+
+ http://wiki.apache.org/solr/QueryElevationComponent
+
+ a search component that enables you to configure the top
+ results for a given query regardless of the normal lucene
+ scoring.
+ -->
+ <searchComponent name="elevator" class="solr.QueryElevationComponent" >
+ <!-- pick a fieldType to analyze queries -->
+ <str name="queryFieldType">string</str>
+ </searchComponent>
+
+ <!-- A request handler for demonstrating the elevator component -->
+ <requestHandler name="/elevate" class="solr.SearchHandler" startup="lazy">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ </lst>
+ <arr name="last-components">
+ <str>elevator</str>
+ </arr>
+ </requestHandler>
+
+ <!-- Highlighting Component
+
+ http://wiki.apache.org/solr/HighlightingParameters
+ -->
+ <searchComponent class="solr.HighlightComponent" name="highlight">
+ <highlighting>
+ <!-- Configure the standard fragmenter -->
+ <!-- This could most likely be commented out in the "default" case -->
+ <fragmenter name="gap"
+ default="true"
+ class="solr.highlight.GapFragmenter">
+ <lst name="defaults">
+ <int name="hl.fragsize">100</int>
+ </lst>
+ </fragmenter>
+
+ <!-- A regular-expression-based fragmenter
+ (for sentence extraction)
+ -->
+ <fragmenter name="regex"
+ class="solr.highlight.RegexFragmenter">
+ <lst name="defaults">
+ <!-- slightly smaller fragsizes work better because of slop -->
+ <int name="hl.fragsize">70</int>
+ <!-- allow 50% slop on fragment sizes -->
+ <float name="hl.regex.slop">0.5</float>
+ <!-- a basic sentence pattern -->
+ <str name="hl.regex.pattern">[-\w ,/\n\&quot;&apos;]{20,200}</str>
+ </lst>
+ </fragmenter>
+
+ <!-- Configure the standard formatter -->
+ <formatter name="html"
+ default="true"
+ class="solr.highlight.HtmlFormatter">
+ <lst name="defaults">
+ <str name="hl.simple.pre"><![CDATA[<em>]]></str>
+ <str name="hl.simple.post"><![CDATA[</em>]]></str>
+ </lst>
+ </formatter>
+
+ <!-- Configure the standard encoder -->
+ <encoder name="html"
+ class="solr.highlight.HtmlEncoder" />
+
+ <!-- Configure the standard fragListBuilder -->
+ <fragListBuilder name="simple"
+ class="solr.highlight.SimpleFragListBuilder"/>
+
+ <!-- Configure the single fragListBuilder -->
+ <fragListBuilder name="single"
+ class="solr.highlight.SingleFragListBuilder"/>
+
+ <!-- Configure the weighted fragListBuilder -->
+ <fragListBuilder name="weighted"
+ default="true"
+ class="solr.highlight.WeightedFragListBuilder"/>
+
+ <!-- default tag FragmentsBuilder -->
+ <fragmentsBuilder name="default"
+ default="true"
+ class="solr.highlight.ScoreOrderFragmentsBuilder">
+ <!--
+ <lst name="defaults">
+ <str name="hl.multiValuedSeparatorChar">/</str>
+ </lst>
+ -->
+ </fragmentsBuilder>
+
+ <!-- multi-colored tag FragmentsBuilder -->
+ <fragmentsBuilder name="colored"
+ class="solr.highlight.ScoreOrderFragmentsBuilder">
+ <lst name="defaults">
+ <str name="hl.tag.pre"><![CDATA[
+ <b style="background:yellow">,<b style="background:lawgreen">,
+ <b style="background:aquamarine">,<b style="background:magenta">,
+ <b style="background:palegreen">,<b style="background:coral">,
+ <b style="background:wheat">,<b style="background:khaki">,
+ <b style="background:lime">,<b style="background:deepskyblue">]]></str>
+ <str name="hl.tag.post"><![CDATA[</b>]]></str>
+ </lst>
+ </fragmentsBuilder>
+
+ <boundaryScanner name="default"
+ default="true"
+ class="solr.highlight.SimpleBoundaryScanner">
+ <lst name="defaults">
+ <str name="hl.bs.maxScan">10</str>
+ <str name="hl.bs.chars">.,!? &#9;&#10;&#13;</str>
+ </lst>
+ </boundaryScanner>
+
+ <boundaryScanner name="breakIterator"
+ class="solr.highlight.BreakIteratorBoundaryScanner">
+ <lst name="defaults">
+ <!-- type should be one of CHARACTER, WORD(default), LINE and SENTENCE -->
+ <str name="hl.bs.type">WORD</str>
+ <!-- language and country are used when constructing Locale object. -->
+ <!-- And the Locale object will be used when getting instance of BreakIterator -->
+ <str name="hl.bs.language">en</str>
+ <str name="hl.bs.country">US</str>
+ </lst>
+ </boundaryScanner>
+ </highlighting>
+ </searchComponent>
+
+ <!-- Update Processors
+
+ Chains of Update Processor Factories for dealing with Update
+ Requests can be declared, and then used by name in Update
+ Request Processors
+
+ http://wiki.apache.org/solr/UpdateRequestProcessor
+
+ -->
+
+ <!-- Add unknown fields to the schema
+
+ Field type guessing update processors that will
+ attempt to parse string-typed field values as Booleans, Longs,
+ Doubles, or Dates, and then add schema fields with the guessed
+ field types. Text content will be indexed as "text_general" as
+ well as a copy to a plain string version in *_str.
+
+ These require that the schema is both managed and mutable, by
+ declaring schemaFactory as ManagedIndexSchemaFactory, with
+ mutable specified as true.
+
+ See http://wiki.apache.org/solr/GuessingFieldTypes
+ -->
+ <updateProcessor class="solr.UUIDUpdateProcessorFactory" name="uuid"/>
+ <updateProcessor class="solr.RemoveBlankFieldUpdateProcessorFactory" name="remove-blank"/>
+ <updateProcessor class="solr.FieldNameMutatingUpdateProcessorFactory" name="field-name-mutating">
+ <str name="pattern">[^\w-\.]</str>
+ <str name="replacement">_</str>
+ </updateProcessor>
+ <updateProcessor class="solr.ParseBooleanFieldUpdateProcessorFactory" name="parse-boolean"/>
+ <updateProcessor class="solr.ParseLongFieldUpdateProcessorFactory" name="parse-long"/>
+ <updateProcessor class="solr.ParseDoubleFieldUpdateProcessorFactory" name="parse-double"/>
+ <updateProcessor class="solr.ParseDateFieldUpdateProcessorFactory" name="parse-date">
+ <arr name="format">
+ <str>yyyy-MM-dd['T'[HH:mm[:ss[.SSS]][z</str>
+ <str>yyyy-MM-dd['T'[HH:mm[:ss[,SSS]][z</str>
+ <str>yyyy-MM-dd HH:mm[:ss[.SSS]][z</str>
+ <str>yyyy-MM-dd HH:mm[:ss[,SSS]][z</str>
+ <str>[EEE, ]dd MMM yyyy HH:mm[:ss] z</str>
+ <str>EEEE, dd-MMM-yy HH:mm:ss z</str>
+ <str>EEE MMM ppd HH:mm:ss [z ]yyyy</str>
+ </arr>
+ </updateProcessor>
+
+ <!-- The update.autoCreateFields property can be turned to false to disable schemaless mode -->
+ <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:false}"
+ processor="uuid,remove-blank,field-name-mutating,parse-boolean,parse-long,parse-double,parse-date">
+ <processor class="solr.LogUpdateProcessorFactory"/>
+ <processor class="solr.DistributedUpdateProcessorFactory"/>
+ <processor class="solr.RunUpdateProcessorFactory"/>
+ </updateRequestProcessorChain>
+
+ <!-- Deduplication
+
+ An example dedup update processor that creates the "id" field
+ on the fly based on the hash code of some other fields. This
+ example has overwriteDupes set to false since we are using the
+ id field as the signatureField and Solr will maintain
+ uniqueness based on that anyway.
+
+ -->
+ <!--
+ <updateRequestProcessorChain name="dedupe">
+ <processor class="solr.processor.SignatureUpdateProcessorFactory">
+ <bool name="enabled">true</bool>
+ <str name="signatureField">id</str>
+ <bool name="overwriteDupes">false</bool>
+ <str name="fields">name,features,cat</str>
+ <str name="signatureClass">solr.processor.Lookup3Signature</str>
+ </processor>
+ <processor class="solr.LogUpdateProcessorFactory" />
+ <processor class="solr.RunUpdateProcessorFactory" />
+ </updateRequestProcessorChain>
+ -->
+
+ <!-- Language identification
+
+ This example update chain identifies the language of the incoming
+ documents using the langid contrib. The detected language is
+ written to field language_s. No field name mapping is done.
+ The fields used for detection are text, title, subject and description,
+ making this example suitable for detecting languages form full-text
+ rich documents injected via ExtractingRequestHandler.
+ See more about langId at http://wiki.apache.org/solr/LanguageDetection
+ -->
+ <!--
+ <updateRequestProcessorChain name="langid">
+ <processor class="org.apache.solr.update.processor.TikaLanguageIdentifierUpdateProcessorFactory">
+ <str name="langid.fl">text,title,subject,description</str>
+ <str name="langid.langField">language_s</str>
+ <str name="langid.fallback">en</str>
+ </processor>
+ <processor class="solr.LogUpdateProcessorFactory" />
+ <processor class="solr.RunUpdateProcessorFactory" />
+ </updateRequestProcessorChain>
+ -->
+
+ <!-- Script update processor
+
+ This example hooks in an update processor implemented using JavaScript.
+
+ See more about the script update processor at http://wiki.apache.org/solr/ScriptUpdateProcessor
+ -->
+ <!--
+ <updateRequestProcessorChain name="script">
+ <processor class="solr.StatelessScriptUpdateProcessorFactory">
+ <str name="script">update-script.js</str>
+ <lst name="params">
+ <str name="config_param">example config parameter</str>
+ </lst>
+ </processor>
+ <processor class="solr.RunUpdateProcessorFactory" />
+ </updateRequestProcessorChain>
+ -->
+
+ <!-- Response Writers
+
+ http://wiki.apache.org/solr/QueryResponseWriter
+
+ Request responses will be written using the writer specified by
+ the 'wt' request parameter matching the name of a registered
+ writer.
+
+ The "default" writer is the default and will be used if 'wt' is
+ not specified in the request.
+ -->
+ <!-- The following response writers are implicitly configured unless
+ overridden...
+ -->
+ <!--
+ <queryResponseWriter name="xml"
+ default="true"
+ class="solr.XMLResponseWriter" />
+ <queryResponseWriter name="json" class="solr.JSONResponseWriter"/>
+ <queryResponseWriter name="python" class="solr.PythonResponseWriter"/>
+ <queryResponseWriter name="ruby" class="solr.RubyResponseWriter"/>
+ <queryResponseWriter name="php" class="solr.PHPResponseWriter"/>
+ <queryResponseWriter name="phps" class="solr.PHPSerializedResponseWriter"/>
+ <queryResponseWriter name="csv" class="solr.CSVResponseWriter"/>
+ <queryResponseWriter name="schema.xml" class="solr.SchemaXmlResponseWriter"/>
+ -->
+
+ <queryResponseWriter name="json" class="solr.JSONResponseWriter">
+ <!-- For the purposes of the tutorial, JSON responses are written as
+ plain text so that they are easy to read in *any* browser.
+ If you expect a MIME type of "application/json" just remove this override.
+ -->
+ <str name="content-type">text/plain; charset=UTF-8</str>
+ </queryResponseWriter>
+
+ <!--
+ Custom response writers can be declared as needed...
+ -->
+ <queryResponseWriter name="velocity" class="solr.VelocityResponseWriter" startup="lazy">
+ <str name="template.base.dir">${velocity.template.base.dir:}</str>
+ <str name="solr.resource.loader.enabled">${velocity.solr.resource.loader.enabled:true}</str>
+ <str name="params.resource.loader.enabled">${velocity.params.resource.loader.enabled:false}</str>
+ </queryResponseWriter>
+
+ <!-- XSLT response writer transforms the XML output by any xslt file found
+ in Solr's conf/xslt directory. Changes to xslt files are checked for
+ every xsltCacheLifetimeSeconds.
+ -->
+ <queryResponseWriter name="xslt" class="solr.XSLTResponseWriter">
+ <int name="xsltCacheLifetimeSeconds">5</int>
+ </queryResponseWriter>
+
+ <!-- Query Parsers
+
+ https://lucene.apache.org/solr/guide/query-syntax-and-parsing.html
+
+ Multiple QParserPlugins can be registered by name, and then
+ used in either the "defType" param for the QueryComponent (used
+ by SearchHandler) or in LocalParams
+ -->
+ <!-- example of registering a query parser -->
+ <!--
+ <queryParser name="myparser" class="com.mycompany.MyQParserPlugin"/>
+ -->
+
+ <!-- Function Parsers
+
+ http://wiki.apache.org/solr/FunctionQuery
+
+ Multiple ValueSourceParsers can be registered by name, and then
+ used as function names when using the "func" QParser.
+ -->
+ <!-- example of registering a custom function parser -->
+ <!--
+ <valueSourceParser name="myfunc"
+ class="com.mycompany.MyValueSourceParser" />
+ -->
+
+
+ <!-- Document Transformers
+ http://wiki.apache.org/solr/DocTransformers
+ -->
+ <!--
+ Could be something like:
+ <transformer name="db" class="com.mycompany.LoadFromDatabaseTransformer" >
+ <int name="connection">jdbc://....</int>
+ </transformer>
+
+ To add a constant value to all docs, use:
+ <transformer name="mytrans2" class="org.apache.solr.response.transform.ValueAugmenterFactory" >
+ <int name="value">5</int>
+ </transformer>
+
+ If you want the user to still be able to change it with _value:something_ use this:
+ <transformer name="mytrans3" class="org.apache.solr.response.transform.ValueAugmenterFactory" >
+ <double name="defaultValue">5</double>
+ </transformer>
+
+ If you are using the QueryElevationComponent, you may wish to mark documents that get boosted. The
+ EditorialMarkerFactory will do exactly that:
+ <transformer name="qecBooster" class="org.apache.solr.response.transform.EditorialMarkerFactory" />
+ -->
+</config>
diff --git a/solr/conf/solrconfig.xml~ b/solr/conf/solrconfig.xml~
new file mode 100644
index 000000000..5854cf2d9
--- /dev/null
+++ b/solr/conf/solrconfig.xml~
@@ -0,0 +1,1358 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+ For more details about configurations options that may appear in
+ this file, see http://wiki.apache.org/solr/SolrConfigXml.
+-->
+<config>
+ <!-- In all configuration below, a prefix of "solr." for class names
+ is an alias that causes solr to search appropriate packages,
+ including org.apache.solr.(search|update|request|core|analysis)
+
+ You may also specify a fully qualified Java classname if you
+ have your own custom plugins.
+ -->
+
+ <!-- Controls what version of Lucene various components of Solr
+ adhere to. Generally, you want to use the latest version to
+ get all bug fixes and improvements. It is highly recommended
+ that you fully re-index after changing this setting as it can
+ affect both how text is indexed and queried.
+ -->
+ <luceneMatchVersion>8.0.0</luceneMatchVersion>
+
+ <!-- <lib/> directives can be used to instruct Solr to load any Jars
+ identified and use them to resolve any "plugins" specified in
+ your solrconfig.xml or schema.xml (ie: Analyzers, Request
+ Handlers, etc...).
+
+ All directories and paths are resolved relative to the
+ instanceDir.
+
+ Please note that <lib/> directives are processed in the order
+ that they appear in your solrconfig.xml file, and are "stacked"
+ on top of each other when building a ClassLoader - so if you have
+ plugin jars with dependencies on other jars, the "lower level"
+ dependency jars should be loaded first.
+
+ If a "./lib" directory exists in your instanceDir, all files
+ found in it are included as if you had used the following
+ syntax...
+
+ <lib dir="./lib" />
+ -->
+
+ <!-- A 'dir' option by itself adds any files found in the directory
+ to the classpath, this is useful for including all jars in a
+ directory.
+
+ When a 'regex' is specified in addition to a 'dir', only the
+ files in that directory which completely match the regex
+ (anchored on both ends) will be included.
+
+ If a 'dir' option (with or without a regex) is used and nothing
+ is found that matches, a warning will be logged.
+
+ The examples below can be used to load some solr-contribs along
+ with their external dependencies.
+ -->
+ <lib dir="${solr.install.dir:../../../..}/contrib/extraction/lib" regex=".*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-cell-\d.*\.jar" />
+
+ <lib dir="${solr.install.dir:../../../..}/contrib/clustering/lib/" regex=".*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-clustering-\d.*\.jar" />
+
+ <lib dir="${solr.install.dir:../../../..}/contrib/langid/lib/" regex=".*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-langid-\d.*\.jar" />
+
+ <lib dir="${solr.install.dir:../../../..}/contrib/velocity/lib" regex=".*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-velocity-\d.*\.jar" />
+ <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-ltr-\d.*\.jar" />
+
+ <!-- an exact 'path' can be used instead of a 'dir' to specify a
+ specific jar file. This will cause a serious error to be logged
+ if it can't be loaded.
+ -->
+ <!--
+ <lib path="../a-jar-that-does-not-exist.jar" />
+ -->
+
+ <!-- Data Directory
+
+ Used to specify an alternate directory to hold all index data
+ other than the default ./data under the Solr home. If
+ replication is in use, this should match the replication
+ configuration.
+ -->
+ <dataDir>${solr.data.dir:}</dataDir>
+
+
+ <!-- The DirectoryFactory to use for indexes.
+
+ solr.StandardDirectoryFactory is filesystem
+ based and tries to pick the best implementation for the current
+ JVM and platform. solr.NRTCachingDirectoryFactory, the default,
+ wraps solr.StandardDirectoryFactory and caches small files in memory
+ for better NRT performance.
+
+ One can force a particular implementation via solr.MMapDirectoryFactory,
+ solr.NIOFSDirectoryFactory, or solr.SimpleFSDirectoryFactory.
+
+ solr.RAMDirectoryFactory is memory based and not persistent.
+ -->
+ <directoryFactory name="DirectoryFactory"
+ class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+
+ <schemaFactory class="ClassicIndexSchemaFactory"/>
+ <!-- The CodecFactory for defining the format of the inverted index.
+ The default implementation is SchemaCodecFactory, which is the official Lucene
+ index format, but hooks into the schema to provide per-field customization of
+ the postings lists and per-document values in the fieldType element
+ (postingsFormat/docValuesFormat). Note that most of the alternative implementations
+ are experimental, so if you choose to customize the index format, it's a good
+ idea to convert back to the official format e.g. via IndexWriter.addIndexes(IndexReader)
+ before upgrading to a newer version to avoid unnecessary reindexing.
+ A "compressionMode" string element can be added to <codecFactory> to choose
+ between the existing compression modes in the default codec: "BEST_SPEED" (default)
+ or "BEST_COMPRESSION".
+ -->
+ <codecFactory class="solr.SchemaCodecFactory"/>
+
+ <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Index Config - These settings control low-level behavior of indexing
+ Most example settings here show the default value, but are commented
+ out, to more easily see where customizations have been made.
+
+ Note: This replaces <indexDefaults> and <mainIndex> from older versions
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
+ <indexConfig>
+ <!-- maxFieldLength was removed in 4.0. To get similar behavior, include a
+ LimitTokenCountFilterFactory in your fieldType definition. E.g.
+ <filter class="solr.LimitTokenCountFilterFactory" maxTokenCount="10000"/>
+ -->
+ <!-- Maximum time to wait for a write lock (ms) for an IndexWriter. Default: 1000 -->
+ <!-- <writeLockTimeout>1000</writeLockTimeout> -->
+
+ <!-- Expert: Enabling compound file will use less files for the index,
+ using fewer file descriptors on the expense of performance decrease.
+ Default in Lucene is "true". Default in Solr is "false" (since 3.6) -->
+ <!-- <useCompoundFile>false</useCompoundFile> -->
+
+ <!-- ramBufferSizeMB sets the amount of RAM that may be used by Lucene
+ indexing for buffering added documents and deletions before they are
+ flushed to the Directory.
+ maxBufferedDocs sets a limit on the number of documents buffered
+ before flushing.
+ If both ramBufferSizeMB and maxBufferedDocs is set, then
+ Lucene will flush based on whichever limit is hit first. -->
+ <!-- <ramBufferSizeMB>100</ramBufferSizeMB> -->
+ <!-- <maxBufferedDocs>1000</maxBufferedDocs> -->
+
+ <!-- Expert: Merge Policy
+ The Merge Policy in Lucene controls how merging of segments is done.
+ The default since Solr/Lucene 3.3 is TieredMergePolicy.
+ The default since Lucene 2.3 was the LogByteSizeMergePolicy,
+ Even older versions of Lucene used LogDocMergePolicy.
+ -->
+ <!--
+ <mergePolicyFactory class="org.apache.solr.index.TieredMergePolicyFactory">
+ <int name="maxMergeAtOnce">10</int>
+ <int name="segmentsPerTier">10</int>
+ <double name="noCFSRatio">0.1</double>
+ </mergePolicyFactory>
+ -->
+
+ <!-- Expert: Merge Scheduler
+ The Merge Scheduler in Lucene controls how merges are
+ performed. The ConcurrentMergeScheduler (Lucene 2.3 default)
+ can perform merges in the background using separate threads.
+ The SerialMergeScheduler (Lucene 2.2 default) does not.
+ -->
+ <!--
+ <mergeScheduler class="org.apache.lucene.index.ConcurrentMergeScheduler"/>
+ -->
+
+ <!-- LockFactory
+
+ This option specifies which Lucene LockFactory implementation
+ to use.
+
+ single = SingleInstanceLockFactory - suggested for a
+ read-only index or when there is no possibility of
+ another process trying to modify the index.
+ native = NativeFSLockFactory - uses OS native file locking.
+ Do not use when multiple solr webapps in the same
+ JVM are attempting to share a single index.
+ simple = SimpleFSLockFactory - uses a plain file for locking
+
+ Defaults: 'native' is default for Solr3.6 and later, otherwise
+ 'simple' is the default
+
+ More details on the nuances of each LockFactory...
+ http://wiki.apache.org/lucene-java/AvailableLockFactories
+ -->
+ <lockType>${solr.lock.type:native}</lockType>
+
+ <!-- Commit Deletion Policy
+ Custom deletion policies can be specified here. The class must
+ implement org.apache.lucene.index.IndexDeletionPolicy.
+
+ The default Solr IndexDeletionPolicy implementation supports
+ deleting index commit points on number of commits, age of
+ commit point and optimized status.
+
+ The latest commit point should always be preserved regardless
+ of the criteria.
+ -->
+ <!--
+ <deletionPolicy class="solr.SolrDeletionPolicy">
+ -->
+ <!-- The number of commit points to be kept -->
+ <!-- <str name="maxCommitsToKeep">1</str> -->
+ <!-- The number of optimized commit points to be kept -->
+ <!-- <str name="maxOptimizedCommitsToKeep">0</str> -->
+ <!--
+ Delete all commit points once they have reached the given age.
+ Supports DateMathParser syntax e.g.
+ -->
+ <!--
+ <str name="maxCommitAge">30MINUTES</str>
+ <str name="maxCommitAge">1DAY</str>
+ -->
+ <!--
+ </deletionPolicy>
+ -->
+
+ <!-- Lucene Infostream
+
+ To aid in advanced debugging, Lucene provides an "InfoStream"
+ of detailed information when indexing.
+
+ Setting The value to true will instruct the underlying Lucene
+ IndexWriter to write its debugging info the specified file
+ -->
+ <!-- <infoStream file="INFOSTREAM.txt">false</infoStream> -->
+ </indexConfig>
+
+
+ <!-- JMX
+
+ This example enables JMX if and only if an existing MBeanServer
+ is found, use this if you want to configure JMX through JVM
+ parameters. Remove this to disable exposing Solr configuration
+ and statistics to JMX.
+
+ For more details see http://wiki.apache.org/solr/SolrJmx
+ -->
+ <jmx />
+ <!-- If you want to connect to a particular server, specify the
+ agentId
+ -->
+ <!-- <jmx agentId="myAgent" /> -->
+ <!-- If you want to start a new MBeanServer, specify the serviceUrl -->
+ <!-- <jmx serviceUrl="service:jmx:rmi:///jndi/rmi://localhost:9999/solr"/>
+ -->
+
+ <!-- The default high-performance update handler -->
+ <updateHandler class="solr.DirectUpdateHandler2">
+
+ <!-- Enables a transaction log, used for real-time get, durability, and
+ and solr cloud replica recovery. The log can grow as big as
+ uncommitted changes to the index, so use of a hard autoCommit
+ is recommended (see below).
+ "dir" - the target directory for transaction logs, defaults to the
+ solr data directory.
+ "numVersionBuckets" - sets the number of buckets used to keep
+ track of max version values when checking for re-ordered
+ updates; increase this value to reduce the cost of
+ synchronizing access to version buckets during high-volume
+ indexing, this requires 8 bytes (long) * numVersionBuckets
+ of heap space per Solr core.
+ -->
+ <updateLog>
+ <str name="dir">${solr.ulog.dir:}</str>
+ <int name="numVersionBuckets">${solr.ulog.numVersionBuckets:65536}</int>
+ </updateLog>
+
+ <!-- AutoCommit
+
+ Perform a hard commit automatically under certain conditions.
+ Instead of enabling autoCommit, consider using "commitWithin"
+ when adding documents.
+
+ http://wiki.apache.org/solr/UpdateXmlMessages
+
+ maxDocs - Maximum number of documents to add since the last
+ commit before automatically triggering a new commit.
+
+ maxTime - Maximum amount of time in ms that is allowed to pass
+ since a document was added before automatically
+ triggering a new commit.
+ openSearcher - if false, the commit causes recent index changes
+ to be flushed to stable storage, but does not cause a new
+ searcher to be opened to make those changes visible.
+
+ If the updateLog is enabled, then it's highly recommended to
+ have some sort of hard autoCommit to limit the log size.
+ -->
+ <autoCommit>
+ <maxTime>600000</maxTime>
+ <openSearcher>false</openSearcher>
+ </autoCommit>
+
+ <!-- softAutoCommit is like autoCommit except it causes a
+ 'soft' commit which only ensures that changes are visible
+ but does not ensure that data is synced to disk. This is
+ faster and more near-realtime friendly than a hard commit.
+ -->
+
+ <autoSoftCommit>
+ <maxTime>1000</maxTime>
+ </autoSoftCommit>
+
+ <!-- Update Related Event Listeners
+
+ Various IndexWriter related events can trigger Listeners to
+ take actions.
+
+ postCommit - fired after every commit or optimize command
+ postOptimize - fired after every optimize command
+ -->
+
+ </updateHandler>
+
+ <!-- IndexReaderFactory
+
+ Use the following format to specify a custom IndexReaderFactory,
+ which allows for alternate IndexReader implementations.
+
+ ** Experimental Feature **
+
+ Please note - Using a custom IndexReaderFactory may prevent
+ certain other features from working. The API to
+ IndexReaderFactory may change without warning or may even be
+ removed from future releases if the problems cannot be
+ resolved.
+
+
+ ** Features that may not work with custom IndexReaderFactory **
+
+ The ReplicationHandler assumes a disk-resident index. Using a
+ custom IndexReader implementation may cause incompatibility
+ with ReplicationHandler and may cause replication to not work
+ correctly. See SOLR-1366 for details.
+
+ -->
+ <!--
+ <indexReaderFactory name="IndexReaderFactory" class="package.class">
+ <str name="someArg">Some Value</str>
+ </indexReaderFactory >
+ -->
+
+ <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Query section - these settings control query time things like caches
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
+ <query>
+
+ <!-- Maximum number of clauses in each BooleanQuery, an exception
+ is thrown if exceeded. It is safe to increase or remove this setting,
+ since it is purely an arbitrary limit to try and catch user errors where
+ large boolean queries may not be the best implementation choice.
+ -->
+ <maxBooleanClauses>${solr.max.booleanClauses:1024}</maxBooleanClauses>
+
+ <!-- Solr Internal Query Caches
+
+ There are two implementations of cache available for Solr,
+ LRUCache, based on a synchronized LinkedHashMap, and
+ FastLRUCache, based on a ConcurrentHashMap.
+
+ FastLRUCache has faster gets and slower puts in single
+ threaded operation and thus is generally faster than LRUCache
+ when the hit ratio of the cache is high (> 75%), and may be
+ faster under other scenarios on multi-cpu systems.
+ -->
+
+ <!-- Filter Cache
+
+ Cache used by SolrIndexSearcher for filters (DocSets),
+ unordered sets of *all* documents that match a query. When a
+ new searcher is opened, its caches may be prepopulated or
+ "autowarmed" using data from caches in the old searcher.
+ autowarmCount is the number of items to prepopulate. For
+ LRUCache, the autowarmed items will be the most recently
+ accessed items.
+
+ Parameters:
+ class - the SolrCache implementation LRUCache or
+ (LRUCache or FastLRUCache)
+ size - the maximum number of entries in the cache
+ initialSize - the initial capacity (number of entries) of
+ the cache. (see java.util.HashMap)
+ autowarmCount - the number of entries to prepopulate from
+ and old cache.
+ maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed
+ to occupy. Note that when this option is specified, the size
+ and initialSize parameters are ignored.
+ -->
+ <filterCache class="solr.FastLRUCache"
+ size="512"
+ initialSize="512"
+ autowarmCount="0"/>
+
+ <!-- Query Result Cache
+
+ Caches results of searches - ordered lists of document ids
+ (DocList) based on a query, a sort, and the range of documents requested.
+ Additional supported parameter by LRUCache:
+ maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed
+ to occupy
+ -->
+ <queryResultCache class="solr.LRUCache"
+ size="512"
+ initialSize="512"
+ autowarmCount="0"/>
+
+ <!-- Document Cache
+
+ Caches Lucene Document objects (the stored fields for each
+ document). Since Lucene internal document ids are transient,
+ this cache will not be autowarmed.
+ -->
+ <documentCache class="solr.LRUCache"
+ size="512"
+ initialSize="512"
+ autowarmCount="0"/>
+
+ <!-- custom cache currently used by block join -->
+ <cache name="perSegFilter"
+ class="solr.search.LRUCache"
+ size="10"
+ initialSize="0"
+ autowarmCount="10"
+ regenerator="solr.NoOpRegenerator" />
+
+ <!-- Field Value Cache
+
+ Cache used to hold field values that are quickly accessible
+ by document id. The fieldValueCache is created by default
+ even if not configured here.
+ -->
+ <!--
+ <fieldValueCache class="solr.FastLRUCache"
+ size="512"
+ autowarmCount="128"
+ showItems="32" />
+ -->
+
+ <!-- Custom Cache
+
+ Example of a generic cache. These caches may be accessed by
+ name through SolrIndexSearcher.getCache(),cacheLookup(), and
+ cacheInsert(). The purpose is to enable easy caching of
+ user/application level data. The regenerator argument should
+ be specified as an implementation of solr.CacheRegenerator
+ if autowarming is desired.
+ -->
+ <!--
+ <cache name="myUserCache"
+ class="solr.LRUCache"
+ size="4096"
+ initialSize="1024"
+ autowarmCount="1024"
+ regenerator="com.mycompany.MyRegenerator"
+ />
+ -->
+
+
+ <!-- Lazy Field Loading
+
+ If true, stored fields that are not requested will be loaded
+ lazily. This can result in a significant speed improvement
+ if the usual case is to not load all stored fields,
+ especially if the skipped fields are large compressed text
+ fields.
+ -->
+ <enableLazyFieldLoading>true</enableLazyFieldLoading>
+
+ <!-- Use Filter For Sorted Query
+
+ A possible optimization that attempts to use a filter to
+ satisfy a search. If the requested sort does not include
+ score, then the filterCache will be checked for a filter
+ matching the query. If found, the filter will be used as the
+ source of document ids, and then the sort will be applied to
+ that.
+
+ For most situations, this will not be useful unless you
+ frequently get the same search repeatedly with different sort
+ options, and none of them ever use "score"
+ -->
+ <!--
+ <useFilterForSortedQuery>true</useFilterForSortedQuery>
+ -->
+
+ <!-- Result Window Size
+
+ An optimization for use with the queryResultCache. When a search
+ is requested, a superset of the requested number of document ids
+ are collected. For example, if a search for a particular query
+ requests matching documents 10 through 19, and queryWindowSize is 50,
+ then documents 0 through 49 will be collected and cached. Any further
+ requests in that range can be satisfied via the cache.
+ -->
+ <queryResultWindowSize>20</queryResultWindowSize>
+
+ <!-- Maximum number of documents to cache for any entry in the
+ queryResultCache.
+ -->
+ <queryResultMaxDocsCached>200</queryResultMaxDocsCached>
+
+ <!-- Query Related Event Listeners
+
+ Various IndexSearcher related events can trigger Listeners to
+ take actions.
+
+ newSearcher - fired whenever a new searcher is being prepared
+ and there is a current searcher handling requests (aka
+ registered). It can be used to prime certain caches to
+ prevent long request times for certain requests.
+
+ firstSearcher - fired whenever a new searcher is being
+ prepared but there is no current registered searcher to handle
+ requests or to gain autowarming data from.
+
+
+ -->
+ <!-- QuerySenderListener takes an array of NamedList and executes a
+ local query request for each NamedList in sequence.
+ -->
+ <listener event="newSearcher" class="solr.QuerySenderListener">
+ <arr name="queries">
+ <!--
+ <lst><str name="q">solr</str><str name="sort">price asc</str></lst>
+ <lst><str name="q">rocks</str><str name="sort">weight asc</str></lst>
+ -->
+ </arr>
+ </listener>
+ <listener event="firstSearcher" class="solr.QuerySenderListener">
+ <arr name="queries">
+ <!--
+ <lst>
+ <str name="q">static firstSearcher warming in solrconfig.xml</str>
+ </lst>
+ -->
+ </arr>
+ </listener>
+
+ <!-- Use Cold Searcher
+
+ If a search request comes in and there is no current
+ registered searcher, then immediately register the still
+ warming searcher and use it. If "false" then all requests
+ will block until the first searcher is done warming.
+ -->
+ <useColdSearcher>false</useColdSearcher>
+
+ </query>
+
+
+ <!-- Request Dispatcher
+
+ This section contains instructions for how the SolrDispatchFilter
+ should behave when processing requests for this SolrCore.
+
+ -->
+ <requestDispatcher>
+ <!-- Request Parsing
+
+ These settings indicate how Solr Requests may be parsed, and
+ what restrictions may be placed on the ContentStreams from
+ those requests
+
+ enableRemoteStreaming - enables use of the stream.file
+ and stream.url parameters for specifying remote streams.
+
+ multipartUploadLimitInKB - specifies the max size (in KiB) of
+ Multipart File Uploads that Solr will allow in a Request.
+
+ formdataUploadLimitInKB - specifies the max size (in KiB) of
+ form data (application/x-www-form-urlencoded) sent via
+ POST. You can use POST to pass request parameters not
+ fitting into the URL.
+
+ addHttpRequestToContext - if set to true, it will instruct
+ the requestParsers to include the original HttpServletRequest
+ object in the context map of the SolrQueryRequest under the
+ key "httpRequest". It will not be used by any of the existing
+ Solr components, but may be useful when developing custom
+ plugins.
+
+ *** WARNING ***
+ Before enabling remote streaming, you should make sure your
+ system has authentication enabled.
+
+ <requestParsers enableRemoteStreaming="false"
+ multipartUploadLimitInKB="-1"
+ formdataUploadLimitInKB="-1"
+ addHttpRequestToContext="false"/>
+ -->
+
+ <!-- HTTP Caching
+
+ Set HTTP caching related parameters (for proxy caches and clients).
+
+ The options below instruct Solr not to output any HTTP Caching
+ related headers
+ -->
+ <httpCaching never304="true" />
+ <!-- If you include a <cacheControl> directive, it will be used to
+ generate a Cache-Control header (as well as an Expires header
+ if the value contains "max-age=")
+
+ By default, no Cache-Control header is generated.
+
+ You can use the <cacheControl> option even if you have set
+ never304="true"
+ -->
+ <!--
+ <httpCaching never304="true" >
+ <cacheControl>max-age=30, public</cacheControl>
+ </httpCaching>
+ -->
+ <!-- To enable Solr to respond with automatically generated HTTP
+ Caching headers, and to response to Cache Validation requests
+ correctly, set the value of never304="false"
+
+ This will cause Solr to generate Last-Modified and ETag
+ headers based on the properties of the Index.
+
+ The following options can also be specified to affect the
+ values of these headers...
+
+ lastModFrom - the default value is "openTime" which means the
+ Last-Modified value (and validation against If-Modified-Since
+ requests) will all be relative to when the current Searcher
+ was opened. You can change it to lastModFrom="dirLastMod" if
+ you want the value to exactly correspond to when the physical
+ index was last modified.
+
+ etagSeed="..." is an option you can change to force the ETag
+ header (and validation against If-None-Match requests) to be
+ different even if the index has not changed (ie: when making
+ significant changes to your config file)
+
+ (lastModifiedFrom and etagSeed are both ignored if you use
+ the never304="true" option)
+ -->
+ <!--
+ <httpCaching lastModifiedFrom="openTime"
+ etagSeed="Solr">
+ <cacheControl>max-age=30, public</cacheControl>
+ </httpCaching>
+ -->
+ </requestDispatcher>
+
+ <!-- Request Handlers
+
+ http://wiki.apache.org/solr/SolrRequestHandler
+
+ Incoming queries will be dispatched to a specific handler by name
+ based on the path specified in the request.
+
+ If a Request Handler is declared with startup="lazy", then it will
+ not be initialized until the first request that uses it.
+
+ -->
+ <!-- SearchHandler
+
+ http://wiki.apache.org/solr/SearchHandler
+
+ For processing Search Queries, the primary Request Handler
+ provided with Solr is "SearchHandler" It delegates to a sequent
+ of SearchComponents (see below) and supports distributed
+ queries across multiple shards
+ -->
+ <requestHandler name="/select" class="solr.SearchHandler">
+ <!-- default values for query parameters can be specified, these
+ will be overridden by parameters in the request
+ -->
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ <int name="rows">10</int>
+ <str name="df">text</str>
+ <!-- Default search field
+ <str name="df">text</str>
+ -->
+ <!-- Change from JSON to XML format (the default prior to Solr 7.0)
+ <str name="wt">xml</str>
+ -->
+ </lst>
+ <!-- In addition to defaults, "appends" params can be specified
+ to identify values which should be appended to the list of
+ multi-val params from the query (or the existing "defaults").
+ -->
+ <!-- In this example, the param "fq=instock:true" would be appended to
+ any query time fq params the user may specify, as a mechanism for
+ partitioning the index, independent of any user selected filtering
+ that may also be desired (perhaps as a result of faceted searching).
+
+ NOTE: there is *absolutely* nothing a client can do to prevent these
+ "appends" values from being used, so don't use this mechanism
+ unless you are sure you always want it.
+ -->
+ <!--
+ <lst name="appends">
+ <str name="fq">inStock:true</str>
+ </lst>
+ -->
+ <!-- "invariants" are a way of letting the Solr maintainer lock down
+ the options available to Solr clients. Any params values
+ specified here are used regardless of what values may be specified
+ in either the query, the "defaults", or the "appends" params.
+
+ In this example, the facet.field and facet.query params would
+ be fixed, limiting the facets clients can use. Faceting is
+ not turned on by default - but if the client does specify
+ facet=true in the request, these are the only facets they
+ will be able to see counts for; regardless of what other
+ facet.field or facet.query params they may specify.
+
+ NOTE: there is *absolutely* nothing a client can do to prevent these
+ "invariants" values from being used, so don't use this mechanism
+ unless you are sure you always want it.
+ -->
+ <!--
+ <lst name="invariants">
+ <str name="facet.field">cat</str>
+ <str name="facet.field">manu_exact</str>
+ <str name="facet.query">price:[* TO 500]</str>
+ <str name="facet.query">price:[500 TO *]</str>
+ </lst>
+ -->
+ <!-- If the default list of SearchComponents is not desired, that
+ list can either be overridden completely, or components can be
+ prepended or appended to the default list. (see below)
+ -->
+ <!--
+ <arr name="components">
+ <str>nameOfCustomComponent1</str>
+ <str>nameOfCustomComponent2</str>
+ </arr>
+ -->
+ </requestHandler>
+
+ <!-- A request handler that returns indented JSON by default -->
+ <requestHandler name="/query" class="solr.SearchHandler">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ <str name="wt">json</str>
+ <str name="indent">true</str>
+ </lst>
+ </requestHandler>
+
+
+ <!-- A Robust Example
+
+ This example SearchHandler declaration shows off usage of the
+ SearchHandler with many defaults declared
+
+ Note that multiple instances of the same Request Handler
+ (SearchHandler) can be registered multiple times with different
+ names (and different init parameters)
+ -->
+ <requestHandler name="/browse" class="solr.SearchHandler" useParams="query,facets,velocity,browse">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ </lst>
+ </requestHandler>
+
+ <initParams path="/update/**,/query,/select,/tvrh,/elevate,/spell,/browse">
+ <lst name="defaults">
+ <str name="df">_text_</str>
+ </lst>
+ </initParams>
+
+ <!-- Solr Cell Update Request Handler
+
+ http://wiki.apache.org/solr/ExtractingRequestHandler
+
+ -->
+ <requestHandler name="/update/extract"
+ startup="lazy"
+ class="solr.extraction.ExtractingRequestHandler" >
+ <lst name="defaults">
+ <str name="lowernames">true</str>
+ <str name="fmap.content">_text_</str>
+ </lst>
+ </requestHandler>
+
+ <!-- Search Components
+
+ Search components are registered to SolrCore and used by
+ instances of SearchHandler (which can access them by name)
+
+ By default, the following components are available:
+
+ <searchComponent name="query" class="solr.QueryComponent" />
+ <searchComponent name="facet" class="solr.FacetComponent" />
+ <searchComponent name="mlt" class="solr.MoreLikeThisComponent" />
+ <searchComponent name="highlight" class="solr.HighlightComponent" />
+ <searchComponent name="stats" class="solr.StatsComponent" />
+ <searchComponent name="debug" class="solr.DebugComponent" />
+
+ Default configuration in a requestHandler would look like:
+
+ <arr name="components">
+ <str>query</str>
+ <str>facet</str>
+ <str>mlt</str>
+ <str>highlight</str>
+ <str>stats</str>
+ <str>debug</str>
+ </arr>
+
+ If you register a searchComponent to one of the standard names,
+ that will be used instead of the default.
+
+ To insert components before or after the 'standard' components, use:
+
+ <arr name="first-components">
+ <str>myFirstComponentName</str>
+ </arr>
+
+ <arr name="last-components">
+ <str>myLastComponentName</str>
+ </arr>
+
+ NOTE: The component registered with the name "debug" will
+ always be executed after the "last-components"
+
+ -->
+
+ <!-- Spell Check
+
+ The spell check component can return a list of alternative spelling
+ suggestions.
+
+ http://wiki.apache.org/solr/SpellCheckComponent
+ -->
+ <!-- <searchComponent name="spellcheck" class="solr.SpellCheckComponent">
+
+ <str name="queryAnalyzerFieldType">text_general</str>
+
+ <!-- Multiple "Spell Checkers" can be declared and used by this
+ component
+ -->
+
+ <!-- a spellchecker built from a field of the main index -->
+ <lst name="spellchecker">
+ <str name="name">default</str>
+ <str name="field">_text_</str>
+ <str name="classname">solr.DirectSolrSpellChecker</str>
+ <!-- the spellcheck distance measure used, the default is the internal levenshtein -->
+ <str name="distanceMeasure">internal</str>
+ <!-- minimum accuracy needed to be considered a valid spellcheck suggestion -->
+ <float name="accuracy">0.5</float>
+ <!-- the maximum #edits we consider when enumerating terms: can be 1 or 2 -->
+ <int name="maxEdits">2</int>
+ <!-- the minimum shared prefix when enumerating terms -->
+ <int name="minPrefix">1</int>
+ <!-- maximum number of inspections per result. -->
+ <int name="maxInspections">5</int>
+ <!-- minimum length of a query term to be considered for correction -->
+ <int name="minQueryLength">4</int>
+ <!-- maximum threshold of documents a query term can appear to be considered for correction -->
+ <float name="maxQueryFrequency">0.01</float>
+ <!-- uncomment this to require suggestions to occur in 1% of the documents
+ <float name="thresholdTokenFrequency">.01</float>
+ -->
+ </lst>
+
+ <!-- a spellchecker that can break or combine words. See "/spell" handler below for usage -->
+ <!--
+ <lst name="spellchecker">
+ <str name="name">wordbreak</str>
+ <str name="classname">solr.WordBreakSolrSpellChecker</str>
+ <str name="field">name</str>
+ <str name="combineWords">true</str>
+ <str name="breakWords">true</str>
+ <int name="maxChanges">10</int>
+ </lst>
+ -->
+ </searchComponent>
+ -->
+
+ <!-- A request handler for demonstrating the spellcheck component.
+
+ NOTE: This is purely as an example. The whole purpose of the
+ SpellCheckComponent is to hook it into the request handler that
+ handles your normal user queries so that a separate request is
+ not needed to get suggestions.
+
+ IN OTHER WORDS, THERE IS REALLY GOOD CHANCE THE SETUP BELOW IS
+ NOT WHAT YOU WANT FOR YOUR PRODUCTION SYSTEM!
+
+ See http://wiki.apache.org/solr/SpellCheckComponent for details
+ on the request parameters.
+ -->
+ <requestHandler name="/spell" class="solr.SearchHandler" startup="lazy">
+ <lst name="defaults">
+ <!-- Solr will use suggestions from both the 'default' spellchecker
+ and from the 'wordbreak' spellchecker and combine them.
+ collations (re-written queries) can include a combination of
+ corrections from both spellcheckers -->
+ <str name="spellcheck.dictionary">default</str>
+ <str name="spellcheck">on</str>
+ <str name="spellcheck.extendedResults">true</str>
+ <str name="spellcheck.count">10</str>
+ <str name="spellcheck.alternativeTermCount">5</str>
+ <str name="spellcheck.maxResultsForSuggest">5</str>
+ <str name="spellcheck.collate">true</str>
+ <str name="spellcheck.collateExtendedResults">true</str>
+ <str name="spellcheck.maxCollationTries">10</str>
+ <str name="spellcheck.maxCollations">5</str>
+ </lst>
+ <arr name="last-components">
+ <str>spellcheck</str>
+ </arr>
+ </requestHandler>
+
+ <!-- Term Vector Component
+
+ http://wiki.apache.org/solr/TermVectorComponent
+ -->
+ <searchComponent name="tvComponent" class="solr.TermVectorComponent"/>
+
+ <!-- A request handler for demonstrating the term vector component
+
+ This is purely as an example.
+
+ In reality you will likely want to add the component to your
+ already specified request handlers.
+ -->
+ <requestHandler name="/tvrh" class="solr.SearchHandler" startup="lazy">
+ <lst name="defaults">
+ <bool name="tv">true</bool>
+ </lst>
+ <arr name="last-components">
+ <str>tvComponent</str>
+ </arr>
+ </requestHandler>
+
+ <!-- Clustering Component. (Omitted here. See the default Solr example for a typical configuration.) -->
+
+ <!-- Terms Component
+
+ http://wiki.apache.org/solr/TermsComponent
+
+ A component to return terms and document frequency of those
+ terms
+ -->
+ <searchComponent name="terms" class="solr.TermsComponent"/>
+
+ <!-- A request handler for demonstrating the terms component -->
+ <requestHandler name="/terms" class="solr.SearchHandler" startup="lazy">
+ <lst name="defaults">
+ <bool name="terms">true</bool>
+ <bool name="distrib">false</bool>
+ </lst>
+ <arr name="components">
+ <str>terms</str>
+ </arr>
+ </requestHandler>
+
+
+ <!-- Query Elevation Component
+
+ http://wiki.apache.org/solr/QueryElevationComponent
+
+ a search component that enables you to configure the top
+ results for a given query regardless of the normal lucene
+ scoring.
+ -->
+ <searchComponent name="elevator" class="solr.QueryElevationComponent" >
+ <!-- pick a fieldType to analyze queries -->
+ <str name="queryFieldType">string</str>
+ </searchComponent>
+
+ <!-- A request handler for demonstrating the elevator component -->
+ <requestHandler name="/elevate" class="solr.SearchHandler" startup="lazy">
+ <lst name="defaults">
+ <str name="echoParams">explicit</str>
+ </lst>
+ <arr name="last-components">
+ <str>elevator</str>
+ </arr>
+ </requestHandler>
+
+ <!-- Highlighting Component
+
+ http://wiki.apache.org/solr/HighlightingParameters
+ -->
+ <searchComponent class="solr.HighlightComponent" name="highlight">
+ <highlighting>
+ <!-- Configure the standard fragmenter -->
+ <!-- This could most likely be commented out in the "default" case -->
+ <fragmenter name="gap"
+ default="true"
+ class="solr.highlight.GapFragmenter">
+ <lst name="defaults">
+ <int name="hl.fragsize">100</int>
+ </lst>
+ </fragmenter>
+
+ <!-- A regular-expression-based fragmenter
+ (for sentence extraction)
+ -->
+ <fragmenter name="regex"
+ class="solr.highlight.RegexFragmenter">
+ <lst name="defaults">
+ <!-- slightly smaller fragsizes work better because of slop -->
+ <int name="hl.fragsize">70</int>
+ <!-- allow 50% slop on fragment sizes -->
+ <float name="hl.regex.slop">0.5</float>
+ <!-- a basic sentence pattern -->
+ <str name="hl.regex.pattern">[-\w ,/\n\&quot;&apos;]{20,200}</str>
+ </lst>
+ </fragmenter>
+
+ <!-- Configure the standard formatter -->
+ <formatter name="html"
+ default="true"
+ class="solr.highlight.HtmlFormatter">
+ <lst name="defaults">
+ <str name="hl.simple.pre"><![CDATA[<em>]]></str>
+ <str name="hl.simple.post"><![CDATA[</em>]]></str>
+ </lst>
+ </formatter>
+
+ <!-- Configure the standard encoder -->
+ <encoder name="html"
+ class="solr.highlight.HtmlEncoder" />
+
+ <!-- Configure the standard fragListBuilder -->
+ <fragListBuilder name="simple"
+ class="solr.highlight.SimpleFragListBuilder"/>
+
+ <!-- Configure the single fragListBuilder -->
+ <fragListBuilder name="single"
+ class="solr.highlight.SingleFragListBuilder"/>
+
+ <!-- Configure the weighted fragListBuilder -->
+ <fragListBuilder name="weighted"
+ default="true"
+ class="solr.highlight.WeightedFragListBuilder"/>
+
+ <!-- default tag FragmentsBuilder -->
+ <fragmentsBuilder name="default"
+ default="true"
+ class="solr.highlight.ScoreOrderFragmentsBuilder">
+ <!--
+ <lst name="defaults">
+ <str name="hl.multiValuedSeparatorChar">/</str>
+ </lst>
+ -->
+ </fragmentsBuilder>
+
+ <!-- multi-colored tag FragmentsBuilder -->
+ <fragmentsBuilder name="colored"
+ class="solr.highlight.ScoreOrderFragmentsBuilder">
+ <lst name="defaults">
+ <str name="hl.tag.pre"><![CDATA[
+ <b style="background:yellow">,<b style="background:lawgreen">,
+ <b style="background:aquamarine">,<b style="background:magenta">,
+ <b style="background:palegreen">,<b style="background:coral">,
+ <b style="background:wheat">,<b style="background:khaki">,
+ <b style="background:lime">,<b style="background:deepskyblue">]]></str>
+ <str name="hl.tag.post"><![CDATA[</b>]]></str>
+ </lst>
+ </fragmentsBuilder>
+
+ <boundaryScanner name="default"
+ default="true"
+ class="solr.highlight.SimpleBoundaryScanner">
+ <lst name="defaults">
+ <str name="hl.bs.maxScan">10</str>
+ <str name="hl.bs.chars">.,!? &#9;&#10;&#13;</str>
+ </lst>
+ </boundaryScanner>
+
+ <boundaryScanner name="breakIterator"
+ class="solr.highlight.BreakIteratorBoundaryScanner">
+ <lst name="defaults">
+ <!-- type should be one of CHARACTER, WORD(default), LINE and SENTENCE -->
+ <str name="hl.bs.type">WORD</str>
+ <!-- language and country are used when constructing Locale object. -->
+ <!-- And the Locale object will be used when getting instance of BreakIterator -->
+ <str name="hl.bs.language">en</str>
+ <str name="hl.bs.country">US</str>
+ </lst>
+ </boundaryScanner>
+ </highlighting>
+ </searchComponent>
+
+ <!-- Update Processors
+
+ Chains of Update Processor Factories for dealing with Update
+ Requests can be declared, and then used by name in Update
+ Request Processors
+
+ http://wiki.apache.org/solr/UpdateRequestProcessor
+
+ -->
+
+ <!-- Add unknown fields to the schema
+
+ Field type guessing update processors that will
+ attempt to parse string-typed field values as Booleans, Longs,
+ Doubles, or Dates, and then add schema fields with the guessed
+ field types. Text content will be indexed as "text_general" as
+ well as a copy to a plain string version in *_str.
+
+ These require that the schema is both managed and mutable, by
+ declaring schemaFactory as ManagedIndexSchemaFactory, with
+ mutable specified as true.
+
+ See http://wiki.apache.org/solr/GuessingFieldTypes
+ -->
+ <updateProcessor class="solr.UUIDUpdateProcessorFactory" name="uuid"/>
+ <updateProcessor class="solr.RemoveBlankFieldUpdateProcessorFactory" name="remove-blank"/>
+ <updateProcessor class="solr.FieldNameMutatingUpdateProcessorFactory" name="field-name-mutating">
+ <str name="pattern">[^\w-\.]</str>
+ <str name="replacement">_</str>
+ </updateProcessor>
+ <updateProcessor class="solr.ParseBooleanFieldUpdateProcessorFactory" name="parse-boolean"/>
+ <updateProcessor class="solr.ParseLongFieldUpdateProcessorFactory" name="parse-long"/>
+ <updateProcessor class="solr.ParseDoubleFieldUpdateProcessorFactory" name="parse-double"/>
+ <updateProcessor class="solr.ParseDateFieldUpdateProcessorFactory" name="parse-date">
+ <arr name="format">
+ <str>yyyy-MM-dd['T'[HH:mm[:ss[.SSS]][z</str>
+ <str>yyyy-MM-dd['T'[HH:mm[:ss[,SSS]][z</str>
+ <str>yyyy-MM-dd HH:mm[:ss[.SSS]][z</str>
+ <str>yyyy-MM-dd HH:mm[:ss[,SSS]][z</str>
+ <str>[EEE, ]dd MMM yyyy HH:mm[:ss] z</str>
+ <str>EEEE, dd-MMM-yy HH:mm:ss z</str>
+ <str>EEE MMM ppd HH:mm:ss [z ]yyyy</str>
+ </arr>
+ </updateProcessor>
+ <updateProcessor class="solr.AddSchemaFieldsUpdateProcessorFactory" name="add-schema-fields">
+ <lst name="typeMapping">
+ <str name="valueClass">java.lang.String</str>
+ <str name="fieldType">text_general</str>
+ <lst name="copyField">
+ <str name="dest">*_str</str>
+ <int name="maxChars">256</int>
+ </lst>
+ <!-- Use as default mapping instead of defaultFieldType -->
+ <bool name="default">true</bool>
+ </lst>
+ <lst name="typeMapping">
+ <str name="valueClass">java.lang.Boolean</str>
+ <str name="fieldType">booleans</str>
+ </lst>
+ <lst name="typeMapping">
+ <str name="valueClass">java.util.Date</str>
+ <str name="fieldType">pdates</str>
+ </lst>
+ <lst name="typeMapping">
+ <str name="valueClass">java.lang.Long</str>
+ <str name="valueClass">java.lang.Integer</str>
+ <str name="fieldType">plongs</str>
+ </lst>
+ <lst name="typeMapping">
+ <str name="valueClass">java.lang.Number</str>
+ <str name="fieldType">pdoubles</str>
+ </lst>
+ </updateProcessor>
+
+ <!-- The update.autoCreateFields property can be turned to false to disable schemaless mode -->
+ <updateRequestProcessorChain name="add-unknown-fields-to-the-schema" default="${update.autoCreateFields:true}"
+ processor="uuid,remove-blank,field-name-mutating,parse-boolean,parse-long,parse-double,parse-date,add-schema-fields">
+ <processor class="solr.LogUpdateProcessorFactory"/>
+ <processor class="solr.DistributedUpdateProcessorFactory"/>
+ <processor class="solr.RunUpdateProcessorFactory"/>
+ </updateRequestProcessorChain>
+
+ <!-- Deduplication
+
+ An example dedup update processor that creates the "id" field
+ on the fly based on the hash code of some other fields. This
+ example has overwriteDupes set to false since we are using the
+ id field as the signatureField and Solr will maintain
+ uniqueness based on that anyway.
+
+ -->
+ <!--
+ <updateRequestProcessorChain name="dedupe">
+ <processor class="solr.processor.SignatureUpdateProcessorFactory">
+ <bool name="enabled">true</bool>
+ <str name="signatureField">id</str>
+ <bool name="overwriteDupes">false</bool>
+ <str name="fields">name,features,cat</str>
+ <str name="signatureClass">solr.processor.Lookup3Signature</str>
+ </processor>
+ <processor class="solr.LogUpdateProcessorFactory" />
+ <processor class="solr.RunUpdateProcessorFactory" />
+ </updateRequestProcessorChain>
+ -->
+
+ <!-- Language identification
+
+ This example update chain identifies the language of the incoming
+ documents using the langid contrib. The detected language is
+ written to field language_s. No field name mapping is done.
+ The fields used for detection are text, title, subject and description,
+ making this example suitable for detecting languages form full-text
+ rich documents injected via ExtractingRequestHandler.
+ See more about langId at http://wiki.apache.org/solr/LanguageDetection
+ -->
+ <!--
+ <updateRequestProcessorChain name="langid">
+ <processor class="org.apache.solr.update.processor.TikaLanguageIdentifierUpdateProcessorFactory">
+ <str name="langid.fl">text,title,subject,description</str>
+ <str name="langid.langField">language_s</str>
+ <str name="langid.fallback">en</str>
+ </processor>
+ <processor class="solr.LogUpdateProcessorFactory" />
+ <processor class="solr.RunUpdateProcessorFactory" />
+ </updateRequestProcessorChain>
+ -->
+
+ <!-- Script update processor
+
+ This example hooks in an update processor implemented using JavaScript.
+
+ See more about the script update processor at http://wiki.apache.org/solr/ScriptUpdateProcessor
+ -->
+ <!--
+ <updateRequestProcessorChain name="script">
+ <processor class="solr.StatelessScriptUpdateProcessorFactory">
+ <str name="script">update-script.js</str>
+ <lst name="params">
+ <str name="config_param">example config parameter</str>
+ </lst>
+ </processor>
+ <processor class="solr.RunUpdateProcessorFactory" />
+ </updateRequestProcessorChain>
+ -->
+
+ <!-- Response Writers
+
+ http://wiki.apache.org/solr/QueryResponseWriter
+
+ Request responses will be written using the writer specified by
+ the 'wt' request parameter matching the name of a registered
+ writer.
+
+ The "default" writer is the default and will be used if 'wt' is
+ not specified in the request.
+ -->
+ <!-- The following response writers are implicitly configured unless
+ overridden...
+ -->
+ <!--
+ <queryResponseWriter name="xml"
+ default="true"
+ class="solr.XMLResponseWriter" />
+ <queryResponseWriter name="json" class="solr.JSONResponseWriter"/>
+ <queryResponseWriter name="python" class="solr.PythonResponseWriter"/>
+ <queryResponseWriter name="ruby" class="solr.RubyResponseWriter"/>
+ <queryResponseWriter name="php" class="solr.PHPResponseWriter"/>
+ <queryResponseWriter name="phps" class="solr.PHPSerializedResponseWriter"/>
+ <queryResponseWriter name="csv" class="solr.CSVResponseWriter"/>
+ <queryResponseWriter name="schema.xml" class="solr.SchemaXmlResponseWriter"/>
+ -->
+
+ <queryResponseWriter name="json" class="solr.JSONResponseWriter">
+ <!-- For the purposes of the tutorial, JSON responses are written as
+ plain text so that they are easy to read in *any* browser.
+ If you expect a MIME type of "application/json" just remove this override.
+ -->
+ <str name="content-type">text/plain; charset=UTF-8</str>
+ </queryResponseWriter>
+
+ <!--
+ Custom response writers can be declared as needed...
+ -->
+ <queryResponseWriter name="velocity" class="solr.VelocityResponseWriter" startup="lazy">
+ <str name="template.base.dir">${velocity.template.base.dir:}</str>
+ <str name="solr.resource.loader.enabled">${velocity.solr.resource.loader.enabled:true}</str>
+ <str name="params.resource.loader.enabled">${velocity.params.resource.loader.enabled:false}</str>
+ </queryResponseWriter>
+
+ <!-- XSLT response writer transforms the XML output by any xslt file found
+ in Solr's conf/xslt directory. Changes to xslt files are checked for
+ every xsltCacheLifetimeSeconds.
+ -->
+ <queryResponseWriter name="xslt" class="solr.XSLTResponseWriter">
+ <int name="xsltCacheLifetimeSeconds">5</int>
+ </queryResponseWriter>
+
+ <!-- Query Parsers
+
+ https://lucene.apache.org/solr/guide/query-syntax-and-parsing.html
+
+ Multiple QParserPlugins can be registered by name, and then
+ used in either the "defType" param for the QueryComponent (used
+ by SearchHandler) or in LocalParams
+ -->
+ <!-- example of registering a query parser -->
+ <!--
+ <queryParser name="myparser" class="com.mycompany.MyQParserPlugin"/>
+ -->
+
+ <!-- Function Parsers
+
+ http://wiki.apache.org/solr/FunctionQuery
+
+ Multiple ValueSourceParsers can be registered by name, and then
+ used as function names when using the "func" QParser.
+ -->
+ <!-- example of registering a custom function parser -->
+ <!--
+ <valueSourceParser name="myfunc"
+ class="com.mycompany.MyValueSourceParser" />
+ -->
+
+
+ <!-- Document Transformers
+ http://wiki.apache.org/solr/DocTransformers
+ -->
+ <!--
+ Could be something like:
+ <transformer name="db" class="com.mycompany.LoadFromDatabaseTransformer" >
+ <int name="connection">jdbc://....</int>
+ </transformer>
+
+ To add a constant value to all docs, use:
+ <transformer name="mytrans2" class="org.apache.solr.response.transform.ValueAugmenterFactory" >
+ <int name="value">5</int>
+ </transformer>
+
+ If you want the user to still be able to change it with _value:something_ use this:
+ <transformer name="mytrans3" class="org.apache.solr.response.transform.ValueAugmenterFactory" >
+ <double name="defaultValue">5</double>
+ </transformer>
+
+ If you are using the QueryElevationComponent, you may wish to mark documents that get boosted. The
+ EditorialMarkerFactory will do exactly that:
+ <transformer name="qecBooster" class="org.apache.solr.response.transform.EditorialMarkerFactory" />
+ -->
+</config>
diff --git a/solr/conf/stopwords.txt b/solr/conf/stopwords.txt
new file mode 100644
index 000000000..ae1e83eeb
--- /dev/null
+++ b/solr/conf/stopwords.txt
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/solr/conf/synonyms.txt b/solr/conf/synonyms.txt
new file mode 100644
index 000000000..eab4ee875
--- /dev/null
+++ b/solr/conf/synonyms.txt
@@ -0,0 +1,29 @@
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#-----------------------------------------------------------------------
+#some test synonym mappings unlikely to appear in real input text
+aaafoo => aaabar
+bbbfoo => bbbfoo bbbbar
+cccfoo => cccbar cccbaz
+fooaaa,baraaa,bazaaa
+
+# Some synonym groups specific to this example
+GB,gib,gigabyte,gigabytes
+MB,mib,megabyte,megabytes
+Television, Televisions, TV, TVs
+#notice we use "gib" instead of "GiB" so any WordDelimiterGraphFilter coming
+#after us won't split it into two words.
+
+# Synonym mappings can be used for spelling correction too
+pixima => pixma
+
diff --git a/src/Utils.ts b/src/Utils.ts
index d4b6f5377..611c61135 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -39,6 +39,19 @@ export class Utils {
document.body.removeChild(textArea);
}
+ public static GetClipboardText(): string {
+ var textArea = document.createElement("textarea");
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try { document.execCommand('paste'); } catch (err) { }
+
+ const val = textArea.value;
+ document.body.removeChild(textArea);
+ return val;
+ }
+
public static loggingEnabled: Boolean = false;
public static logFilter: number | undefined = undefined;
private static log(prefix: string, messageName: string, message: any, receiving: boolean) {
@@ -128,6 +141,7 @@ export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
if (!predicate || predicate(entry)) {
deepCopy.set(entry[0], entry[1]);
}
+ next = entries.next();
}
return deepCopy;
} \ No newline at end of file
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index a288d394a..cbcf751ee 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,15 +1,22 @@
import * as OpenSocket from 'socket.io-client';
import { MessageStore } from "./../server/Message";
import { Opt } from '../new_fields/Doc';
-import { Utils } from '../Utils';
+import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
-import { RefField, HandleUpdate, Id } from '../new_fields/RefField';
+import { RefField } from '../new_fields/RefField';
+import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
export namespace DocServer {
const _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
const GUID: string = Utils.GenerateGuid();
+ export function makeReadOnly() {
+ _CreateField = emptyFunction;
+ _UpdateField = emptyFunction;
+ _respondToUpdate = emptyFunction;
+ }
+
export function prepend(extension: string): string {
return window.location.origin + extension;
}
@@ -21,12 +28,10 @@ export namespace DocServer {
export async function GetRefField(id: string): Promise<Opt<RefField>> {
let cached = _cache[id];
if (cached === undefined) {
- const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(fieldJson => {
+ const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(async fieldJson => {
const field = SerializationHelper.Deserialize(fieldJson);
- if (_cache[id] !== undefined && !(_cache[id] instanceof Promise)) {
- id;
- }
if (field !== undefined) {
+ await field.proto;
_cache[id] = field;
} else {
delete _cache[id];
@@ -65,6 +70,7 @@ export namespace DocServer {
fieldMap[field.id] = SerializationHelper.Deserialize(field);
}
}
+
return fieldMap;
});
requestedIds.forEach(id => _cache[id] = prom.then(fields => fields[id]));
@@ -78,26 +84,40 @@ export namespace DocServer {
}
map[id] = field;
});
+ await Promise.all(requestedIds.map(async id => {
+ const field = fields[id];
+ if (field) {
+ await (field as any).proto;
+ }
+ }));
const otherFields = await Promise.all(promises);
waitingIds.forEach((id, index) => map[id] = otherFields[index]);
return map;
}
- export function UpdateField(id: string, diff: any) {
+ let _UpdateField = (id: string, diff: any) => {
if (id === updatingId) {
return;
}
Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ };
+
+ export function UpdateField(id: string, diff: any) {
+ _UpdateField(id, diff);
}
- export function CreateField(field: RefField) {
+ let _CreateField = (field: RefField) => {
_cache[field[Id]] = field;
const initialState = SerializationHelper.Serialize(field);
Utils.Emit(_socket, MessageStore.CreateField, initialState);
+ };
+
+ export function CreateField(field: RefField) {
+ _CreateField(field);
}
let updatingId: string | undefined;
- function respondToUpdate(diff: any) {
+ let _respondToUpdate = (diff: any) => {
const id = diff.id;
if (id === undefined) {
return;
@@ -119,6 +139,9 @@ export namespace DocServer {
} else {
update(field);
}
+ };
+ function respondToUpdate(diff: any) {
+ _respondToUpdate(diff);
}
function connected() {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 8706359e4..ab61b915c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -19,7 +19,6 @@ import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel
import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel";
import { AggregateFunction } from "../northstar/model/idea/idea";
import { Template } from "../views/Templates";
-import { TemplateField } from "../../fields/TemplateField";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { Field, Doc, Opt } from "../../new_fields/Doc";
@@ -27,12 +26,17 @@ import { OmitKeys } from "../../Utils";
import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
-import { Cast } from "../../new_fields/Types";
+import { Cast, NumCast } from "../../new_fields/Types";
import { IconField } from "../../new_fields/IconField";
import { listSpec } from "../../new_fields/Schema";
import { DocServer } from "../DocServer";
import { StrokeData, InkField } from "../../new_fields/InkField";
import { dropActionType } from "../util/DragManager";
+import { DateField } from "../../new_fields/DateField";
+import { UndoManager } from "../util/UndoManager";
+import { RouteStore } from "../../server/RouteStore";
+var requestImageSize = require('request-image-size');
+var path = require('path');
export interface DocumentOptions {
x?: number;
@@ -59,10 +63,45 @@ export interface DocumentOptions {
borderRounding?: number;
schemaColumns?: List<string>;
dockingConfig?: string;
+ dbDoc?: Doc;
// [key: string]: Opt<Field>;
}
const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
+export namespace DocUtils {
+ export function MakeLink(source: Doc, target: Doc) {
+ let protoSrc = source.proto ? source.proto : source;
+ let protoTarg = target.proto ? target.proto : target;
+ UndoManager.RunInBatch(() => {
+ let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
+ //let linkDoc = new Doc;
+ linkDoc.proto!.title = "-link name-";
+ linkDoc.proto!.linkDescription = "";
+ linkDoc.proto!.linkTags = "Default";
+
+ linkDoc.proto!.linkedTo = target;
+ linkDoc.proto!.linkedToPage = target.curPage;
+ linkDoc.proto!.linkedFrom = source;
+ linkDoc.proto!.linkedFromPage = source.curPage;
+
+ let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
+ if (!linkedFrom) {
+ protoTarg.linkedFromDocs = linkedFrom = new List<Doc>();
+ }
+ linkedFrom.push(linkDoc);
+
+ let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
+ if (!linkedTo) {
+ protoSrc.linkedToDocs = linkedTo = new List<Doc>();
+ }
+ linkedTo.push(linkDoc);
+ return linkDoc;
+ }, "make link");
+ }
+
+
+}
+
export namespace Docs {
let textProto: Doc;
let histoProto: Doc;
@@ -108,8 +147,8 @@ export namespace Docs {
deleg.data = value;
return Doc.assign(deleg, options);
}
- function SetDelegateOptions<U extends Field>(doc: Doc, options: DocumentOptions) {
- const deleg = Doc.MakeDelegate(doc);
+ function SetDelegateOptions(doc: Doc, options: DocumentOptions, id?: string) {
+ const deleg = Doc.MakeDelegate(doc, id);
return Doc.assign(deleg, options);
}
@@ -131,7 +170,7 @@ export namespace Docs {
}
function CreateTextPrototype(): Doc {
let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150 });
+ { x: 0, y: 0, width: 300, height: 150, backgroundColor: "#f1efeb" });
return textProto;
}
function CreatePdfPrototype(): Doc {
@@ -166,13 +205,32 @@ export namespace Docs {
return audioProto;
}
- function CreateInstance(proto: Doc, data: Field, options: DocumentOptions) {
+ function CreateInstance(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) {
const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
- return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps);
+ if (!("author" in protoProps)) {
+ protoProps.author = CurrentUserUtils.email;
+ }
+ if (!("creationDate" in protoProps)) {
+ protoProps.creationDate = new DateField;
+ }
+ protoProps.isPrototype = true;
+
+ return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps, delegId);
}
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(imageProto, new ImageField(new URL(url)), options);
+ let inst = CreateInstance(imageProto, new ImageField(new URL(url)), { title: path.basename(url), ...options });
+ requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
+ .then((size: any) => {
+ let aspect = size.height / size.width;
+ if (!inst.proto!.nativeWidth) {
+ inst.proto!.nativeWidth = size.width;
+ }
+ inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
+ inst.proto!.height = NumCast(inst.proto!.width) * aspect;
+ })
+ .catch((err: any) => console.log(err));
+ return inst;
// let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
// [new URL(url), ImageField]);
// doc.SetText(KeyStore.Caption, "my caption...");
@@ -199,16 +257,18 @@ export namespace Docs {
export function PdfDocument(url: string, options: DocumentOptions = {}) {
return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
}
- export async function DBDocument(url: string, options: DocumentOptions = {}) {
+
+ export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) {
let schemaName = options.title ? options.title : "-no schema-";
let ctlog = await Gateway.Instance.GetSchema(url, schemaName);
if (ctlog && ctlog.schemas) {
let schema = ctlog.schemas[0];
let schemaDoc = Docs.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
- let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc));
+ let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
if (!schemaDocuments) {
return;
}
+ CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc);
const docs = schemaDocuments;
CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
@@ -220,7 +280,7 @@ export namespace Docs {
new AttributeTransformationModel(atmod, AggregateFunction.None),
new AttributeTransformationModel(atmod, AggregateFunction.Count),
new AttributeTransformationModel(atmod, AggregateFunction.Count));
- docs.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
+ docs.push(Docs.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! }));
}
}));
});
@@ -235,7 +295,7 @@ export namespace Docs {
return CreateInstance(webProto, new HtmlField(html), options);
}
export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
- return CreateInstance(kvpProto, document, options);
+ return CreateInstance(kvpProto, document, { title: document.title + ".kvp", ...options });
}
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) {
if (!makePrototype) {
@@ -243,14 +303,17 @@ export namespace Docs {
}
return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
}
- export function SchemaDocument(documents: Array<Doc>, options: DocumentOptions) {
- return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Schema });
+ export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) {
+ return CreateInstance(collProto, new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
}
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
}
- export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions) {
- return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config });
+ export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
+ }
+ export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
+ return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
export function CaptionDocument(doc: Doc) {
diff --git a/src/client/goldenLayout.d.ts b/src/client/goldenLayout.d.ts
new file mode 100644
index 000000000..b50240563
--- /dev/null
+++ b/src/client/goldenLayout.d.ts
@@ -0,0 +1,3 @@
+
+declare const GoldenLayout: any;
+export = GoldenLayout; \ No newline at end of file
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
new file mode 100644
index 000000000..54c9c6068
--- /dev/null
+++ b/src/client/goldenLayout.js
@@ -0,0 +1,5360 @@
+(function ($) {
+ var lm = { "config": {}, "container": {}, "controls": {}, "errors": {}, "items": {}, "utils": {} };
+ lm.utils.F = function () {
+ };
+
+ lm.utils.extend = function (subClass, superClass) {
+ subClass.prototype = lm.utils.createObject(superClass.prototype);
+ subClass.prototype.contructor = subClass;
+ };
+
+ lm.utils.createObject = function (prototype) {
+ if (typeof Object.create === 'function') {
+ return Object.create(prototype);
+ } else {
+ lm.utils.F.prototype = prototype;
+ return new lm.utils.F();
+ }
+ };
+
+ lm.utils.objectKeys = function (object) {
+ var keys, key;
+
+ if (typeof Object.keys === 'function') {
+ return Object.keys(object);
+ } else {
+ keys = [];
+ for (key in object) {
+ keys.push(key);
+ }
+ return keys;
+ }
+ };
+
+ lm.utils.getHashValue = function (key) {
+ var matches = location.hash.match(new RegExp(key + '=([^&]*)'));
+ return matches ? matches[1] : null;
+ };
+
+ lm.utils.getQueryStringParam = function (param) {
+ if (window.location.hash) {
+ return lm.utils.getHashValue(param);
+ } else if (!window.location.search) {
+ return null;
+ }
+
+ var keyValuePairs = window.location.search.substr(1).split('&'),
+ params = {},
+ pair,
+ i;
+
+ for (i = 0; i < keyValuePairs.length; i++) {
+ pair = keyValuePairs[i].split('=');
+ params[pair[0]] = pair[1];
+ }
+
+ return params[param] || null;
+ };
+
+ lm.utils.copy = function (target, source) {
+ for (var key in source) {
+ target[key] = source[key];
+ }
+ return target;
+ };
+
+ /**
+ * This is based on Paul Irish's shim, but looks quite odd in comparison. Why?
+ * Because
+ * a) it shouldn't affect the global requestAnimationFrame function
+ * b) it shouldn't pass on the time that has passed
+ *
+ * @param {Function} fn
+ *
+ * @returns {void}
+ */
+ lm.utils.animFrame = function (fn) {
+ return (window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ function (callback) {
+ window.setTimeout(callback, 1000 / 60);
+ })(function () {
+ fn();
+ });
+ };
+
+ lm.utils.indexOf = function (needle, haystack) {
+ if (!(haystack instanceof Array)) {
+ throw new Error('Haystack is not an Array');
+ }
+
+ if (haystack.indexOf) {
+ return haystack.indexOf(needle);
+ } else {
+ for (var i = 0; i < haystack.length; i++) {
+ if (haystack[i] === needle) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ };
+
+ if (typeof /./ != 'function' && typeof Int8Array != 'object') {
+ lm.utils.isFunction = function (obj) {
+ return typeof obj == 'function' || false;
+ };
+ } else {
+ lm.utils.isFunction = function (obj) {
+ return toString.call(obj) === '[object Function]';
+ };
+ }
+
+ lm.utils.fnBind = function (fn, context, boundArgs) {
+
+ if (Function.prototype.bind !== undefined) {
+ return Function.prototype.bind.apply(fn, [context].concat(boundArgs || []));
+ }
+
+ var bound = function () {
+
+ // Join the already applied arguments to the now called ones (after converting to an array again).
+ var args = (boundArgs || []).concat(Array.prototype.slice.call(arguments, 0));
+
+ // If not being called as a constructor
+ if (!(this instanceof bound)) {
+ // return the result of the function called bound to target and partially applied.
+ return fn.apply(context, args);
+ }
+ // If being called as a constructor, apply the function bound to self.
+ fn.apply(this, args);
+ };
+ // Attach the prototype of the function to our newly created function.
+ bound.prototype = fn.prototype;
+ return bound;
+ };
+
+ lm.utils.removeFromArray = function (item, array) {
+ var index = lm.utils.indexOf(item, array);
+
+ if (index === -1) {
+ throw new Error('Can\'t remove item from array. Item is not in the array');
+ }
+
+ array.splice(index, 1);
+ };
+
+ lm.utils.now = function () {
+ if (typeof Date.now === 'function') {
+ return Date.now();
+ } else {
+ return (new Date()).getTime();
+ }
+ };
+
+ lm.utils.getUniqueId = function () {
+ return (Math.random() * 1000000000000000)
+ .toString(36)
+ .replace('.', '');
+ };
+
+ /**
+ * A basic XSS filter. It is ultimately up to the
+ * implementing developer to make sure their particular
+ * applications and usecases are save from cross site scripting attacks
+ *
+ * @param {String} input
+ * @param {Boolean} keepTags
+ *
+ * @returns {String} filtered input
+ */
+ lm.utils.filterXss = function (input, keepTags) {
+
+ var output = input
+ .replace(/javascript/gi, 'j&#97;vascript')
+ .replace(/expression/gi, 'expr&#101;ssion')
+ .replace(/onload/gi, 'onlo&#97;d')
+ .replace(/script/gi, '&#115;cript')
+ .replace(/onerror/gi, 'on&#101;rror');
+
+ if (keepTags === true) {
+ return output;
+ } else {
+ return output
+ .replace(/>/g, '&gt;')
+ .replace(/</g, '&lt;');
+ }
+ };
+
+ /**
+ * Removes html tags from a string
+ *
+ * @param {String} input
+ *
+ * @returns {String} input without tags
+ */
+ lm.utils.stripTags = function (input) {
+ return $.trim(input.replace(/(<([^>]+)>)/ig, ''));
+ };
+ /**
+ * A generic and very fast EventEmitter
+ * implementation. On top of emitting the
+ * actual event it emits an
+ *
+ * lm.utils.EventEmitter.ALL_EVENT
+ *
+ * event for every event triggered. This allows
+ * to hook into it and proxy events forwards
+ *
+ * @constructor
+ */
+ lm.utils.EventEmitter = function () {
+ this._mSubscriptions = {};
+ this._mSubscriptions[lm.utils.EventEmitter.ALL_EVENT] = [];
+
+ /**
+ * Listen for events
+ *
+ * @param {String} sEvent The name of the event to listen to
+ * @param {Function} fCallback The callback to execute when the event occurs
+ * @param {[Object]} oContext The value of the this pointer within the callback function
+ *
+ * @returns {void}
+ */
+ this.on = function (sEvent, fCallback, oContext) {
+ if (!lm.utils.isFunction(fCallback)) {
+ throw new Error('Tried to listen to event ' + sEvent + ' with non-function callback ' + fCallback);
+ }
+
+ if (!this._mSubscriptions[sEvent]) {
+ this._mSubscriptions[sEvent] = [];
+ }
+
+ this._mSubscriptions[sEvent].push({ fn: fCallback, ctx: oContext });
+ };
+
+ /**
+ * Emit an event and notify listeners
+ *
+ * @param {String} sEvent The name of the event
+ * @param {Mixed} various additional arguments that will be passed to the listener
+ *
+ * @returns {void}
+ */
+ this.emit = function (sEvent) {
+ var i, ctx, args;
+
+ args = Array.prototype.slice.call(arguments, 1);
+
+ var subs = this._mSubscriptions[sEvent];
+
+ if (subs) {
+ subs = subs.slice();
+ for (i = 0; i < subs.length; i++) {
+ ctx = subs[i].ctx || {};
+ subs[i].fn.apply(ctx, args);
+ }
+ }
+
+ args.unshift(sEvent);
+
+ var allEventSubs = this._mSubscriptions[lm.utils.EventEmitter.ALL_EVENT].slice()
+
+ for (i = 0; i < allEventSubs.length; i++) {
+ ctx = allEventSubs[i].ctx || {};
+ allEventSubs[i].fn.apply(ctx, args);
+ }
+ };
+
+ /**
+ * Removes a listener for an event, or all listeners if no callback and context is provided.
+ *
+ * @param {String} sEvent The name of the event
+ * @param {Function} fCallback The previously registered callback method (optional)
+ * @param {Object} oContext The previously registered context (optional)
+ *
+ * @returns {void}
+ */
+ this.unbind = function (sEvent, fCallback, oContext) {
+ if (!this._mSubscriptions[sEvent]) {
+ throw new Error('No subscribtions to unsubscribe for event ' + sEvent);
+ }
+
+ var i, bUnbound = false;
+
+ for (i = 0; i < this._mSubscriptions[sEvent].length; i++) {
+ if
+ (
+ (!fCallback || this._mSubscriptions[sEvent][i].fn === fCallback) &&
+ (!oContext || oContext === this._mSubscriptions[sEvent][i].ctx)
+ ) {
+ this._mSubscriptions[sEvent].splice(i, 1);
+ bUnbound = true;
+ }
+ }
+
+ if (bUnbound === false) {
+ throw new Error('Nothing to unbind for ' + sEvent);
+ }
+ };
+
+ /**
+ * Alias for unbind
+ */
+ this.off = this.unbind;
+
+ /**
+ * Alias for emit
+ */
+ this.trigger = this.emit;
+ };
+
+ /**
+ * The name of the event that's triggered for every other event
+ *
+ * usage
+ *
+ * myEmitter.on( lm.utils.EventEmitter.ALL_EVENT, function( eventName, argsArray ){
+ * //do stuff
+ * });
+ *
+ * @type {String}
+ */
+ lm.utils.EventEmitter.ALL_EVENT = '__all';
+ lm.utils.DragListener = function (eElement, nButtonCode) {
+ lm.utils.EventEmitter.call(this);
+
+ this._eElement = $(eElement);
+ this._oDocument = $(document);
+ this._eBody = $(document.body);
+ this._nButtonCode = nButtonCode || 0;
+
+ /**
+ * The delay after which to start the drag in milliseconds
+ */
+ this._nDelay = 200;
+
+ /**
+ * The distance the mouse needs to be moved to qualify as a drag
+ */
+ this._nDistance = 10;//TODO - works better with delay only
+
+ this._nX = 0;
+ this._nY = 0;
+
+ this._nOriginalX = 0;
+ this._nOriginalY = 0;
+
+ this._bDragging = false;
+
+ this._fMove = lm.utils.fnBind(this.onMouseMove, this);
+ this._fUp = lm.utils.fnBind(this.onMouseUp, this);
+ this._fDown = lm.utils.fnBind(this.onMouseDown, this);
+
+
+ this._eElement.on('mousedown touchstart', this._fDown);
+ };
+
+ lm.utils.DragListener.timeout = null;
+
+ lm.utils.copy(lm.utils.DragListener.prototype, {
+ destroy: function () {
+ this._eElement.unbind('mousedown touchstart', this._fDown);
+ this._oDocument.unbind('mouseup touchend', this._fUp);
+ this._eElement = null;
+ this._oDocument = null;
+ this._eBody = null;
+ },
+
+ onMouseDown: function (oEvent) {
+ oEvent.preventDefault();
+
+ if (oEvent.button == 0 || oEvent.type === "touchstart") {
+ var coordinates = this._getCoordinates(oEvent);
+
+ this._nOriginalX = coordinates.x;
+ this._nOriginalY = coordinates.y;
+
+ this._oDocument.on('mousemove touchmove', this._fMove);
+ this._oDocument.one('mouseup touchend', this._fUp);
+
+ this._timeout = setTimeout(lm.utils.fnBind(this._startDrag, this), this._nDelay);
+ }
+ },
+
+ onMouseMove: function (oEvent) {
+ if (this._timeout != null) {
+ oEvent.preventDefault();
+
+ var coordinates = this._getCoordinates(oEvent);
+
+ this._nX = coordinates.x - this._nOriginalX;
+ this._nY = coordinates.y - this._nOriginalY;
+
+ if (this._bDragging === false) {
+ if (
+ Math.abs(this._nX) > this._nDistance ||
+ Math.abs(this._nY) > this._nDistance
+ ) {
+ clearTimeout(this._timeout);
+ this._startDrag();
+ }
+ }
+
+ if (this._bDragging) {
+ this.emit('drag', this._nX, this._nY, oEvent);
+ }
+ }
+ },
+
+ onMouseUp: function (oEvent) {
+ if (this._timeout != null) {
+ clearTimeout(this._timeout);
+ this._eBody.removeClass('lm_dragging');
+ this._eElement.removeClass('lm_dragging');
+ this._oDocument.find('iframe').css('pointer-events', '');
+ this._oDocument.unbind('mousemove touchmove', this._fMove);
+ this._oDocument.unbind('mouseup touchend', this._fUp);
+
+ if (this._bDragging === true) {
+ this._bDragging = false;
+ this.emit('dragStop', oEvent, this._nOriginalX + this._nX);
+ }
+ }
+ },
+
+ _startDrag: function () {
+ this._bDragging = true;
+ this._eBody.addClass('lm_dragging');
+ this._eElement.addClass('lm_dragging');
+ this._oDocument.find('iframe').css('pointer-events', 'none');
+ this.emit('dragStart', this._nOriginalX, this._nOriginalY);
+ },
+
+ _getCoordinates: function (event) {
+ event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[0] : event;
+ return {
+ x: event.pageX,
+ y: event.pageY
+ };
+ }
+ });
+ /**
+ * The main class that will be exposed as GoldenLayout.
+ *
+ * @public
+ * @constructor
+ * @param {GoldenLayout config} config
+ * @param {[DOM element container]} container Can be a jQuery selector string or a Dom element. Defaults to body
+ *
+ * @returns {VOID}
+ */
+ lm.LayoutManager = function (config, container) {
+
+ if (!$ || typeof $.noConflict !== 'function') {
+ var errorMsg = 'jQuery is missing as dependency for GoldenLayout. ';
+ errorMsg += 'Please either expose $ on GoldenLayout\'s scope (e.g. window) or add "jquery" to ';
+ errorMsg += 'your paths when using RequireJS/AMD';
+ throw new Error(errorMsg);
+ }
+ lm.utils.EventEmitter.call(this);
+
+ this.isInitialised = false;
+ this._isFullPage = false;
+ this._resizeTimeoutId = null;
+ this._components = { 'lm-react-component': lm.utils.ReactComponentHandler };
+ this._itemAreas = [];
+ this._resizeFunction = lm.utils.fnBind(this._onResize, this);
+ this._unloadFunction = lm.utils.fnBind(this._onUnload, this);
+ this._maximisedItem = null;
+ this._maximisePlaceholder = $('<div class="lm_maximise_place"></div>');
+ this._creationTimeoutPassed = false;
+ this._subWindowsCreated = false;
+ this._dragSources = [];
+ this._updatingColumnsResponsive = false;
+ this._firstLoad = true;
+
+ this.width = null;
+ this.height = null;
+ this.root = null;
+ this.openPopouts = [];
+ this.selectedItem = null;
+ this.isSubWindow = false;
+ this.eventHub = new lm.utils.EventHub(this);
+ this.config = this._createConfig(config);
+ this.container = container;
+ this.dropTargetIndicator = null;
+ this.transitionIndicator = null;
+ this.tabDropPlaceholder = $('<div class="lm_drop_tab_placeholder"></div>');
+
+ if (this.isSubWindow === true) {
+ $('body').css('visibility', 'hidden');
+ }
+
+ this._typeToItem = {
+ 'column': lm.utils.fnBind(lm.items.RowOrColumn, this, [true]),
+ 'row': lm.utils.fnBind(lm.items.RowOrColumn, this, [false]),
+ 'stack': lm.items.Stack,
+ 'component': lm.items.Component
+ };
+ };
+
+ /**
+ * Hook that allows to access private classes
+ */
+ lm.LayoutManager.__lm = lm;
+
+ /**
+ * Takes a GoldenLayout configuration object and
+ * replaces its keys and values recursively with
+ * one letter codes
+ *
+ * @static
+ * @public
+ * @param {Object} config A GoldenLayout config object
+ *
+ * @returns {Object} minified config
+ */
+ lm.LayoutManager.minifyConfig = function (config) {
+ return (new lm.utils.ConfigMinifier()).minifyConfig(config);
+ };
+
+ /**
+ * Takes a configuration Object that was previously minified
+ * using minifyConfig and returns its original version
+ *
+ * @static
+ * @public
+ * @param {Object} minifiedConfig
+ *
+ * @returns {Object} the original configuration
+ */
+ lm.LayoutManager.unminifyConfig = function (config) {
+ return (new lm.utils.ConfigMinifier()).unminifyConfig(config);
+ };
+
+ lm.utils.copy(lm.LayoutManager.prototype, {
+
+ /**
+ * Register a component with the layout manager. If a configuration node
+ * of type component is reached it will look up componentName and create the
+ * associated component
+ *
+ * {
+ * type: "component",
+ * componentName: "EquityNewsFeed",
+ * componentState: { "feedTopic": "us-bluechips" }
+ * }
+ *
+ * @public
+ * @param {String} name
+ * @param {Function} constructor
+ *
+ * @returns {void}
+ */
+ registerComponent: function (name, constructor) {
+ if (typeof constructor !== 'function') {
+ throw new Error('Please register a constructor function');
+ }
+
+ if (this._components[name] !== undefined) {
+ throw new Error('Component ' + name + ' is already registered');
+ }
+
+ this._components[name] = constructor;
+ },
+
+ /**
+ * Creates a layout configuration object based on the the current state
+ *
+ * @public
+ * @returns {Object} GoldenLayout configuration
+ */
+ toConfig: function (root) {
+ var config, next, i;
+
+ if (this.isInitialised === false) {
+ throw new Error('Can\'t create config, layout not yet initialised');
+ }
+
+ if (root && !(root instanceof lm.items.AbstractContentItem)) {
+ throw new Error('Root must be a ContentItem');
+ }
+
+ /*
+ * settings & labels
+ */
+ config = {
+ settings: lm.utils.copy({}, this.config.settings),
+ dimensions: lm.utils.copy({}, this.config.dimensions),
+ labels: lm.utils.copy({}, this.config.labels)
+ };
+
+ /*
+ * Content
+ */
+ config.content = [];
+ next = function (configNode, item) {
+ var key, i;
+
+ for (key in item.config) {
+ if (key !== 'content') {
+ configNode[key] = item.config[key];
+ }
+ }
+
+ if (item.contentItems.length) {
+ configNode.content = [];
+
+ for (i = 0; i < item.contentItems.length; i++) {
+ configNode.content[i] = {};
+ next(configNode.content[i], item.contentItems[i]);
+ }
+ }
+ };
+
+ if (root) {
+ next(config, { contentItems: [root] });
+ } else {
+ next(config, this.root);
+ }
+
+ /*
+ * Retrieve config for subwindows
+ */
+ this._$reconcilePopoutWindows();
+ config.openPopouts = [];
+ for (i = 0; i < this.openPopouts.length; i++) {
+ config.openPopouts.push(this.openPopouts[i].toConfig());
+ }
+
+ /*
+ * Add maximised item
+ */
+ config.maximisedItemId = this._maximisedItem ? '__glMaximised' : null;
+ return config;
+ },
+
+ /**
+ * Returns a previously registered component
+ *
+ * @public
+ * @param {String} name The name used
+ *
+ * @returns {Function}
+ */
+ getComponent: function (name) {
+ if (this._components[name] === undefined) {
+ throw new lm.errors.ConfigurationError('Unknown component "' + name + '"');
+ }
+
+ return this._components[name];
+ },
+
+ /**
+ * Creates the actual layout. Must be called after all initial components
+ * are registered. Recurses through the configuration and sets up
+ * the item tree.
+ *
+ * If called before the document is ready it adds itself as a listener
+ * to the document.ready event
+ *
+ * @public
+ *
+ * @returns {void}
+ */
+ init: function () {
+
+ /**
+ * Create the popout windows straight away. If popouts are blocked
+ * an error is thrown on the same 'thread' rather than a timeout and can
+ * be caught. This also prevents any further initilisation from taking place.
+ */
+ if (this._subWindowsCreated === false) {
+ this._createSubWindows();
+ this._subWindowsCreated = true;
+ }
+
+
+ /**
+ * If the document isn't ready yet, wait for it.
+ */
+ if (document.readyState === 'loading' || document.body === null) {
+ $(document).ready(lm.utils.fnBind(this.init, this));
+ return;
+ }
+
+ /**
+ * If this is a subwindow, wait a few milliseconds for the original
+ * page's js calls to be executed, then replace the bodies content
+ * with GoldenLayout
+ */
+ if (this.isSubWindow === true && this._creationTimeoutPassed === false) {
+ setTimeout(lm.utils.fnBind(this.init, this), 7);
+ this._creationTimeoutPassed = true;
+ return;
+ }
+
+ if (this.isSubWindow === true) {
+ this._adjustToWindowMode();
+ }
+
+ this._setContainer();
+ this.dropTargetIndicator = new lm.controls.DropTargetIndicator(this.container);
+ this.transitionIndicator = new lm.controls.TransitionIndicator();
+ this.updateSize();
+ this._create(this.config);
+ this._bindEvents();
+ this.isInitialised = true;
+ this._adjustColumnsResponsive();
+ this.emit('initialised');
+ },
+
+ /**
+ * Updates the layout managers size
+ *
+ * @public
+ * @param {[int]} width height in pixels
+ * @param {[int]} height width in pixels
+ *
+ * @returns {void}
+ */
+ updateSize: function (width, height) {
+ if (arguments.length === 2) {
+ this.width = width;
+ this.height = height;
+ } else {
+ this.width = this.container.width();
+ this.height = this.container.height();
+ }
+
+ if (this.isInitialised === true) {
+ this.root.callDownwards('setSize', [this.width, this.height]);
+
+ if (this._maximisedItem) {
+ this._maximisedItem.element.width(this.container.width());
+ this._maximisedItem.element.height(this.container.height());
+ this._maximisedItem.callDownwards('setSize');
+ }
+
+ this._adjustColumnsResponsive();
+ }
+ },
+
+ /**
+ * Destroys the LayoutManager instance itself as well as every ContentItem
+ * within it. After this is called nothing should be left of the LayoutManager.
+ *
+ * @public
+ * @returns {void}
+ */
+ destroy: function () {
+ if (this.isInitialised === false) {
+ return;
+ }
+ this._onUnload();
+ $(window).off('resize', this._resizeFunction);
+ $(window).off('unload beforeunload', this._unloadFunction);
+ this.root.callDownwards('_$destroy', [], true);
+ this.root.contentItems = [];
+ this.tabDropPlaceholder.remove();
+ this.dropTargetIndicator.destroy();
+ this.transitionIndicator.destroy();
+ this.eventHub.destroy();
+
+ this._dragSources.forEach(function (dragSource) {
+ dragSource._dragListener.destroy();
+ dragSource._element = null;
+ dragSource._itemConfig = null;
+ dragSource._dragListener = null;
+ });
+ this._dragSources = [];
+ },
+
+ /**
+ * Recursively creates new item tree structures based on a provided
+ * ItemConfiguration object
+ *
+ * @public
+ * @param {Object} config ItemConfig
+ * @param {[ContentItem]} parent The item the newly created item should be a child of
+ *
+ * @returns {lm.items.ContentItem}
+ */
+ createContentItem: function (config, parent) {
+ var typeErrorMsg, contentItem;
+
+ if (typeof config.type !== 'string') {
+ throw new lm.errors.ConfigurationError('Missing parameter \'type\'', config);
+ }
+
+ if (config.type === 'react-component') {
+ config.type = 'component';
+ config.componentName = 'lm-react-component';
+ }
+
+ if (!this._typeToItem[config.type]) {
+ typeErrorMsg = 'Unknown type \'' + config.type + '\'. ' +
+ 'Valid types are ' + lm.utils.objectKeys(this._typeToItem).join(',');
+
+ throw new lm.errors.ConfigurationError(typeErrorMsg);
+ }
+
+
+ /**
+ * We add an additional stack around every component that's not within a stack anyways.
+ */
+ if (
+ // If this is a component
+ config.type === 'component' &&
+
+ // and it's not already within a stack
+ !(parent instanceof lm.items.Stack) &&
+
+ // and we have a parent
+ !!parent &&
+
+ // and it's not the topmost item in a new window
+ !(this.isSubWindow === true && parent instanceof lm.items.Root)
+ ) {
+ config = {
+ type: 'stack',
+ width: config.width,
+ height: config.height,
+ content: [config]
+ };
+ }
+
+ contentItem = new this._typeToItem[config.type](this, config, parent);
+ return contentItem;
+ },
+
+ /**
+ * Creates a popout window with the specified content and dimensions
+ *
+ * @param {Object|lm.itemsAbstractContentItem} configOrContentItem
+ * @param {[Object]} dimensions A map with width, height, left and top
+ * @param {[String]} parentId the id of the element this item will be appended to
+ * when popIn is called
+ * @param {[Number]} indexInParent The position of this item within its parent element
+
+ * @returns {lm.controls.BrowserPopout}
+ */
+ createPopout: function (configOrContentItem, dimensions, parentId, indexInParent) {
+ var config = configOrContentItem,
+ isItem = configOrContentItem instanceof lm.items.AbstractContentItem,
+ self = this,
+ windowLeft,
+ windowTop,
+ offset,
+ parent,
+ child,
+ browserPopout;
+
+ parentId = parentId || null;
+
+ if (isItem) {
+ config = this.toConfig(configOrContentItem).content;
+ parentId = lm.utils.getUniqueId();
+
+ /**
+ * If the item is the only component within a stack or for some
+ * other reason the only child of its parent the parent will be destroyed
+ * when the child is removed.
+ *
+ * In order to support this we move up the tree until we find something
+ * that will remain after the item is being popped out
+ */
+ parent = configOrContentItem.parent;
+ child = configOrContentItem;
+ while (parent.contentItems.length === 1 && !parent.isRoot) {
+ parent = parent.parent;
+ child = child.parent;
+ }
+
+ parent.addId(parentId);
+ if (isNaN(indexInParent)) {
+ indexInParent = lm.utils.indexOf(child, parent.contentItems);
+ }
+ } else {
+ if (!(config instanceof Array)) {
+ config = [config];
+ }
+ }
+
+
+ if (!dimensions && isItem) {
+ windowLeft = window.screenX || window.screenLeft;
+ windowTop = window.screenY || window.screenTop;
+ offset = configOrContentItem.element.offset();
+
+ dimensions = {
+ left: windowLeft + offset.left,
+ top: windowTop + offset.top,
+ width: configOrContentItem.element.width(),
+ height: configOrContentItem.element.height()
+ };
+ }
+
+ if (!dimensions && !isItem) {
+ dimensions = {
+ left: window.screenX || window.screenLeft + 20,
+ top: window.screenY || window.screenTop + 20,
+ width: 500,
+ height: 309
+ };
+ }
+
+ if (isItem) {
+ configOrContentItem.remove();
+ }
+
+ browserPopout = new lm.controls.BrowserPopout(config, dimensions, parentId, indexInParent, this);
+
+ browserPopout.on('initialised', function () {
+ self.emit('windowOpened', browserPopout);
+ });
+
+ browserPopout.on('closed', function () {
+ self._$reconcilePopoutWindows();
+ });
+
+ this.openPopouts.push(browserPopout);
+
+ return browserPopout;
+ },
+
+ /**
+ * Attaches DragListener to any given DOM element
+ * and turns it into a way of creating new ContentItems
+ * by 'dragging' the DOM element into the layout
+ *
+ * @param {jQuery DOM element} element
+ * @param {Object|Function} itemConfig for the new item to be created, or a function which will provide it
+ *
+ * @returns {void}
+ */
+ createDragSource: function (element, itemConfig) {
+ this.config.settings.constrainDragToContainer = false;
+ var dragSource = new lm.controls.DragSource($(element), itemConfig, this);
+ this._dragSources.push(dragSource);
+
+ return dragSource;
+ },
+
+ /**
+ * Programmatically selects an item. This deselects
+ * the currently selected item, selects the specified item
+ * and emits a selectionChanged event
+ *
+ * @param {lm.item.AbstractContentItem} item#
+ * @param {[Boolean]} _$silent Wheather to notify the item of its selection
+ * @event selectionChanged
+ *
+ * @returns {VOID}
+ */
+ selectItem: function (item, _$silent) {
+
+ if (this.config.settings.selectionEnabled !== true) {
+ throw new Error('Please set selectionEnabled to true to use this feature');
+ }
+
+ if (item === this.selectedItem) {
+ return;
+ }
+
+ if (this.selectedItem !== null) {
+ this.selectedItem.deselect();
+ }
+
+ if (item && _$silent !== true) {
+ item.select();
+ }
+
+ this.selectedItem = item;
+
+ this.emit('selectionChanged', item);
+ },
+
+ /*************************
+ * PACKAGE PRIVATE
+ *************************/
+ _$maximiseItem: function (contentItem) {
+ if (this._maximisedItem !== null) {
+ this._$minimiseItem(this._maximisedItem);
+ }
+ this._maximisedItem = contentItem;
+ this._maximisedItem.addId('__glMaximised');
+ contentItem.element.addClass('lm_maximised');
+ contentItem.element.after(this._maximisePlaceholder);
+ this.root.element.prepend(contentItem.element);
+ contentItem.element.width(this.container.width());
+ contentItem.element.height(this.container.height());
+ contentItem.callDownwards('setSize');
+ this._maximisedItem.emit('maximised');
+ this.emit('stateChanged');
+ },
+
+ _$minimiseItem: function (contentItem) {
+ contentItem.element.removeClass('lm_maximised');
+ contentItem.removeId('__glMaximised');
+ this._maximisePlaceholder.after(contentItem.element);
+ this._maximisePlaceholder.remove();
+ contentItem.parent.callDownwards('setSize');
+ this._maximisedItem = null;
+ contentItem.emit('minimised');
+ this.emit('stateChanged');
+ },
+
+ /**
+ * This method is used to get around sandboxed iframe restrictions.
+ * If 'allow-top-navigation' is not specified in the iframe's 'sandbox' attribute
+ * (as is the case with codepens) the parent window is forbidden from calling certain
+ * methods on the child, such as window.close() or setting document.location.href.
+ *
+ * This prevented GoldenLayout popouts from popping in in codepens. The fix is to call
+ * _$closeWindow on the child window's gl instance which (after a timeout to disconnect
+ * the invoking method from the close call) closes itself.
+ *
+ * @packagePrivate
+ *
+ * @returns {void}
+ */
+ _$closeWindow: function () {
+ window.setTimeout(function () {
+ window.close();
+ }, 1);
+ },
+
+ _$getArea: function (x, y) {
+ var i, area, smallestSurface = Infinity, mathingArea = null;
+
+ for (i = 0; i < this._itemAreas.length; i++) {
+ area = this._itemAreas[i];
+
+ if (
+ x > area.x1 &&
+ x < area.x2 &&
+ y > area.y1 &&
+ y < area.y2 &&
+ smallestSurface > area.surface
+ ) {
+ smallestSurface = area.surface;
+ mathingArea = area;
+ }
+ }
+
+ return mathingArea;
+ },
+
+ _$createRootItemAreas: function () {
+ var areaSize = 50;
+ var sides = { y2: 0, x2: 0, y1: 'y2', x1: 'x2' };
+ for (var side in sides) {
+ var area = this.root._$getArea();
+ area.side = side;
+ if (sides[side])
+ area[side] = area[sides[side]] - areaSize;
+ else
+ area[side] = areaSize;
+ area.surface = (area.x2 - area.x1) * (area.y2 - area.y1);
+ this._itemAreas.push(area);
+ }
+ },
+
+ _$calculateItemAreas: function () {
+ var i, area, allContentItems = this._getAllContentItems();
+ this._itemAreas = [];
+
+ /**
+ * If the last item is dragged out, highlight the entire container size to
+ * allow to re-drop it. allContentItems[ 0 ] === this.root at this point
+ *
+ * Don't include root into the possible drop areas though otherwise since it
+ * will used for every gap in the layout, e.g. splitters
+ */
+ if (allContentItems.length === 1) {
+ this._itemAreas.push(this.root._$getArea());
+ return;
+ }
+ this._$createRootItemAreas();
+
+ for (i = 0; i < allContentItems.length; i++) {
+
+ if (!(allContentItems[i].isStack)) {
+ continue;
+ }
+
+ area = allContentItems[i]._$getArea();
+
+ if (area === null) {
+ continue;
+ } else if (area instanceof Array) {
+ this._itemAreas = this._itemAreas.concat(area);
+ } else {
+ this._itemAreas.push(area);
+ var header = {};
+ lm.utils.copy(header, area);
+ lm.utils.copy(header, area.contentItem._contentAreaDimensions.header.highlightArea);
+ header.surface = (header.x2 - header.x1) * (header.y2 - header.y1);
+ this._itemAreas.push(header);
+ }
+ }
+ },
+
+ /**
+ * Takes a contentItem or a configuration and optionally a parent
+ * item and returns an initialised instance of the contentItem.
+ * If the contentItem is a function, it is first called
+ *
+ * @packagePrivate
+ *
+ * @param {lm.items.AbtractContentItem|Object|Function} contentItemOrConfig
+ * @param {lm.items.AbtractContentItem} parent Only necessary when passing in config
+ *
+ * @returns {lm.items.AbtractContentItem}
+ */
+ _$normalizeContentItem: function (contentItemOrConfig, parent) {
+ if (!contentItemOrConfig) {
+ throw new Error('No content item defined');
+ }
+
+ if (lm.utils.isFunction(contentItemOrConfig)) {
+ contentItemOrConfig = contentItemOrConfig();
+ }
+
+ if (contentItemOrConfig instanceof lm.items.AbstractContentItem) {
+ return contentItemOrConfig;
+ }
+
+ if ($.isPlainObject(contentItemOrConfig) && contentItemOrConfig.type) {
+ var newContentItem = this.createContentItem(contentItemOrConfig, parent);
+ newContentItem.callDownwards('_$init');
+ return newContentItem;
+ } else {
+ throw new Error('Invalid contentItem');
+ }
+ },
+
+ /**
+ * Iterates through the array of open popout windows and removes the ones
+ * that are effectively closed. This is necessary due to the lack of reliably
+ * listening for window.close / unload events in a cross browser compatible fashion.
+ *
+ * @packagePrivate
+ *
+ * @returns {void}
+ */
+ _$reconcilePopoutWindows: function () {
+ var openPopouts = [], i;
+
+ for (i = 0; i < this.openPopouts.length; i++) {
+ if (this.openPopouts[i].getWindow().closed === false) {
+ openPopouts.push(this.openPopouts[i]);
+ } else {
+ this.emit('windowClosed', this.openPopouts[i]);
+ }
+ }
+
+ if (this.openPopouts.length !== openPopouts.length) {
+ this.emit('stateChanged');
+ this.openPopouts = openPopouts;
+ }
+
+ },
+
+ /***************************
+ * PRIVATE
+ ***************************/
+ /**
+ * Returns a flattened array of all content items,
+ * regardles of level or type
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _getAllContentItems: function () {
+ var allContentItems = [];
+
+ var addChildren = function (contentItem) {
+ allContentItems.push(contentItem);
+
+ if (contentItem.contentItems instanceof Array) {
+ for (var i = 0; i < contentItem.contentItems.length; i++) {
+ addChildren(contentItem.contentItems[i]);
+ }
+ }
+ };
+
+ addChildren(this.root);
+
+ return allContentItems;
+ },
+
+ /**
+ * Binds to DOM/BOM events on init
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _bindEvents: function () {
+ if (this._isFullPage) {
+ $(window).resize(this._resizeFunction);
+ }
+ $(window).on('unload beforeunload', this._unloadFunction);
+ },
+
+ /**
+ * Debounces resize events
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _onResize: function () {
+ clearTimeout(this._resizeTimeoutId);
+ this._resizeTimeoutId = setTimeout(lm.utils.fnBind(this.updateSize, this), 100);
+ },
+
+ /**
+ * Extends the default config with the user specific settings and applies
+ * derivations. Please note that there's a seperate method (AbstractContentItem._extendItemNode)
+ * that deals with the extension of item configs
+ *
+ * @param {Object} config
+ * @static
+ * @returns {Object} config
+ */
+ _createConfig: function (config) {
+ var windowConfigKey = lm.utils.getQueryStringParam('gl-window');
+
+ if (windowConfigKey) {
+ this.isSubWindow = true;
+ config = localStorage.getItem(windowConfigKey);
+ config = JSON.parse(config);
+ config = (new lm.utils.ConfigMinifier()).unminifyConfig(config);
+ localStorage.removeItem(windowConfigKey);
+ }
+
+ config = $.extend(true, {}, lm.config.defaultConfig, config);
+
+ var nextNode = function (node) {
+ for (var key in node) {
+ if (key !== 'props' && typeof node[key] === 'object') {
+ nextNode(node[key]);
+ }
+ else if (key === 'type' && node[key] === 'react-component') {
+ node.type = 'component';
+ node.componentName = 'lm-react-component';
+ }
+ }
+ }
+
+ nextNode(config);
+
+ if (config.settings.hasHeaders === false) {
+ config.dimensions.headerHeight = 0;
+ }
+
+ return config;
+ },
+
+ /**
+ * This is executed when GoldenLayout detects that it is run
+ * within a previously opened popout window.
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _adjustToWindowMode: function () {
+ var popInButton = $('<div class="lm_popin" title="' + this.config.labels.popin + '">' +
+ '<div class="lm_icon"></div>' +
+ '<div class="lm_bg"></div>' +
+ '</div>');
+
+ popInButton.click(lm.utils.fnBind(function () {
+ this.emit('popIn');
+ }, this));
+
+ document.title = lm.utils.stripTags(this.config.content[0].title);
+
+ $('head').append($('body link, body style, template, .gl_keep'));
+
+ this.container = $('body')
+ .html('')
+ .css('visibility', 'visible')
+ .append(popInButton);
+
+ /*
+ * This seems a bit pointless, but actually causes a reflow/re-evaluation getting around
+ * slickgrid's "Cannot find stylesheet." bug in chrome
+ */
+ var x = document.body.offsetHeight; // jshint ignore:line
+
+ /*
+ * Expose this instance on the window object
+ * to allow the opening window to interact with
+ * it
+ */
+ window.__glInstance = this;
+ },
+
+ /**
+ * Creates Subwindows (if there are any). Throws an error
+ * if popouts are blocked.
+ *
+ * @returns {void}
+ */
+ _createSubWindows: function () {
+ var i, popout;
+
+ for (i = 0; i < this.config.openPopouts.length; i++) {
+ popout = this.config.openPopouts[i];
+
+ this.createPopout(
+ popout.content,
+ popout.dimensions,
+ popout.parentId,
+ popout.indexInParent
+ );
+ }
+ },
+
+ /**
+ * Determines what element the layout will be created in
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _setContainer: function () {
+ var container = $(this.container || 'body');
+
+ if (container.length === 0) {
+ throw new Error('GoldenLayout container not found');
+ }
+
+ if (container.length > 1) {
+ throw new Error('GoldenLayout more than one container element specified');
+ }
+
+ if (container[0] === document.body) {
+ this._isFullPage = true;
+
+ $('html, body').css({
+ height: '100%',
+ margin: 0,
+ padding: 0,
+ overflow: 'hidden'
+ });
+ }
+
+ this.container = container;
+ },
+
+ /**
+ * Kicks of the initial, recursive creation chain
+ *
+ * @param {Object} config GoldenLayout Config
+ *
+ * @returns {void}
+ */
+ _create: function (config) {
+ var errorMsg;
+
+ if (!(config.content instanceof Array)) {
+ if (config.content === undefined) {
+ errorMsg = 'Missing setting \'content\' on top level of configuration';
+ } else {
+ errorMsg = 'Configuration parameter \'content\' must be an array';
+ }
+
+ throw new lm.errors.ConfigurationError(errorMsg, config);
+ }
+
+ if (config.content.length > 1) {
+ errorMsg = 'Top level content can\'t contain more then one element.';
+ throw new lm.errors.ConfigurationError(errorMsg, config);
+ }
+
+ this.root = new lm.items.Root(this, { content: config.content }, this.container);
+ this.root.callDownwards('_$init');
+
+ if (config.maximisedItemId === '__glMaximised') {
+ this.root.getItemsById(config.maximisedItemId)[0].toggleMaximise();
+ }
+ },
+
+ /**
+ * Called when the window is closed or the user navigates away
+ * from the page
+ *
+ * @returns {void}
+ */
+ _onUnload: function () {
+ if (this.config.settings.closePopoutsOnUnload === true) {
+ for (var i = 0; i < this.openPopouts.length; i++) {
+ this.openPopouts[i].close();
+ }
+ }
+ },
+
+ /**
+ * Adjusts the number of columns to be lower to fit the screen and still maintain minItemWidth.
+ *
+ * @returns {void}
+ */
+ _adjustColumnsResponsive: function () {
+
+ // If there is no min width set, or not content items, do nothing.
+ if (!this._useResponsiveLayout() || this._updatingColumnsResponsive || !this.config.dimensions || !this.config.dimensions.minItemWidth || this.root.contentItems.length === 0 || !this.root.contentItems[0].isRow) {
+ this._firstLoad = false;
+ return;
+ }
+
+ this._firstLoad = false;
+
+ // If there is only one column, do nothing.
+ var columnCount = this.root.contentItems[0].contentItems.length;
+ if (columnCount <= 1) {
+ return;
+ }
+
+ // If they all still fit, do nothing.
+ var minItemWidth = this.config.dimensions.minItemWidth;
+ var totalMinWidth = columnCount * minItemWidth;
+ if (totalMinWidth <= this.width) {
+ return;
+ }
+
+ // Prevent updates while it is already happening.
+ this._updatingColumnsResponsive = true;
+
+ // Figure out how many columns to stack, and put them all in the first stack container.
+ var finalColumnCount = Math.max(Math.floor(this.width / minItemWidth), 1);
+ var stackColumnCount = columnCount - finalColumnCount;
+
+ var rootContentItem = this.root.contentItems[0];
+ var firstStackContainer = this._findAllStackContainers()[0];
+ for (var i = 0; i < stackColumnCount; i++) {
+ // Stack from right.
+ var column = rootContentItem.contentItems[rootContentItem.contentItems.length - 1];
+ this._addChildContentItemsToContainer(firstStackContainer, column);
+ }
+
+ this._updatingColumnsResponsive = false;
+ },
+
+ /**
+ * Determines if responsive layout should be used.
+ *
+ * @returns {bool} - True if responsive layout should be used; otherwise false.
+ */
+ _useResponsiveLayout: function () {
+ return this.config.settings && (this.config.settings.responsiveMode == 'always' || (this.config.settings.responsiveMode == 'onload' && this._firstLoad));
+ },
+
+ /**
+ * Adds all children of a node to another container recursively.
+ * @param {object} container - Container to add child content items to.
+ * @param {object} node - Node to search for content items.
+ * @returns {void}
+ */
+ _addChildContentItemsToContainer: function (container, node) {
+ if (node.type === 'stack') {
+ node.contentItems.forEach(function (item) {
+ container.addChild(item);
+ node.removeChild(item, true);
+ });
+ }
+ else {
+ node.contentItems.forEach(lm.utils.fnBind(function (item) {
+ this._addChildContentItemsToContainer(container, item);
+ }, this));
+ }
+ },
+
+ /**
+ * Finds all the stack containers.
+ * @returns {array} - The found stack containers.
+ */
+ _findAllStackContainers: function () {
+ var stackContainers = [];
+ this._findAllStackContainersRecursive(stackContainers, this.root);
+
+ return stackContainers;
+ },
+
+ /**
+ * Finds all the stack containers.
+ *
+ * @param {array} - Set of containers to populate.
+ * @param {object} - Current node to process.
+ *
+ * @returns {void}
+ */
+ _findAllStackContainersRecursive: function (stackContainers, node) {
+ node.contentItems.forEach(lm.utils.fnBind(function (item) {
+ if (item.type == 'stack') {
+ stackContainers.push(item);
+ }
+ else if (!item.isComponent) {
+ this._findAllStackContainersRecursive(stackContainers, item);
+ }
+ }, this));
+ }
+ });
+
+ /**
+ * Expose the Layoutmanager as the single entrypoint using UMD
+ */
+ (function () {
+ /* global define */
+ if (typeof define === 'function' && define.amd) {
+ define(['jquery'], function (jquery) {
+ $ = jquery;
+ return lm.LayoutManager;
+ }); // jshint ignore:line
+ } else if (typeof exports === 'object') {
+ module.exports = lm.LayoutManager;
+ } else {
+ window.GoldenLayout = lm.LayoutManager;
+ }
+ })();
+
+ lm.config.itemDefaultConfig = {
+ isClosable: true,
+ reorderEnabled: true,
+ title: ''
+ };
+ lm.config.defaultConfig = {
+ openPopouts: [],
+ settings: {
+ hasHeaders: true,
+ constrainDragToContainer: true,
+ reorderEnabled: true,
+ selectionEnabled: false,
+ popoutWholeStack: false,
+ blockedPopoutsThrowError: true,
+ closePopoutsOnUnload: true,
+ showPopoutIcon: true,
+ showMaximiseIcon: true,
+ showCloseIcon: true,
+ responsiveMode: 'onload', // Can be onload, always, or none.
+ tabOverlapAllowance: 0, // maximum pixel overlap per tab
+ reorderOnTabMenuClick: true,
+ tabControlOffset: 10
+ },
+ dimensions: {
+ borderWidth: 5,
+ borderGrabWidth: 15,
+ minItemHeight: 10,
+ minItemWidth: 10,
+ headerHeight: 20,
+ dragProxyWidth: 300,
+ dragProxyHeight: 200
+ },
+ labels: {
+ close: 'close',
+ maximise: 'maximise',
+ minimise: 'minimise',
+ popout: 'open in new window',
+ popin: 'pop in',
+ tabDropdown: 'additional tabs'
+ }
+ };
+
+ lm.container.ItemContainer = function (config, parent, layoutManager) {
+ lm.utils.EventEmitter.call(this);
+
+ this.width = null;
+ this.height = null;
+ this.title = config.componentName;
+ this.parent = parent;
+ this.layoutManager = layoutManager;
+ this.isHidden = false;
+
+ this._config = config;
+ this._element = $([
+ '<div class="lm_item_container">',
+ '<div class="lm_content"></div>',
+ '</div>'
+ ].join(''));
+
+ this._contentElement = this._element.find('.lm_content');
+ };
+
+ lm.utils.copy(lm.container.ItemContainer.prototype, {
+
+ /**
+ * Get the inner DOM element the container's content
+ * is intended to live in
+ *
+ * @returns {DOM element}
+ */
+ getElement: function () {
+ return this._contentElement;
+ },
+
+ /**
+ * Hide the container. Notifies the containers content first
+ * and then hides the DOM node. If the container is already hidden
+ * this should have no effect
+ *
+ * @returns {void}
+ */
+ hide: function () {
+ this.emit('hide');
+ this.isHidden = true;
+ this._element.hide();
+ },
+
+ /**
+ * Shows a previously hidden container. Notifies the
+ * containers content first and then shows the DOM element.
+ * If the container is already visible this has no effect.
+ *
+ * @returns {void}
+ */
+ show: function () {
+ this.emit('show');
+ this.isHidden = false;
+ this._element.show();
+ // call shown only if the container has a valid size
+ if (this.height != 0 || this.width != 0) {
+ this.emit('shown');
+ }
+ },
+
+ /**
+ * Set the size from within the container. Traverses up
+ * the item tree until it finds a row or column element
+ * and resizes its items accordingly.
+ *
+ * If this container isn't a descendant of a row or column
+ * it returns false
+ * @todo Rework!!!
+ * @param {Number} width The new width in pixel
+ * @param {Number} height The new height in pixel
+ *
+ * @returns {Boolean} resizeSuccesful
+ */
+ setSize: function (width, height) {
+ var rowOrColumn = this.parent,
+ rowOrColumnChild = this,
+ totalPixel,
+ percentage,
+ direction,
+ newSize,
+ delta,
+ i;
+
+ while (!rowOrColumn.isColumn && !rowOrColumn.isRow) {
+ rowOrColumnChild = rowOrColumn;
+ rowOrColumn = rowOrColumn.parent;
+
+
+ /**
+ * No row or column has been found
+ */
+ if (rowOrColumn.isRoot) {
+ return false;
+ }
+ }
+
+ direction = rowOrColumn.isColumn ? "height" : "width";
+ newSize = direction === "height" ? height : width;
+
+ totalPixel = this[direction] * (1 / (rowOrColumnChild.config[direction] / 100));
+ percentage = (newSize / totalPixel) * 100;
+ delta = (rowOrColumnChild.config[direction] - percentage) / (rowOrColumn.contentItems.length - 1);
+
+ for (i = 0; i < rowOrColumn.contentItems.length; i++) {
+ if (rowOrColumn.contentItems[i] === rowOrColumnChild) {
+ rowOrColumn.contentItems[i].config[direction] = percentage;
+ } else {
+ rowOrColumn.contentItems[i].config[direction] += delta;
+ }
+ }
+
+ rowOrColumn.callDownwards('setSize');
+
+ return true;
+ },
+
+ /**
+ * Closes the container if it is closable. Can be called by
+ * both the component within at as well as the contentItem containing
+ * it. Emits a close event before the container itself is closed.
+ *
+ * @returns {void}
+ */
+ close: function () {
+ if (this._config.isClosable) {
+ this.emit('close');
+ this.parent.close();
+ }
+ },
+
+ /**
+ * Returns the current state object
+ *
+ * @returns {Object} state
+ */
+ getState: function () {
+ return this._config.componentState;
+ },
+
+ /**
+ * Merges the provided state into the current one
+ *
+ * @param {Object} state
+ *
+ * @returns {void}
+ */
+ extendState: function (state) {
+ this.setState($.extend(true, this.getState(), state));
+ },
+
+ /**
+ * Notifies the layout manager of a stateupdate
+ *
+ * @param {serialisable} state
+ */
+ setState: function (state) {
+ this._config.componentState = state;
+ this.parent.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Set's the components title
+ *
+ * @param {String} title
+ */
+ setTitle: function (title) {
+ this.parent.setTitle(title);
+ },
+
+ /**
+ * Set's the containers size. Called by the container's component.
+ * To set the size programmatically from within the container please
+ * use the public setSize method
+ *
+ * @param {[Int]} width in px
+ * @param {[Int]} height in px
+ *
+ * @returns {void}
+ */
+ _$setSize: function (width, height) {
+ if (width !== this.width || height !== this.height) {
+ this.width = width;
+ this.height = height;
+ var cl = this._contentElement[0];
+ var hdelta = cl.offsetWidth - cl.clientWidth;
+ var vdelta = cl.offsetHeight - cl.clientHeight;
+ this._contentElement.width(this.width - hdelta)
+ .height(this.height - vdelta);
+ this.emit('resize');
+ }
+ }
+ });
+
+ /**
+ * Pops a content item out into a new browser window.
+ * This is achieved by
+ *
+ * - Creating a new configuration with the content item as root element
+ * - Serializing and minifying the configuration
+ * - Opening the current window's URL with the configuration as a GET parameter
+ * - GoldenLayout when opened in the new window will look for the GET parameter
+ * and use it instead of the provided configuration
+ *
+ * @param {Object} config GoldenLayout item config
+ * @param {Object} dimensions A map with width, height, top and left
+ * @param {String} parentId The id of the element the item will be appended to on popIn
+ * @param {Number} indexInParent The position of this element within its parent
+ * @param {lm.LayoutManager} layoutManager
+ */
+ lm.controls.BrowserPopout = function (config, dimensions, parentId, indexInParent, layoutManager) {
+ lm.utils.EventEmitter.call(this);
+ this.isInitialised = false;
+
+ this._config = config;
+ this._dimensions = dimensions;
+ this._parentId = parentId;
+ this._indexInParent = indexInParent;
+ this._layoutManager = layoutManager;
+ this._popoutWindow = null;
+ this._id = null;
+ this._createWindow();
+ };
+
+ lm.utils.copy(lm.controls.BrowserPopout.prototype, {
+
+ toConfig: function () {
+ if (this.isInitialised === false) {
+ throw new Error('Can\'t create config, layout not yet initialised');
+ return;
+ }
+ return {
+ dimensions: {
+ width: this.getGlInstance().width,
+ height: this.getGlInstance().height,
+ left: this._popoutWindow.screenX || this._popoutWindow.screenLeft,
+ top: this._popoutWindow.screenY || this._popoutWindow.screenTop
+ },
+ content: this.getGlInstance().toConfig().content,
+ parentId: this._parentId,
+ indexInParent: this._indexInParent
+ };
+ },
+
+ getGlInstance: function () {
+ return this._popoutWindow.__glInstance;
+ },
+
+ getWindow: function () {
+ return this._popoutWindow;
+ },
+
+ close: function () {
+ if (this.getGlInstance()) {
+ this.getGlInstance()._$closeWindow();
+ } else {
+ try {
+ this.getWindow().close();
+ } catch (e) {
+ }
+ }
+ },
+
+ /**
+ * Returns the popped out item to its original position. If the original
+ * parent isn't available anymore it falls back to the layout's topmost element
+ */
+ popIn: function () {
+ var childConfig,
+ parentItem,
+ index = this._indexInParent;
+
+ if (this._parentId) {
+
+ /*
+ * The $.extend call seems a bit pointless, but it's crucial to
+ * copy the config returned by this.getGlInstance().toConfig()
+ * onto a new object. Internet Explorer keeps the references
+ * to objects on the child window, resulting in the following error
+ * once the child window is closed:
+ *
+ * The callee (server [not server application]) is not available and disappeared
+ */
+ childConfig = $.extend(true, {}, this.getGlInstance().toConfig()).content[0];
+ parentItem = this._layoutManager.root.getItemsById(this._parentId)[0];
+
+ /*
+ * Fallback if parentItem is not available. Either add it to the topmost
+ * item or make it the topmost item if the layout is empty
+ */
+ if (!parentItem) {
+ if (this._layoutManager.root.contentItems.length > 0) {
+ parentItem = this._layoutManager.root.contentItems[0];
+ } else {
+ parentItem = this._layoutManager.root;
+ }
+ index = 0;
+ }
+ }
+
+ parentItem.addChild(childConfig, this._indexInParent);
+ this.close();
+ },
+
+ /**
+ * Creates the URL and window parameter
+ * and opens a new window
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _createWindow: function () {
+ var checkReadyInterval,
+ url = this._createUrl(),
+
+ /**
+ * Bogus title to prevent re-usage of existing window with the
+ * same title. The actual title will be set by the new window's
+ * GoldenLayout instance if it detects that it is in subWindowMode
+ */
+ title = Math.floor(Math.random() * 1000000).toString(36),
+
+ /**
+ * The options as used in the window.open string
+ */
+ options = this._serializeWindowOptions({
+ width: this._dimensions.width,
+ height: this._dimensions.height,
+ innerWidth: this._dimensions.width,
+ innerHeight: this._dimensions.height,
+ menubar: 'no',
+ toolbar: 'no',
+ location: 'no',
+ personalbar: 'no',
+ resizable: 'yes',
+ scrollbars: 'no',
+ status: 'no'
+ });
+
+ this._popoutWindow = window.open(url, title, options);
+
+ if (!this._popoutWindow) {
+ if (this._layoutManager.config.settings.blockedPopoutsThrowError === true) {
+ var error = new Error('Popout blocked');
+ error.type = 'popoutBlocked';
+ throw error;
+ } else {
+ return;
+ }
+ }
+
+ $(this._popoutWindow)
+ .on('load', lm.utils.fnBind(this._positionWindow, this))
+ .on('unload beforeunload', lm.utils.fnBind(this._onClose, this));
+
+ /**
+ * Polling the childwindow to find out if GoldenLayout has been initialised
+ * doesn't seem optimal, but the alternatives - adding a callback to the parent
+ * window or raising an event on the window object - both would introduce knowledge
+ * about the parent to the child window which we'd rather avoid
+ */
+ checkReadyInterval = setInterval(lm.utils.fnBind(function () {
+ if (this._popoutWindow.__glInstance && this._popoutWindow.__glInstance.isInitialised) {
+ this._onInitialised();
+ clearInterval(checkReadyInterval);
+ }
+ }, this), 10);
+ },
+
+ /**
+ * Serialises a map of key:values to a window options string
+ *
+ * @param {Object} windowOptions
+ *
+ * @returns {String} serialised window options
+ */
+ _serializeWindowOptions: function (windowOptions) {
+ var windowOptionsString = [], key;
+
+ for (key in windowOptions) {
+ windowOptionsString.push(key + '=' + windowOptions[key]);
+ }
+
+ return windowOptionsString.join(',');
+ },
+
+ /**
+ * Creates the URL for the new window, including the
+ * config GET parameter
+ *
+ * @returns {String} URL
+ */
+ _createUrl: function () {
+ var config = { content: this._config },
+ storageKey = 'gl-window-config-' + lm.utils.getUniqueId(),
+ urlParts;
+
+ config = (new lm.utils.ConfigMinifier()).minifyConfig(config);
+
+ try {
+ localStorage.setItem(storageKey, JSON.stringify(config));
+ } catch (e) {
+ throw new Error('Error while writing to localStorage ' + e.toString());
+ }
+
+ urlParts = document.location.href.split('?');
+
+ // URL doesn't contain GET-parameters
+ if (urlParts.length === 1) {
+ return urlParts[0] + '?gl-window=' + storageKey;
+
+ // URL contains GET-parameters
+ } else {
+ return document.location.href + '&gl-window=' + storageKey;
+ }
+ },
+
+ /**
+ * Move the newly created window roughly to
+ * where the component used to be.
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _positionWindow: function () {
+ this._popoutWindow.moveTo(this._dimensions.left, this._dimensions.top);
+ this._popoutWindow.focus();
+ },
+
+ /**
+ * Callback when the new window is opened and the GoldenLayout instance
+ * within it is initialised
+ *
+ * @returns {void}
+ */
+ _onInitialised: function () {
+ this.isInitialised = true;
+ this.getGlInstance().on('popIn', this.popIn, this);
+ this.emit('initialised');
+ },
+
+ /**
+ * Invoked 50ms after the window unload event
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _onClose: function () {
+ setTimeout(lm.utils.fnBind(this.emit, this, ['closed']), 50);
+ }
+ });
+ /**
+ * This class creates a temporary container
+ * for the component whilst it is being dragged
+ * and handles drag events
+ *
+ * @constructor
+ * @private
+ *
+ * @param {Number} x The initial x position
+ * @param {Number} y The initial y position
+ * @param {lm.utils.DragListener} dragListener
+ * @param {lm.LayoutManager} layoutManager
+ * @param {lm.item.AbstractContentItem} contentItem
+ * @param {lm.item.AbstractContentItem} originalParent
+ */
+ lm.controls.DragProxy = function (x, y, dragListener, layoutManager, contentItem, originalParent) {
+
+ lm.utils.EventEmitter.call(this);
+
+ this._dragListener = dragListener;
+ this._layoutManager = layoutManager;
+ this._contentItem = contentItem;
+ this._originalParent = originalParent;
+
+ this._area = null;
+ this._lastValidArea = null;
+
+ this._dragListener.on('drag', this._onDrag, this);
+ this._dragListener.on('dragStop', this._onDrop, this);
+
+ this.element = $(lm.controls.DragProxy._template);
+ if (originalParent && originalParent._side) {
+ this._sided = originalParent._sided;
+ this.element.addClass('lm_' + originalParent._side);
+ if (['right', 'bottom'].indexOf(originalParent._side) >= 0)
+ this.element.find('.lm_content').after(this.element.find('.lm_header'));
+ }
+ this.element.css({ left: x, top: y });
+ this.element.find('.lm_tab').attr('title', lm.utils.stripTags(this._contentItem.config.title));
+ this.element.find('.lm_title').html(this._contentItem.config.title);
+ this.childElementContainer = this.element.find('.lm_content');
+ this.childElementContainer.append(contentItem.element);
+
+ this._updateTree();
+ this._layoutManager._$calculateItemAreas();
+ this._setDimensions();
+
+ $(document.body).append(this.element);
+
+ var offset = this._layoutManager.container.offset();
+
+ this._minX = offset.left;
+ this._minY = offset.top;
+ this._maxX = this._layoutManager.container.width() + this._minX;
+ this._maxY = this._layoutManager.container.height() + this._minY;
+ this._width = this.element.width();
+ this._height = this.element.height();
+
+ this._setDropPosition(x, y);
+ };
+
+ lm.controls.DragProxy._template = '<div class="lm_dragProxy">' +
+ '<div class="lm_header">' +
+ '<ul class="lm_tabs">' +
+ '<li class="lm_tab lm_active"><i class="lm_left"></i>' +
+ '<span class="lm_title"></span>' +
+ '<i class="lm_right"></i></li>' +
+ '</ul>' +
+ '</div>' +
+ '<div class="lm_content"></div>' +
+ '</div>';
+
+ lm.utils.copy(lm.controls.DragProxy.prototype, {
+
+ /**
+ * Callback on every mouseMove event during a drag. Determines if the drag is
+ * still within the valid drag area and calls the layoutManager to highlight the
+ * current drop area
+ *
+ * @param {Number} offsetX The difference from the original x position in px
+ * @param {Number} offsetY The difference from the original y position in px
+ * @param {jQuery DOM event} event
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _onDrag: function (offsetX, offsetY, event) {
+
+ event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[0] : event;
+
+ var x = event.pageX,
+ y = event.pageY,
+ isWithinContainer = x > this._minX && x < this._maxX && y > this._minY && y < this._maxY;
+
+ if (!isWithinContainer && this._layoutManager.config.settings.constrainDragToContainer === true) {
+ return;
+ }
+
+ this._setDropPosition(x, y);
+ },
+
+ /**
+ * Sets the target position, highlighting the appropriate area
+ *
+ * @param {Number} x The x position in px
+ * @param {Number} y The y position in px
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _setDropPosition: function (x, y) {
+ this.element.css({ left: x, top: y });
+ this._area = this._layoutManager._$getArea(x, y);
+
+ if (this._area !== null) {
+ this._lastValidArea = this._area;
+ this._area.contentItem._$highlightDropZone(x, y, this._area);
+ }
+ },
+
+ /**
+ * Callback when the drag has finished. Determines the drop area
+ * and adds the child to it
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _onDrop: function () {
+ this._layoutManager.dropTargetIndicator.hide();
+
+ /*
+ * Valid drop area found
+ */
+ if (this._area !== null) {
+ this._area.contentItem._$onDrop(this._contentItem, this._area);
+
+ /**
+ * No valid drop area available at present, but one has been found before.
+ * Use it
+ */
+ } else if (this._lastValidArea !== null) {
+ this._lastValidArea.contentItem._$onDrop(this._contentItem, this._lastValidArea);
+
+ /**
+ * No valid drop area found during the duration of the drag. Return
+ * content item to its original position if a original parent is provided.
+ * (Which is not the case if the drag had been initiated by createDragSource)
+ */
+ } else if (this._originalParent) {
+ this._originalParent.addChild(this._contentItem);
+
+ /**
+ * The drag didn't ultimately end up with adding the content item to
+ * any container. In order to ensure clean up happens, destroy the
+ * content item.
+ */
+ } else {
+ this._contentItem._$destroy();
+ }
+
+ this.element.remove();
+
+ this._layoutManager.emit('itemDropped', this._contentItem);
+ },
+
+ /**
+ * Removes the item from its original position within the tree
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _updateTree: function () {
+
+ /**
+ * parent is null if the drag had been initiated by a external drag source
+ */
+ if (this._contentItem.parent) {
+ this._contentItem.parent.removeChild(this._contentItem, true);
+ }
+
+ this._contentItem._$setParent(this);
+ },
+
+ /**
+ * Updates the Drag Proxie's dimensions
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _setDimensions: function () {
+ var dimensions = this._layoutManager.config.dimensions,
+ width = dimensions.dragProxyWidth,
+ height = dimensions.dragProxyHeight;
+
+ this.element.width(width);
+ this.element.height(height);
+ width -= (this._sided ? dimensions.headerHeight : 0);
+ height -= (!this._sided ? dimensions.headerHeight : 0);
+ this.childElementContainer.width(width);
+ this.childElementContainer.height(height);
+ this._contentItem.element.width(width);
+ this._contentItem.element.height(height);
+ this._contentItem.callDownwards('_$show');
+ this._contentItem.callDownwards('setSize');
+ }
+ });
+
+ /**
+ * Allows for any DOM item to create a component on drag
+ * start tobe dragged into the Layout
+ *
+ * @param {jQuery element} element
+ * @param {Object} itemConfig the configuration for the contentItem that will be created
+ * @param {LayoutManager} layoutManager
+ *
+ * @constructor
+ */
+ lm.controls.DragSource = function (element, itemConfig, layoutManager) {
+ this._element = element;
+ this._itemConfig = itemConfig;
+ this._layoutManager = layoutManager;
+ this._dragListener = null;
+
+ this._createDragListener();
+ };
+
+ lm.utils.copy(lm.controls.DragSource.prototype, {
+
+ /**
+ * Called initially and after every drag
+ *
+ * @returns {void}
+ */
+ _createDragListener: function () {
+ if (this._dragListener !== null) {
+ this._dragListener.destroy();
+ }
+
+ this._dragListener = new lm.utils.DragListener(this._element);
+ this._dragListener.on('dragStart', this._onDragStart, this);
+ this._dragListener.on('dragStop', this._createDragListener, this);
+ },
+
+ /**
+ * Callback for the DragListener's dragStart event
+ *
+ * @param {int} x the x position of the mouse on dragStart
+ * @param {int} y the x position of the mouse on dragStart
+ *
+ * @returns {void}
+ */
+ _onDragStart: function (x, y) {
+ var itemConfig = this._itemConfig;
+ if (lm.utils.isFunction(itemConfig)) {
+ itemConfig = itemConfig();
+ }
+ var contentItem = this._layoutManager._$normalizeContentItem($.extend(true, {}, itemConfig)),
+ dragProxy = new lm.controls.DragProxy(x, y, this._dragListener, this._layoutManager, contentItem, null);
+
+ this._layoutManager.transitionIndicator.transitionElements(this._element, dragProxy.element);
+ }
+ });
+
+ lm.controls.DropTargetIndicator = function () {
+ this.element = $(lm.controls.DropTargetIndicator._template);
+ $(document.body).append(this.element);
+ };
+
+ lm.controls.DropTargetIndicator._template = '<div class="lm_dropTargetIndicator"><div class="lm_inner"></div></div>';
+
+ lm.utils.copy(lm.controls.DropTargetIndicator.prototype, {
+ destroy: function () {
+ this.element.remove();
+ },
+
+ highlight: function (x1, y1, x2, y2) {
+ this.highlightArea({ x1: x1, y1: y1, x2: x2, y2: y2 });
+ },
+
+ highlightArea: function (area) {
+ this.element.css({
+ left: area.x1,
+ top: area.y1,
+ width: area.x2 - area.x1,
+ height: area.y2 - area.y1
+ }).show();
+ },
+
+ hide: function () {
+ this.element.hide();
+ }
+ });
+ /**
+ * This class represents a header above a Stack ContentItem.
+ *
+ * @param {lm.LayoutManager} layoutManager
+ * @param {lm.item.AbstractContentItem} parent
+ */
+ lm.controls.Header = function (layoutManager, parent) {
+ lm.utils.EventEmitter.call(this);
+
+ this.layoutManager = layoutManager;
+ this.element = $(lm.controls.Header._template);
+
+ if (this.layoutManager.config.settings.selectionEnabled === true) {
+ this.element.addClass('lm_selectable');
+ this.element.on('click touchstart', lm.utils.fnBind(this._onHeaderClick, this));
+ }
+
+ this.tabsContainer = this.element.find('.lm_tabs');
+ this.tabDropdownContainer = this.element.find('.lm_tabdropdown_list');
+ this.tabDropdownContainer.hide();
+ this.controlsContainer = this.element.find('.lm_controls');
+ this.parent = parent;
+ this.parent.on('resize', this._updateTabSizes, this);
+ this.tabs = [];
+ this.activeContentItem = null;
+ this.closeButton = null;
+ this.tabDropdownButton = null;
+ this.hideAdditionalTabsDropdown = lm.utils.fnBind(this._hideAdditionalTabsDropdown, this);
+ $(document).mouseup(this.hideAdditionalTabsDropdown);
+
+ this._lastVisibleTabIndex = -1;
+ this._tabControlOffset = this.layoutManager.config.settings.tabControlOffset;
+ this._createControls();
+ };
+
+ lm.controls.Header._template = [
+ '<div class="lm_header">',
+ '<ul class="lm_tabs"></ul>',
+ '<ul class="lm_controls"></ul>',
+ '<ul class="lm_tabdropdown_list"></ul>',
+ '</div>'
+ ].join('');
+
+ lm.utils.copy(lm.controls.Header.prototype, {
+
+ /**
+ * Creates a new tab and associates it with a contentItem
+ *
+ * @param {lm.item.AbstractContentItem} contentItem
+ * @param {Integer} index The position of the tab
+ *
+ * @returns {void}
+ */
+ createTab: function (contentItem, index) {
+ var tab, i;
+
+ //If there's already a tab relating to the
+ //content item, don't do anything
+ for (i = 0; i < this.tabs.length; i++) {
+ if (this.tabs[i].contentItem === contentItem) {
+ return;
+ }
+ }
+
+ tab = new lm.controls.Tab(this, contentItem);
+
+ if (this.tabs.length === 0) {
+ this.tabs.push(tab);
+ this.tabsContainer.append(tab.element);
+ return;
+ }
+
+ if (index === undefined) {
+ index = this.tabs.length;
+ }
+
+ if (index > 0) {
+ this.tabs[index - 1].element.after(tab.element);
+ } else {
+ this.tabs[0].element.before(tab.element);
+ }
+
+ this.tabs.splice(index, 0, tab);
+ this._updateTabSizes();
+ },
+
+ /**
+ * Finds a tab based on the contentItem its associated with and removes it.
+ *
+ * @param {lm.item.AbstractContentItem} contentItem
+ *
+ * @returns {void}
+ */
+ removeTab: function (contentItem) {
+ for (var i = 0; i < this.tabs.length; i++) {
+ if (this.tabs[i].contentItem === contentItem) {
+ this.tabs[i]._$destroy();
+ this.tabs.splice(i, 1);
+ return;
+ }
+ }
+
+ throw new Error('contentItem is not controlled by this header');
+ },
+
+ /**
+ * The programmatical equivalent of clicking a Tab.
+ *
+ * @param {lm.item.AbstractContentItem} contentItem
+ */
+ setActiveContentItem: function (contentItem) {
+ var i, j, isActive, activeTab;
+
+ for (i = 0; i < this.tabs.length; i++) {
+ isActive = this.tabs[i].contentItem === contentItem;
+ this.tabs[i].setActive(isActive);
+ if (isActive === true) {
+ this.activeContentItem = contentItem;
+ this.parent.config.activeItemIndex = i;
+ }
+ }
+
+ if (this.layoutManager.config.settings.reorderOnTabMenuClick) {
+ /**
+ * If the tab selected was in the dropdown, move everything down one to make way for this one to be the first.
+ * This will make sure the most used tabs stay visible.
+ */
+ if (this._lastVisibleTabIndex !== -1 && this.parent.config.activeItemIndex > this._lastVisibleTabIndex) {
+ activeTab = this.tabs[this.parent.config.activeItemIndex];
+ for (j = this.parent.config.activeItemIndex; j > 0; j--) {
+ this.tabs[j] = this.tabs[j - 1];
+ }
+ this.tabs[0] = activeTab;
+ this.parent.config.activeItemIndex = 0;
+ }
+ }
+
+ this._updateTabSizes();
+ this.parent.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Programmatically operate with header position.
+ *
+ * @param {string} position one of ('top','left','right','bottom') to set or empty to get it.
+ *
+ * @returns {string} previous header position
+ */
+ position: function (position) {
+ var previous = this.parent._header.show;
+ if (previous && !this.parent._side)
+ previous = 'top';
+ if (position !== undefined && this.parent._header.show != position) {
+ this.parent._header.show = position;
+ this.parent._setupHeaderPosition();
+ }
+ return previous;
+ },
+
+ /**
+ * Programmatically set closability.
+ *
+ * @package private
+ * @param {Boolean} isClosable Whether to enable/disable closability.
+ *
+ * @returns {Boolean} Whether the action was successful
+ */
+ _$setClosable: function (isClosable) {
+ if (this.closeButton && this._isClosable()) {
+ this.closeButton.element[isClosable ? "show" : "hide"]();
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Destroys the entire header
+ *
+ * @package private
+ *
+ * @returns {void}
+ */
+ _$destroy: function () {
+ this.emit('destroy', this);
+
+ for (var i = 0; i < this.tabs.length; i++) {
+ this.tabs[i]._$destroy();
+ }
+ $(document).off('mouseup', this.hideAdditionalTabsDropdown);
+ this.element.remove();
+ },
+
+ /**
+ * get settings from header
+ *
+ * @returns {string} when exists
+ */
+ _getHeaderSetting: function (name) {
+ if (name in this.parent._header)
+ return this.parent._header[name];
+ },
+ /**
+ * Creates the popout, maximise and close buttons in the header's top right corner
+ *
+ * @returns {void}
+ */
+ _createControls: function () {
+ var closeStack,
+ popout,
+ label,
+ maximiseLabel,
+ minimiseLabel,
+ maximise,
+ maximiseButton,
+ tabDropdownLabel,
+ showTabDropdown;
+
+ /**
+ * Dropdown to show additional tabs.
+ */
+ showTabDropdown = lm.utils.fnBind(this._showAdditionalTabsDropdown, this);
+ tabDropdownLabel = this.layoutManager.config.labels.tabDropdown;
+ this.tabDropdownButton = new lm.controls.HeaderButton(this, tabDropdownLabel, 'lm_tabdropdown', showTabDropdown);
+ this.tabDropdownButton.element.hide();
+
+ /**
+ * Popout control to launch component in new window.
+ */
+ if (this._getHeaderSetting('popout')) {
+ popout = lm.utils.fnBind(this._onPopoutClick, this);
+ label = this._getHeaderSetting('popout');
+ new lm.controls.HeaderButton(this, label, 'lm_popout', popout);
+ }
+
+ /**
+ * Maximise control - set the component to the full size of the layout
+ */
+ if (this._getHeaderSetting('maximise')) {
+ maximise = lm.utils.fnBind(this.parent.toggleMaximise, this.parent);
+ maximiseLabel = this._getHeaderSetting('maximise');
+ minimiseLabel = this._getHeaderSetting('minimise');
+ maximiseButton = new lm.controls.HeaderButton(this, maximiseLabel, 'lm_maximise', maximise);
+
+ this.parent.on('maximised', function () {
+ maximiseButton.element.attr('title', minimiseLabel);
+ });
+
+ this.parent.on('minimised', function () {
+ maximiseButton.element.attr('title', maximiseLabel);
+ });
+ }
+
+ /**
+ * Close button
+ */
+ if (this._isClosable()) {
+ closeStack = lm.utils.fnBind(this.parent.remove, this.parent);
+ label = this._getHeaderSetting('close');
+ this.closeButton = new lm.controls.HeaderButton(this, label, 'lm_close', closeStack);
+ }
+ },
+
+ /**
+ * Shows drop down for additional tabs when there are too many to display.
+ *
+ * @returns {void}
+ */
+ _showAdditionalTabsDropdown: function () {
+ this.tabDropdownContainer.show();
+ },
+
+ /**
+ * Hides drop down for additional tabs when there are too many to display.
+ *
+ * @returns {void}
+ */
+ _hideAdditionalTabsDropdown: function (e) {
+ this.tabDropdownContainer.hide();
+ },
+
+ /**
+ * Checks whether the header is closable based on the parent config and
+ * the global config.
+ *
+ * @returns {Boolean} Whether the header is closable.
+ */
+ _isClosable: function () {
+ return this.parent.config.isClosable && this.layoutManager.config.settings.showCloseIcon;
+ },
+
+ _onPopoutClick: function () {
+ if (this.layoutManager.config.settings.popoutWholeStack === true) {
+ this.parent.popout();
+ } else {
+ this.activeContentItem.popout();
+ }
+ },
+
+
+ /**
+ * Invoked when the header's background is clicked (not it's tabs or controls)
+ *
+ * @param {jQuery DOM event} event
+ *
+ * @returns {void}
+ */
+ _onHeaderClick: function (event) {
+ if (event.target === this.element[0]) {
+ this.parent.select();
+ }
+ },
+
+ /**
+ * Pushes the tabs to the tab dropdown if the available space is not sufficient
+ *
+ * @returns {void}
+ */
+ _updateTabSizes: function (showTabMenu) {
+ if (this.tabs.length === 0) {
+ return;
+ }
+
+ //Show the menu based on function argument
+ this.tabDropdownButton.element.toggle(showTabMenu === true);
+
+ var size = function (val) {
+ return val ? 'width' : 'height';
+ };
+ this.element.css(size(!this.parent._sided), '');
+ this.element[size(this.parent._sided)](this.layoutManager.config.dimensions.headerHeight);
+ var availableWidth = this.element.outerWidth() - this.controlsContainer.outerWidth() - this._tabControlOffset,
+ cumulativeTabWidth = 0,
+ visibleTabWidth = 0,
+ tabElement,
+ i,
+ j,
+ marginLeft,
+ overlap = 0,
+ tabWidth,
+ tabOverlapAllowance = this.layoutManager.config.settings.tabOverlapAllowance,
+ tabOverlapAllowanceExceeded = false,
+ activeIndex = (this.activeContentItem ? this.tabs.indexOf(this.activeContentItem.tab) : 0),
+ activeTab = this.tabs[activeIndex];
+ if (this.parent._sided)
+ availableWidth = this.element.outerHeight() - this.controlsContainer.outerHeight() - this._tabControlOffset;
+ this._lastVisibleTabIndex = -1;
+
+ for (i = 0; i < this.tabs.length; i++) {
+ tabElement = this.tabs[i].element;
+
+ //Put the tab in the tabContainer so its true width can be checked
+ this.tabsContainer.append(tabElement);
+ tabWidth = tabElement.outerWidth() + parseInt(tabElement.css('margin-right'), 10);
+
+ cumulativeTabWidth += tabWidth;
+
+ //Include the active tab's width if it isn't already
+ //This is to ensure there is room to show the active tab
+ if (activeIndex <= i) {
+ visibleTabWidth = cumulativeTabWidth;
+ } else {
+ visibleTabWidth = cumulativeTabWidth + activeTab.element.outerWidth() + parseInt(activeTab.element.css('margin-right'), 10);
+ }
+
+ // If the tabs won't fit, check the overlap allowance.
+ if (visibleTabWidth > availableWidth) {
+
+ //Once allowance is exceeded, all remaining tabs go to menu.
+ if (!tabOverlapAllowanceExceeded) {
+
+ //No overlap for first tab or active tab
+ //Overlap spreads among non-active, non-first tabs
+ if (activeIndex > 0 && activeIndex <= i) {
+ overlap = (visibleTabWidth - availableWidth) / (i - 1);
+ } else {
+ overlap = (visibleTabWidth - availableWidth) / i;
+ }
+
+ //Check overlap against allowance.
+ if (overlap < tabOverlapAllowance) {
+ for (j = 0; j <= i; j++) {
+ marginLeft = (j !== activeIndex && j !== 0) ? '-' + overlap + 'px' : '';
+ this.tabs[j].element.css({ 'z-index': i - j, 'margin-left': marginLeft });
+ }
+ this._lastVisibleTabIndex = i;
+ this.tabsContainer.append(tabElement);
+ } else {
+ tabOverlapAllowanceExceeded = true;
+ }
+
+ } else if (i === activeIndex) {
+ //Active tab should show even if allowance exceeded. (We left room.)
+ tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+ this.tabsContainer.append(tabElement);
+ }
+
+ if (tabOverlapAllowanceExceeded && i !== activeIndex) {
+ if (showTabMenu) {
+ //Tab menu already shown, so we just add to it.
+ tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+ this.tabDropdownContainer.append(tabElement);
+ } else {
+ //We now know the tab menu must be shown, so we have to recalculate everything.
+ this._updateTabSizes(true);
+ return;
+ }
+ }
+
+ }
+ else {
+ this._lastVisibleTabIndex = i;
+ tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+ this.tabsContainer.append(tabElement);
+ }
+ }
+
+ }
+ });
+
+
+ lm.controls.HeaderButton = function (header, label, cssClass, action) {
+ this._header = header;
+ this.element = $('<li class="' + cssClass + '" title="' + label + '"></li>');
+ this._header.on('destroy', this._$destroy, this);
+ this._action = action;
+ this.element.on('click touchstart', this._action);
+ this._header.controlsContainer.append(this.element);
+ };
+
+ lm.utils.copy(lm.controls.HeaderButton.prototype, {
+ _$destroy: function () {
+ this.element.off();
+ this.element.remove();
+ }
+ });
+ lm.controls.Splitter = function (isVertical, size, grabSize) {
+ this._isVertical = isVertical;
+ this._size = size;
+ this._grabSize = grabSize < size ? size : grabSize;
+
+ this.element = this._createElement();
+ this._dragListener = new lm.utils.DragListener(this.element);
+ };
+
+ lm.utils.copy(lm.controls.Splitter.prototype, {
+ on: function (event, callback, context) {
+ this._dragListener.on(event, callback, context);
+ },
+
+ _$destroy: function () {
+ this.element.remove();
+ },
+
+ _createElement: function () {
+ var dragHandle = $('<div class="lm_drag_handle"></div>');
+ var element = $('<div class="lm_splitter"></div>');
+ element.append(dragHandle);
+
+ var handleExcessSize = this._grabSize - this._size;
+ var handleExcessPos = handleExcessSize / 2;
+
+ if (this._isVertical) {
+ dragHandle.css('top', -handleExcessPos);
+ dragHandle.css('height', this._size + handleExcessSize);
+ element.addClass('lm_vertical');
+ element['height'](this._size);
+ } else {
+ dragHandle.css('left', -handleExcessPos);
+ dragHandle.css('width', this._size + handleExcessSize);
+ element.addClass('lm_horizontal');
+ element['width'](this._size);
+ }
+
+ return element;
+ }
+ });
+
+ /**
+ * Represents an individual tab within a Stack's header
+ *
+ * @param {lm.controls.Header} header
+ * @param {lm.items.AbstractContentItem} contentItem
+ *
+ * @constructor
+ */
+ lm.controls.Tab = function (header, contentItem) {
+ this.header = header;
+ this.contentItem = contentItem;
+ this.element = $(lm.controls.Tab._template);
+ this.titleElement = this.element.find('.lm_title');
+ this.closeElement = this.element.find('.lm_close_tab');
+ this.closeElement[contentItem.config.isClosable ? 'show' : 'hide']();
+ this.isActive = false;
+
+ this.setTitle(contentItem.config.title);
+ this.contentItem.on('titleChanged', this.setTitle, this);
+
+ this._layoutManager = this.contentItem.layoutManager;
+
+ if (
+ this._layoutManager.config.settings.reorderEnabled === true &&
+ contentItem.config.reorderEnabled === true
+ ) {
+ this._dragListener = new lm.utils.DragListener(this.element);
+ this._dragListener.on('dragStart', this._onDragStart, this);
+ this.contentItem.on('destroy', this._dragListener.destroy, this._dragListener);
+ }
+
+ this._onTabClickFn = lm.utils.fnBind(this._onTabClick, this);
+ this._onCloseClickFn = lm.utils.fnBind(this._onCloseClick, this);
+
+ this.element.on('mousedown touchstart', this._onTabClickFn);
+
+ if (this.contentItem.config.isClosable) {
+ this.closeElement.on('click touchstart', this._onCloseClickFn);
+ this.closeElement.on('mousedown', this._onCloseMousedown);
+ } else {
+ this.closeElement.remove();
+ }
+
+ this.contentItem.tab = this;
+ this.contentItem.emit('tab', this);
+ this.contentItem.layoutManager.emit('tabCreated', this);
+
+ if (this.contentItem.isComponent) {
+ this.contentItem.container.tab = this;
+ this.contentItem.container.emit('tab', this);
+ }
+ };
+
+ /**
+ * The tab's html template
+ *
+ * @type {String}
+ */
+ lm.controls.Tab._template = '<li class="lm_tab"><i class="lm_left"></i>' +
+ '<span class="lm_title"></span><div class="lm_close_tab"></div>' +
+ '<i class="lm_right"></i></li>';
+
+ lm.utils.copy(lm.controls.Tab.prototype, {
+
+ /**
+ * Sets the tab's title to the provided string and sets
+ * its title attribute to a pure text representation (without
+ * html tags) of the same string.
+ *
+ * @public
+ * @param {String} title can contain html
+ */
+ setTitle: function (title) {
+ this.element.attr('title', lm.utils.stripTags(title));
+ this.titleElement.html(title);
+ },
+
+ /**
+ * Sets this tab's active state. To programmatically
+ * switch tabs, use header.setActiveContentItem( item ) instead.
+ *
+ * @public
+ * @param {Boolean} isActive
+ */
+ setActive: function (isActive) {
+ if (isActive === this.isActive) {
+ return;
+ }
+ this.isActive = isActive;
+
+ if (isActive) {
+ this.element.addClass('lm_active');
+ } else {
+ this.element.removeClass('lm_active');
+ }
+ },
+
+ /**
+ * Destroys the tab
+ *
+ * @private
+ * @returns {void}
+ */
+ _$destroy: function () {
+ this._layoutManager.emit('tabDestroyed', this);
+ this.element.off('mousedown touchstart', this._onTabClickFn);
+ this.closeElement.off('click touchstart', this._onCloseClickFn);
+ if (this._dragListener) {
+ this.contentItem.off('destroy', this._dragListener.destroy, this._dragListener);
+ this._dragListener.off('dragStart', this._onDragStart);
+ this._dragListener = null;
+ }
+ this.element.remove();
+ },
+
+ /**
+ * Callback for the DragListener
+ *
+ * @param {Number} x The tabs absolute x position
+ * @param {Number} y The tabs absolute y position
+ *
+ * @private
+ * @returns {void}
+ */
+ _onDragStart: function (x, y) {
+ if (this.contentItem.parent.isMaximised === true) {
+ this.contentItem.parent.toggleMaximise();
+ }
+ new lm.controls.DragProxy(
+ x,
+ y,
+ this._dragListener,
+ this._layoutManager,
+ this.contentItem,
+ this.header.parent
+ );
+ },
+
+ /**
+ * Callback when the tab is clicked
+ *
+ * @param {jQuery DOM event} event
+ *
+ * @private
+ * @returns {void}
+ */
+ _onTabClick: function (event) {
+ // left mouse button or tap
+ if (event.button === 0 || event.type === 'touchstart') {
+ var activeContentItem = this.header.parent.getActiveContentItem();
+ if (this.contentItem !== activeContentItem) {
+ this.header.parent.setActiveContentItem(this.contentItem);
+ }
+
+ // middle mouse button
+ } else if (event.button === 1 && this.contentItem.config.isClosable) {
+ this._onCloseClick(event);
+ }
+ },
+
+ /**
+ * Callback when the tab's close button is
+ * clicked
+ *
+ * @param {jQuery DOM event} event
+ *
+ * @private
+ * @returns {void}
+ */
+ _onCloseClick: function (event) {
+ event.stopPropagation();
+ this.header.parent.removeChild(this.contentItem);
+ },
+
+
+ /**
+ * Callback to capture tab close button mousedown
+ * to prevent tab from activating.
+ *
+ * @param (jQuery DOM event) event
+ *
+ * @private
+ * @returns {void}
+ */
+ _onCloseMousedown: function (event) {
+ event.stopPropagation();
+ }
+ });
+
+ lm.controls.TransitionIndicator = function () {
+ this._element = $('<div class="lm_transition_indicator"></div>');
+ $(document.body).append(this._element);
+
+ this._toElement = null;
+ this._fromDimensions = null;
+ this._totalAnimationDuration = 200;
+ this._animationStartTime = null;
+ };
+
+ lm.utils.copy(lm.controls.TransitionIndicator.prototype, {
+ destroy: function () {
+ this._element.remove();
+ },
+
+ transitionElements: function (fromElement, toElement) {
+ /**
+ * TODO - This is not quite as cool as expected. Review.
+ */
+ return;
+ this._toElement = toElement;
+ this._animationStartTime = lm.utils.now();
+ this._fromDimensions = this._measure(fromElement);
+ this._fromDimensions.opacity = 0.8;
+ this._element.show().css(this._fromDimensions);
+ lm.utils.animFrame(lm.utils.fnBind(this._nextAnimationFrame, this));
+ },
+
+ _nextAnimationFrame: function () {
+ var toDimensions = this._measure(this._toElement),
+ animationProgress = (lm.utils.now() - this._animationStartTime) / this._totalAnimationDuration,
+ currentFrameStyles = {},
+ cssProperty;
+
+ if (animationProgress >= 1) {
+ this._element.hide();
+ return;
+ }
+
+ toDimensions.opacity = 0;
+
+ for (cssProperty in this._fromDimensions) {
+ currentFrameStyles[cssProperty] = this._fromDimensions[cssProperty] +
+ (toDimensions[cssProperty] - this._fromDimensions[cssProperty]) *
+ animationProgress;
+ }
+
+ this._element.css(currentFrameStyles);
+ lm.utils.animFrame(lm.utils.fnBind(this._nextAnimationFrame, this));
+ },
+
+ _measure: function (element) {
+ var offset = element.offset();
+
+ return {
+ left: offset.left,
+ top: offset.top,
+ width: element.outerWidth(),
+ height: element.outerHeight()
+ };
+ }
+ });
+ lm.errors.ConfigurationError = function (message, node) {
+ Error.call(this);
+
+ this.name = 'Configuration Error';
+ this.message = message;
+ this.node = node;
+ };
+
+ lm.errors.ConfigurationError.prototype = new Error();
+
+ /**
+ * This is the baseclass that all content items inherit from.
+ * Most methods provide a subset of what the sub-classes do.
+ *
+ * It also provides a number of functions for tree traversal
+ *
+ * @param {lm.LayoutManager} layoutManager
+ * @param {item node configuration} config
+ * @param {lm.item} parent
+ *
+ * @event stateChanged
+ * @event beforeItemDestroyed
+ * @event itemDestroyed
+ * @event itemCreated
+ * @event componentCreated
+ * @event rowCreated
+ * @event columnCreated
+ * @event stackCreated
+ *
+ * @constructor
+ */
+ lm.items.AbstractContentItem = function (layoutManager, config, parent) {
+ lm.utils.EventEmitter.call(this);
+
+ this.config = this._extendItemNode(config);
+ this.type = config.type;
+ this.contentItems = [];
+ this.parent = parent;
+
+ this.isInitialised = false;
+ this.isMaximised = false;
+ this.isRoot = false;
+ this.isRow = false;
+ this.isColumn = false;
+ this.isStack = false;
+ this.isComponent = false;
+
+ this.layoutManager = layoutManager;
+ this._pendingEventPropagations = {};
+ this._throttledEvents = ['stateChanged'];
+
+ this.on(lm.utils.EventEmitter.ALL_EVENT, this._propagateEvent, this);
+
+ if (config.content) {
+ this._createContentItems(config);
+ }
+ };
+
+ lm.utils.copy(lm.items.AbstractContentItem.prototype, {
+
+ /**
+ * Set the size of the component and its children, called recursively
+ *
+ * @abstract
+ * @returns void
+ */
+ setSize: function () {
+ throw new Error('Abstract Method');
+ },
+
+ /**
+ * Calls a method recursively downwards on the tree
+ *
+ * @param {String} functionName the name of the function to be called
+ * @param {[Array]}functionArguments optional arguments that are passed to every function
+ * @param {[bool]} bottomUp Call methods from bottom to top, defaults to false
+ * @param {[bool]} skipSelf Don't invoke the method on the class that calls it, defaults to false
+ *
+ * @returns {void}
+ */
+ callDownwards: function (functionName, functionArguments, bottomUp, skipSelf) {
+ var i;
+
+ if (bottomUp !== true && skipSelf !== true) {
+ this[functionName].apply(this, functionArguments || []);
+ }
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.contentItems[i].callDownwards(functionName, functionArguments, bottomUp);
+ }
+ if (bottomUp === true && skipSelf !== true) {
+ this[functionName].apply(this, functionArguments || []);
+ }
+ },
+
+ /**
+ * Removes a child node (and its children) from the tree
+ *
+ * @param {lm.items.ContentItem} contentItem
+ *
+ * @returns {void}
+ */
+ removeChild: function (contentItem, keepChild) {
+
+ /*
+ * Get the position of the item that's to be removed within all content items this node contains
+ */
+ var index = lm.utils.indexOf(contentItem, this.contentItems);
+
+ /*
+ * Make sure the content item to be removed is actually a child of this item
+ */
+ if (index === -1) {
+ throw new Error('Can\'t remove child item. Unknown content item');
+ }
+
+ /**
+ * Call ._$destroy on the content item. This also calls ._$destroy on all its children
+ */
+ if (keepChild !== true) {
+ this.contentItems[index]._$destroy();
+ }
+
+ /**
+ * Remove the content item from this nodes array of children
+ */
+ this.contentItems.splice(index, 1);
+
+ /**
+ * Remove the item from the configuration
+ */
+ this.config.content.splice(index, 1);
+
+ /**
+ * If this node still contains other content items, adjust their size
+ */
+ if (this.contentItems.length > 0) {
+ this.callDownwards('setSize');
+
+ /**
+ * If this was the last content item, remove this node as well
+ */
+ } else if (!(this instanceof lm.items.Root) && this.config.isClosable === true) {
+ this.parent.removeChild(this);
+ }
+ },
+
+ /**
+ * Sets up the tree structure for the newly added child
+ * The responsibility for the actual DOM manipulations lies
+ * with the concrete item
+ *
+ * @param {lm.items.AbstractContentItem} contentItem
+ * @param {[Int]} index If omitted item will be appended
+ */
+ addChild: function (contentItem, index) {
+ if (index === undefined) {
+ index = this.contentItems.length;
+ }
+
+ this.contentItems.splice(index, 0, contentItem);
+
+ if (this.config.content === undefined) {
+ this.config.content = [];
+ }
+
+ this.config.content.splice(index, 0, contentItem.config);
+ contentItem.parent = this;
+
+ if (contentItem.parent.isInitialised === true && contentItem.isInitialised === false) {
+ contentItem._$init();
+ }
+ },
+
+ /**
+ * Replaces oldChild with newChild. This used to use jQuery.replaceWith... which for
+ * some reason removes all event listeners, so isn't really an option.
+ *
+ * @param {lm.item.AbstractContentItem} oldChild
+ * @param {lm.item.AbstractContentItem} newChild
+ *
+ * @returns {void}
+ */
+ replaceChild: function (oldChild, newChild, _$destroyOldChild) {
+
+ newChild = this.layoutManager._$normalizeContentItem(newChild);
+
+ var index = lm.utils.indexOf(oldChild, this.contentItems),
+ parentNode = oldChild.element[0].parentNode;
+
+ if (index === -1) {
+ throw new Error('Can\'t replace child. oldChild is not child of this');
+ }
+
+ parentNode.replaceChild(newChild.element[0], oldChild.element[0]);
+
+ /*
+ * Optionally destroy the old content item
+ */
+ if (_$destroyOldChild === true) {
+ oldChild.parent = null;
+ oldChild._$destroy();
+ }
+
+ /*
+ * Wire the new contentItem into the tree
+ */
+ this.contentItems[index] = newChild;
+ newChild.parent = this;
+
+ /*
+ * Update tab reference
+ */
+ if (this.isStack) {
+ this.header.tabs[index].contentItem = newChild;
+ }
+
+ //TODO This doesn't update the config... refactor to leave item nodes untouched after creation
+ if (newChild.parent.isInitialised === true && newChild.isInitialised === false) {
+ newChild._$init();
+ }
+
+ this.callDownwards('setSize');
+ },
+
+ /**
+ * Convenience method.
+ * Shorthand for this.parent.removeChild( this )
+ *
+ * @returns {void}
+ */
+ remove: function () {
+ this.parent.removeChild(this);
+ },
+
+ /**
+ * Removes the component from the layout and creates a new
+ * browser window with the component and its children inside
+ *
+ * @returns {lm.controls.BrowserPopout}
+ */
+ popout: function () {
+ var browserPopout = this.layoutManager.createPopout(this);
+ this.emitBubblingEvent('stateChanged');
+ return browserPopout;
+ },
+
+ /**
+ * Maximises the Item or minimises it if it is already maximised
+ *
+ * @returns {void}
+ */
+ toggleMaximise: function (e) {
+ e && e.preventDefault();
+ if (this.isMaximised === true) {
+ this.layoutManager._$minimiseItem(this);
+ } else {
+ this.layoutManager._$maximiseItem(this);
+ }
+
+ this.isMaximised = !this.isMaximised;
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Selects the item if it is not already selected
+ *
+ * @returns {void}
+ */
+ select: function () {
+ if (this.layoutManager.selectedItem !== this) {
+ this.layoutManager.selectItem(this, true);
+ this.element.addClass('lm_selected');
+ }
+ },
+
+ /**
+ * De-selects the item if it is selected
+ *
+ * @returns {void}
+ */
+ deselect: function () {
+ if (this.layoutManager.selectedItem === this) {
+ this.layoutManager.selectedItem = null;
+ this.element.removeClass('lm_selected');
+ }
+ },
+
+ /**
+ * Set this component's title
+ *
+ * @public
+ * @param {String} title
+ *
+ * @returns {void}
+ */
+ setTitle: function (title) {
+ this.config.title = title;
+ this.emit('titleChanged', title);
+ this.emit('stateChanged');
+ },
+
+ /**
+ * Checks whether a provided id is present
+ *
+ * @public
+ * @param {String} id
+ *
+ * @returns {Boolean} isPresent
+ */
+ hasId: function (id) {
+ if (!this.config.id) {
+ return false;
+ } else if (typeof this.config.id === 'string') {
+ return this.config.id === id;
+ } else if (this.config.id instanceof Array) {
+ return lm.utils.indexOf(id, this.config.id) !== -1;
+ }
+ },
+
+ /**
+ * Adds an id. Adds it as a string if the component doesn't
+ * have an id yet or creates/uses an array
+ *
+ * @public
+ * @param {String} id
+ *
+ * @returns {void}
+ */
+ addId: function (id) {
+ if (this.hasId(id)) {
+ return;
+ }
+
+ if (!this.config.id) {
+ this.config.id = id;
+ } else if (typeof this.config.id === 'string') {
+ this.config.id = [this.config.id, id];
+ } else if (this.config.id instanceof Array) {
+ this.config.id.push(id);
+ }
+ },
+
+ /**
+ * Removes an existing id. Throws an error
+ * if the id is not present
+ *
+ * @public
+ * @param {String} id
+ *
+ * @returns {void}
+ */
+ removeId: function (id) {
+ if (!this.hasId(id)) {
+ throw new Error('Id not found');
+ }
+
+ if (typeof this.config.id === 'string') {
+ delete this.config.id;
+ } else if (this.config.id instanceof Array) {
+ var index = lm.utils.indexOf(id, this.config.id);
+ this.config.id.splice(index, 1);
+ }
+ },
+
+ /****************************************
+ * SELECTOR
+ ****************************************/
+ getItemsByFilter: function (filter) {
+ var result = [],
+ next = function (contentItem) {
+ for (var i = 0; i < contentItem.contentItems.length; i++) {
+
+ if (filter(contentItem.contentItems[i]) === true) {
+ result.push(contentItem.contentItems[i]);
+ }
+
+ next(contentItem.contentItems[i]);
+ }
+ };
+
+ next(this);
+ return result;
+ },
+
+ getItemsById: function (id) {
+ return this.getItemsByFilter(function (item) {
+ if (item.config.id instanceof Array) {
+ return lm.utils.indexOf(id, item.config.id) !== -1;
+ } else {
+ return item.config.id === id;
+ }
+ });
+ },
+
+ getItemsByType: function (type) {
+ return this._$getItemsByProperty('type', type);
+ },
+
+ getComponentsByName: function (componentName) {
+ var components = this._$getItemsByProperty('componentName', componentName),
+ instances = [],
+ i;
+
+ for (i = 0; i < components.length; i++) {
+ instances.push(components[i].instance);
+ }
+
+ return instances;
+ },
+
+ /****************************************
+ * PACKAGE PRIVATE
+ ****************************************/
+ _$getItemsByProperty: function (key, value) {
+ return this.getItemsByFilter(function (item) {
+ return item[key] === value;
+ });
+ },
+
+ _$setParent: function (parent) {
+ this.parent = parent;
+ },
+
+ _$highlightDropZone: function (x, y, area) {
+ this.layoutManager.dropTargetIndicator.highlightArea(area);
+ },
+
+ _$onDrop: function (contentItem) {
+ this.addChild(contentItem);
+ },
+
+ _$hide: function () {
+ this._callOnActiveComponents('hide');
+ this.element.hide();
+ this.layoutManager.updateSize();
+ },
+
+ _$show: function () {
+ this._callOnActiveComponents('show');
+ this.element.show();
+ this.layoutManager.updateSize();
+ },
+
+ _callOnActiveComponents: function (methodName) {
+ var stacks = this.getItemsByType('stack'),
+ activeContentItem,
+ i;
+
+ for (i = 0; i < stacks.length; i++) {
+ activeContentItem = stacks[i].getActiveContentItem();
+
+ if (activeContentItem && activeContentItem.isComponent) {
+ activeContentItem.container[methodName]();
+ }
+ }
+ },
+
+ /**
+ * Destroys this item ands its children
+ *
+ * @returns {void}
+ */
+ _$destroy: function () {
+ this.emitBubblingEvent('beforeItemDestroyed');
+ this.callDownwards('_$destroy', [], true, true);
+ this.element.remove();
+ this.emitBubblingEvent('itemDestroyed');
+ },
+
+ /**
+ * Returns the area the component currently occupies in the format
+ *
+ * {
+ * x1: int
+ * xy: int
+ * y1: int
+ * y2: int
+ * contentItem: contentItem
+ * }
+ */
+ _$getArea: function (element) {
+ element = element || this.element;
+
+ var offset = element.offset(),
+ width = element.width(),
+ height = element.height();
+
+ return {
+ x1: offset.left,
+ y1: offset.top,
+ x2: offset.left + width,
+ y2: offset.top + height,
+ surface: width * height,
+ contentItem: this
+ };
+ },
+
+ /**
+ * The tree of content items is created in two steps: First all content items are instantiated,
+ * then init is called recursively from top to bottem. This is the basic init function,
+ * it can be used, extended or overwritten by the content items
+ *
+ * Its behaviour depends on the content item
+ *
+ * @package private
+ *
+ * @returns {void}
+ */
+ _$init: function () {
+ var i;
+ this.setSize();
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.childElementContainer.append(this.contentItems[i].element);
+ }
+
+ this.isInitialised = true;
+ this.emitBubblingEvent('itemCreated');
+ this.emitBubblingEvent(this.type + 'Created');
+ },
+
+ /**
+ * Emit an event that bubbles up the item tree.
+ *
+ * @param {String} name The name of the event
+ *
+ * @returns {void}
+ */
+ emitBubblingEvent: function (name) {
+ var event = new lm.utils.BubblingEvent(name, this);
+ this.emit(name, event);
+ },
+
+ /**
+ * Private method, creates all content items for this node at initialisation time
+ * PLEASE NOTE, please see addChild for adding contentItems add runtime
+ * @private
+ * @param {configuration item node} config
+ *
+ * @returns {void}
+ */
+ _createContentItems: function (config) {
+ var oContentItem, i;
+
+ if (!(config.content instanceof Array)) {
+ throw new lm.errors.ConfigurationError('content must be an Array', config);
+ }
+
+ for (i = 0; i < config.content.length; i++) {
+ oContentItem = this.layoutManager.createContentItem(config.content[i], this);
+ this.contentItems.push(oContentItem);
+ }
+ },
+
+ /**
+ * Extends an item configuration node with default settings
+ * @private
+ * @param {configuration item node} config
+ *
+ * @returns {configuration item node} extended config
+ */
+ _extendItemNode: function (config) {
+
+ for (var key in lm.config.itemDefaultConfig) {
+ if (config[key] === undefined) {
+ config[key] = lm.config.itemDefaultConfig[key];
+ }
+ }
+
+ return config;
+ },
+
+ /**
+ * Called for every event on the item tree. Decides whether the event is a bubbling
+ * event and propagates it to its parent
+ *
+ * @param {String} name the name of the event
+ * @param {lm.utils.BubblingEvent} event
+ *
+ * @returns {void}
+ */
+ _propagateEvent: function (name, event) {
+ if (event instanceof lm.utils.BubblingEvent &&
+ event.isPropagationStopped === false &&
+ this.isInitialised === true) {
+
+ /**
+ * In some cases (e.g. if an element is created from a DragSource) it
+ * doesn't have a parent and is not below root. If that's the case
+ * propagate the bubbling event from the top level of the substree directly
+ * to the layoutManager
+ */
+ if (this.isRoot === false && this.parent) {
+ this.parent.emit.apply(this.parent, Array.prototype.slice.call(arguments, 0));
+ } else {
+ this._scheduleEventPropagationToLayoutManager(name, event);
+ }
+ }
+ },
+
+ /**
+ * All raw events bubble up to the root element. Some events that
+ * are propagated to - and emitted by - the layoutManager however are
+ * only string-based, batched and sanitized to make them more usable
+ *
+ * @param {String} name the name of the event
+ *
+ * @private
+ * @returns {void}
+ */
+ _scheduleEventPropagationToLayoutManager: function (name, event) {
+ if (lm.utils.indexOf(name, this._throttledEvents) === -1) {
+ this.layoutManager.emit(name, event.origin);
+ } else {
+ if (this._pendingEventPropagations[name] !== true) {
+ this._pendingEventPropagations[name] = true;
+ lm.utils.animFrame(lm.utils.fnBind(this._propagateEventToLayoutManager, this, [name, event]));
+ }
+ }
+
+ },
+
+ /**
+ * Callback for events scheduled by _scheduleEventPropagationToLayoutManager
+ *
+ * @param {String} name the name of the event
+ *
+ * @private
+ * @returns {void}
+ */
+ _propagateEventToLayoutManager: function (name, event) {
+ this._pendingEventPropagations[name] = false;
+ this.layoutManager.emit(name, event);
+ }
+ });
+
+ /**
+ * @param {[type]} layoutManager [description]
+ * @param {[type]} config [description]
+ * @param {[type]} parent [description]
+ */
+ lm.items.Component = function (layoutManager, config, parent) {
+ lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+ var ComponentConstructor = layoutManager.getComponent(this.config.componentName),
+ componentConfig = $.extend(true, {}, this.config.componentState || {});
+
+ componentConfig.componentName = this.config.componentName;
+ this.componentName = this.config.componentName;
+
+ if (this.config.title === '') {
+ this.config.title = this.config.componentName;
+ }
+
+ this.isComponent = true;
+ this.container = new lm.container.ItemContainer(this.config, this, layoutManager);
+ this.instance = new ComponentConstructor(this.container, componentConfig);
+ this.element = this.container._element;
+ };
+
+ lm.utils.extend(lm.items.Component, lm.items.AbstractContentItem);
+
+ lm.utils.copy(lm.items.Component.prototype, {
+
+ close: function () {
+ this.parent.removeChild(this);
+ },
+
+ setSize: function () {
+ if (this.element.is(':visible')) {
+ // Do not update size of hidden components to prevent unwanted reflows
+ this.container._$setSize(this.element.width(), this.element.height());
+ }
+ },
+
+ _$init: function () {
+ lm.items.AbstractContentItem.prototype._$init.call(this);
+ this.container.emit('open');
+ },
+
+ _$hide: function () {
+ this.container.hide();
+ lm.items.AbstractContentItem.prototype._$hide.call(this);
+ },
+
+ _$show: function () {
+ this.container.show();
+ lm.items.AbstractContentItem.prototype._$show.call(this);
+ },
+
+ _$shown: function () {
+ this.container.shown();
+ lm.items.AbstractContentItem.prototype._$shown.call(this);
+ },
+
+ _$destroy: function () {
+ this.container.emit('destroy', this);
+ lm.items.AbstractContentItem.prototype._$destroy.call(this);
+ },
+
+ /**
+ * Dragging onto a component directly is not an option
+ *
+ * @returns null
+ */
+ _$getArea: function () {
+ return null;
+ }
+ });
+
+ lm.items.Root = function (layoutManager, config, containerElement) {
+ lm.items.AbstractContentItem.call(this, layoutManager, config, null);
+ this.isRoot = true;
+ this.type = 'root';
+ this.element = $('<div class="lm_goldenlayout lm_item lm_root"></div>');
+ this.childElementContainer = this.element;
+ this._containerElement = containerElement;
+ this._containerElement.append(this.element);
+ };
+
+ lm.utils.extend(lm.items.Root, lm.items.AbstractContentItem);
+
+ lm.utils.copy(lm.items.Root.prototype, {
+ addChild: function (contentItem) {
+ if (this.contentItems.length > 0) {
+ throw new Error('Root node can only have a single child');
+ }
+
+ contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+ this.childElementContainer.append(contentItem.element);
+ lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem);
+
+ this.callDownwards('setSize');
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ setSize: function (width, height) {
+ width = (typeof width === 'undefined') ? this._containerElement.width() : width;
+ height = (typeof height === 'undefined') ? this._containerElement.height() : height;
+
+ this.element.width(width);
+ this.element.height(height);
+
+ /*
+ * Root can be empty
+ */
+ if (this.contentItems[0]) {
+ this.contentItems[0].element.width(width);
+ this.contentItems[0].element.height(height);
+ }
+ },
+ _$highlightDropZone: function (x, y, area) {
+ this.layoutManager.tabDropPlaceholder.remove();
+ lm.items.AbstractContentItem.prototype._$highlightDropZone.apply(this, arguments);
+ },
+
+ _$onDrop: function (contentItem, area) {
+ var stack;
+
+ if (contentItem.isComponent) {
+ stack = this.layoutManager.createContentItem({
+ type: 'stack',
+ header: contentItem.config.header || {}
+ }, this);
+ stack._$init();
+ stack.addChild(contentItem);
+ contentItem = stack;
+ }
+
+ if (!this.contentItems.length) {
+ this.addChild(contentItem);
+ } else {
+ var type = area.side[0] == 'x' ? 'row' : 'column';
+ var dimension = area.side[0] == 'x' ? 'width' : 'height';
+ var insertBefore = area.side[1] == '2';
+ var column = this.contentItems[0];
+ if (!column instanceof lm.items.RowOrColumn || column.type != type) {
+ var rowOrColumn = this.layoutManager.createContentItem({ type: type }, this);
+ this.replaceChild(column, rowOrColumn);
+ rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
+ rowOrColumn.addChild(column, insertBefore ? undefined : 0, true);
+ column.config[dimension] = 50;
+ contentItem.config[dimension] = 50;
+ rowOrColumn.callDownwards('setSize');
+ } else {
+ var sibbling = column.contentItems[insertBefore ? 0 : column.contentItems.length - 1]
+ column.addChild(contentItem, insertBefore ? 0 : undefined, true);
+ sibbling.config[dimension] *= 0.5;
+ contentItem.config[dimension] = sibbling.config[dimension];
+ column.callDownwards('setSize');
+ }
+ }
+ }
+ });
+
+
+
+ lm.items.RowOrColumn = function (isColumn, layoutManager, config, parent) {
+ lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+ this.isRow = !isColumn;
+ this.isColumn = isColumn;
+
+ this.element = $('<div class="lm_item lm_' + (isColumn ? 'column' : 'row') + '"></div>');
+ this.childElementContainer = this.element;
+ this._splitterSize = layoutManager.config.dimensions.borderWidth;
+ this._splitterGrabSize = layoutManager.config.dimensions.borderGrabWidth;
+ this._isColumn = isColumn;
+ this._dimension = isColumn ? 'height' : 'width';
+ this._splitter = [];
+ this._splitterPosition = null;
+ this._splitterMinPosition = null;
+ this._splitterMaxPosition = null;
+ };
+
+ lm.utils.extend(lm.items.RowOrColumn, lm.items.AbstractContentItem);
+
+ lm.utils.copy(lm.items.RowOrColumn.prototype, {
+
+ /**
+ * Add a new contentItem to the Row or Column
+ *
+ * @param {lm.item.AbstractContentItem} contentItem
+ * @param {[int]} index The position of the new item within the Row or Column.
+ * If no index is provided the item will be added to the end
+ * @param {[bool]} _$suspendResize If true the items won't be resized. This will leave the item in
+ * an inconsistent state and is only intended to be used if multiple
+ * children need to be added in one go and resize is called afterwards
+ *
+ * @returns {void}
+ */
+ addChild: function (contentItem, index, _$suspendResize) {
+
+ var newItemSize, itemSize, i, splitterElement;
+
+ contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+
+ if (index === undefined) {
+ index = this.contentItems.length;
+ }
+
+ if (this.contentItems.length > 0) {
+ splitterElement = this._createSplitter(Math.max(0, index - 1)).element;
+
+ if (index > 0) {
+ this.contentItems[index - 1].element.after(splitterElement);
+ splitterElement.after(contentItem.element);
+ } else {
+ this.contentItems[0].element.before(splitterElement);
+ splitterElement.before(contentItem.element);
+ }
+ } else {
+ this.childElementContainer.append(contentItem.element);
+ }
+
+ lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem, index);
+
+ let fixedItemSize = 0;
+ let variableItemCount = 0;
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config.fixed)
+ fixedItemSize += this.contentItems[i].config[this._dimension];
+ else variableItemCount++;
+ }
+
+ newItemSize = (1 / variableItemCount) * (100 - fixedItemSize);
+
+ if (_$suspendResize === true) {
+ this.emitBubblingEvent('stateChanged');
+ return;
+ }
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config.fixed)
+ ;
+ else if (this.contentItems[i] === contentItem) {
+ contentItem.config[this._dimension] = newItemSize;
+ } else {
+ itemSize = this.contentItems[i].config[this._dimension] *= (100 - newItemSize - fixedItemSize) / (100 - fixedItemSize);
+ this.contentItems[i].config[this._dimension] = itemSize;
+ }
+ }
+
+ this.callDownwards('setSize');
+ this.emitBubblingEvent('stateChanged');
+
+ },
+
+ /**
+ * Removes a child of this element
+ *
+ * @param {lm.items.AbstractContentItem} contentItem
+ * @param {boolean} keepChild If true the child will be removed, but not destroyed
+ *
+ * @returns {void}
+ */
+ removeChild: function (contentItem, keepChild) {
+ var removedItemSize = contentItem.config[this._dimension],
+ index = lm.utils.indexOf(contentItem, this.contentItems),
+ splitterIndex = Math.max(index - 1, 0),
+ i,
+ childItem;
+
+ if (index === -1) {
+ throw new Error('Can\'t remove child. ContentItem is not child of this Row or Column');
+ }
+
+ /**
+ * Remove the splitter before the item or after if the item happens
+ * to be the first in the row/column
+ */
+ if (this._splitter[splitterIndex]) {
+ this._splitter[splitterIndex]._$destroy();
+ this._splitter.splice(splitterIndex, 1);
+ }
+
+ let fixedItemSize = 0;
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config.fixed)
+ fixedItemSize += this.contentItems[i].config[this._dimension];
+ }
+ /**
+ * Allocate the space that the removed item occupied to the remaining items
+ */
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config.fixed)
+ ;
+ else if (this.contentItems[i] !== contentItem) {
+ this.contentItems[i].config[this._dimension] *= (100 - fixedItemSize) / (100 - removedItemSize - fixedItemSize);
+ }
+ }
+
+ lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
+
+ if (this.contentItems.length === 1 && this.config.isClosable === true) {
+ childItem = this.contentItems[0];
+ this.contentItems = [];
+ this.parent.replaceChild(this, childItem, true);
+ } else {
+ this.callDownwards('setSize');
+ this.emitBubblingEvent('stateChanged');
+ }
+ },
+
+ /**
+ * Replaces a child of this Row or Column with another contentItem
+ *
+ * @param {lm.items.AbstractContentItem} oldChild
+ * @param {lm.items.AbstractContentItem} newChild
+ *
+ * @returns {void}
+ */
+ replaceChild: function (oldChild, newChild) {
+ var size = oldChild.config[this._dimension];
+ lm.items.AbstractContentItem.prototype.replaceChild.call(this, oldChild, newChild);
+ newChild.config[this._dimension] = size;
+ this.callDownwards('setSize');
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Called whenever the dimensions of this item or one of its parents change
+ *
+ * @returns {void}
+ */
+ setSize: function () {
+ if (this.contentItems.length > 0) {
+ this._calculateRelativeSizes();
+ this._setAbsoluteSizes();
+ }
+ this.emitBubblingEvent('stateChanged');
+ this.emit('resize');
+ },
+
+ /**
+ * Invoked recursively by the layout manager. AbstractContentItem.init appends
+ * the contentItem's DOM elements to the container, RowOrColumn init adds splitters
+ * in between them
+ *
+ * @package private
+ * @override AbstractContentItem._$init
+ * @returns {void}
+ */
+ _$init: function () {
+ if (this.isInitialised === true) return;
+
+ var i;
+
+ lm.items.AbstractContentItem.prototype._$init.call(this);
+
+ for (i = 0; i < this.contentItems.length - 1; i++) {
+ this.contentItems[i].element.after(this._createSplitter(i).element);
+ }
+ },
+
+ /**
+ * Turns the relative sizes calculated by _calculateRelativeSizes into
+ * absolute pixel values and applies them to the children's DOM elements
+ *
+ * Assigns additional pixels to counteract Math.floor
+ *
+ * @private
+ * @returns {void}
+ */
+ _setAbsoluteSizes: function () {
+ var i,
+ sizeData = this._calculateAbsoluteSizes();
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (sizeData.additionalPixel - i > 0) {
+ sizeData.itemSizes[i]++;
+ }
+
+ if (this._isColumn) {
+ this.contentItems[i].element.width(sizeData.totalWidth);
+ this.contentItems[i].element.height(sizeData.itemSizes[i]);
+ } else {
+ this.contentItems[i].element.width(sizeData.itemSizes[i]);
+ this.contentItems[i].element.height(sizeData.totalHeight);
+ }
+ }
+ },
+
+ /**
+ * Calculates the absolute sizes of all of the children of this Item.
+ * @returns {object} - Set with absolute sizes and additional pixels.
+ */
+ _calculateAbsoluteSizes: function () {
+ var i,
+ totalSplitterSize = (this.contentItems.length - 1) * this._splitterSize,
+ totalWidth = this.element.width(),
+ totalHeight = this.element.height(),
+ totalAssigned = 0,
+ additionalPixel,
+ itemSize,
+ itemSizes = [];
+
+ if (this._isColumn) {
+ totalHeight -= totalSplitterSize;
+ } else {
+ totalWidth -= totalSplitterSize;
+ }
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this._isColumn) {
+ itemSize = Math.floor(totalHeight * (this.contentItems[i].config.height / 100));
+ } else {
+ itemSize = Math.floor(totalWidth * (this.contentItems[i].config.width / 100));
+ }
+
+ totalAssigned += itemSize;
+ itemSizes.push(itemSize);
+ }
+
+ additionalPixel = Math.floor((this._isColumn ? totalHeight : totalWidth) - totalAssigned);
+
+ return {
+ itemSizes: itemSizes,
+ additionalPixel: additionalPixel,
+ totalWidth: totalWidth,
+ totalHeight: totalHeight
+ };
+ },
+
+ /**
+ * Calculates the relative sizes of all children of this Item. The logic
+ * is as follows:
+ *
+ * - Add up the total size of all items that have a configured size
+ *
+ * - If the total == 100 (check for floating point errors)
+ * Excellent, job done
+ *
+ * - If the total is > 100,
+ * set the size of items without set dimensions to 1/3 and add this to the total
+ * set the size off all items so that the total is hundred relative to their original size
+ *
+ * - If the total is < 100
+ * If there are items without set dimensions, distribute the remainder to 100 evenly between them
+ * If there are no items without set dimensions, increase all items sizes relative to
+ * their original size so that they add up to 100
+ *
+ * @private
+ * @returns {void}
+ */
+ _calculateRelativeSizes: function () {
+
+ var i,
+ total = 0,
+ itemsWithoutSetDimension = [],
+ dimension = this._isColumn ? 'height' : 'width';
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config[dimension] !== undefined) {
+ total += this.contentItems[i].config[dimension];
+ } else {
+ itemsWithoutSetDimension.push(this.contentItems[i]);
+ }
+ }
+
+ /**
+ * Everything adds up to hundred, all good :-)
+ */
+ if (Math.round(total) === 100) {
+ this._respectMinItemWidth();
+ return;
+ }
+
+ /**
+ * Allocate the remaining size to the items without a set dimension
+ */
+ if (Math.round(total) < 100 && itemsWithoutSetDimension.length > 0) {
+ for (i = 0; i < itemsWithoutSetDimension.length; i++) {
+ itemsWithoutSetDimension[i].config[dimension] = (100 - total) / itemsWithoutSetDimension.length;
+ }
+ this._respectMinItemWidth();
+ return;
+ }
+
+ /**
+ * If the total is > 100, but there are also items without a set dimension left, assing 50
+ * as their dimension and add it to the total
+ *
+ * This will be reset in the next step
+ */
+ if (Math.round(total) > 100) {
+ for (i = 0; i < itemsWithoutSetDimension.length; i++) {
+ itemsWithoutSetDimension[i].config[dimension] = 50;
+ total += 50;
+ }
+ }
+
+ /**
+ * Set every items size relative to 100 relative to its size to total
+ */
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.contentItems[i].config[dimension] = (this.contentItems[i].config[dimension] / total) * 100;
+ }
+
+ this._respectMinItemWidth();
+ },
+
+ /**
+ * Adjusts the column widths to respect the dimensions minItemWidth if set.
+ * @returns {}
+ */
+ _respectMinItemWidth: function () {
+ var minItemWidth = this.layoutManager.config.dimensions ? (this.layoutManager.config.dimensions.minItemWidth || 0) : 0,
+ sizeData = null,
+ entriesOverMin = [],
+ totalOverMin = 0,
+ totalUnderMin = 0,
+ remainingWidth = 0,
+ itemSize = 0,
+ contentItem = null,
+ reducePercent,
+ reducedWidth,
+ allEntries = [],
+ entry;
+
+ if (this._isColumn || !minItemWidth || this.contentItems.length <= 1) {
+ return;
+ }
+
+ sizeData = this._calculateAbsoluteSizes();
+
+ /**
+ * Figure out how much we are under the min item size total and how much room we have to use.
+ */
+ for (var i = 0; i < this.contentItems.length; i++) {
+
+ contentItem = this.contentItems[i];
+ itemSize = sizeData.itemSizes[i];
+
+ if (itemSize < minItemWidth) {
+ totalUnderMin += minItemWidth - itemSize;
+ entry = { width: minItemWidth };
+
+ }
+ else {
+ totalOverMin += itemSize - minItemWidth;
+ entry = { width: itemSize };
+ entriesOverMin.push(entry);
+ }
+
+ allEntries.push(entry);
+ }
+
+ /**
+ * If there is nothing under min, or there is not enough over to make up the difference, do nothing.
+ */
+ if (totalUnderMin === 0 || totalUnderMin > totalOverMin) {
+ return;
+ }
+
+ /**
+ * Evenly reduce all columns that are over the min item width to make up the difference.
+ */
+ reducePercent = totalUnderMin / totalOverMin;
+ remainingWidth = totalUnderMin;
+ for (i = 0; i < entriesOverMin.length; i++) {
+ entry = entriesOverMin[i];
+ reducedWidth = Math.round((entry.width - minItemWidth) * reducePercent);
+ remainingWidth -= reducedWidth;
+ entry.width -= reducedWidth;
+ }
+
+ /**
+ * Take anything remaining from the last item.
+ */
+ if (remainingWidth !== 0) {
+ allEntries[allEntries.length - 1].width -= remainingWidth;
+ }
+
+ /**
+ * Set every items size relative to 100 relative to its size to total
+ */
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.contentItems[i].config.width = (allEntries[i].width / sizeData.totalWidth) * 100;
+ }
+ },
+
+ /**
+ * Instantiates a new lm.controls.Splitter, binds events to it and adds
+ * it to the array of splitters at the position specified as the index argument
+ *
+ * What it doesn't do though is append the splitter to the DOM
+ *
+ * @param {Int} index The position of the splitter
+ *
+ * @returns {lm.controls.Splitter}
+ */
+ _createSplitter: function (index) {
+ var splitter;
+ splitter = new lm.controls.Splitter(this._isColumn, this._splitterSize, this._splitterGrabSize);
+ splitter.on('drag', lm.utils.fnBind(this._onSplitterDrag, this, [splitter]), this);
+ splitter.on('dragStop', lm.utils.fnBind(this._onSplitterDragStop, this, [splitter]), this);
+ splitter.on('dragStart', lm.utils.fnBind(this._onSplitterDragStart, this, [splitter]), this);
+ this._splitter.splice(index, 0, splitter);
+ return splitter;
+ },
+
+ /**
+ * Locates the instance of lm.controls.Splitter in the array of
+ * registered splitters and returns a map containing the contentItem
+ * before and after the splitters, both of which are affected if the
+ * splitter is moved
+ *
+ * @param {lm.controls.Splitter} splitter
+ *
+ * @returns {Object} A map of contentItems that the splitter affects
+ */
+ _getItemsForSplitter: function (splitter) {
+ var index = lm.utils.indexOf(splitter, this._splitter);
+
+ return {
+ before: this.contentItems[index],
+ after: this.contentItems[index + 1]
+ };
+ },
+
+ /**
+ * Gets the minimum dimensions for the given item configuration array
+ * @param item
+ * @private
+ */
+ _getMinimumDimensions: function (arr) {
+ var minWidth = 0, minHeight = 0;
+
+ for (var i = 0; i < arr.length; ++i) {
+ minWidth = Math.max(arr[i].minWidth || 0, minWidth);
+ minHeight = Math.max(arr[i].minHeight || 0, minHeight);
+ }
+
+ return { horizontal: minWidth, vertical: minHeight };
+ },
+
+ /**
+ * Invoked when a splitter's dragListener fires dragStart. Calculates the splitters
+ * movement area once (so that it doesn't need calculating on every mousemove event)
+ *
+ * @param {lm.controls.Splitter} splitter
+ *
+ * @returns {void}
+ */
+ _onSplitterDragStart: function (splitter) {
+ var items = this._getItemsForSplitter(splitter),
+ minSize = this.layoutManager.config.dimensions[this._isColumn ? 'minItemHeight' : 'minItemWidth'];
+
+ var beforeMinDim = this._getMinimumDimensions(items.before.config.content);
+ var beforeMinSize = this._isColumn ? beforeMinDim.vertical : beforeMinDim.horizontal;
+
+ var afterMinDim = this._getMinimumDimensions(items.after.config.content);
+ var afterMinSize = this._isColumn ? afterMinDim.vertical : afterMinDim.horizontal;
+
+ this._splitterPosition = 0;
+ this._splitterMinPosition = -1 * (items.before.element[this._dimension]() - (beforeMinSize || minSize));
+ this._splitterMaxPosition = items.after.element[this._dimension]() - (afterMinSize || minSize);
+ },
+
+ /**
+ * Invoked when a splitter's DragListener fires drag. Updates the splitters DOM position,
+ * but not the sizes of the elements the splitter controls in order to minimize resize events
+ *
+ * @param {lm.controls.Splitter} splitter
+ * @param {Int} offsetX Relative pixel values to the splitters original position. Can be negative
+ * @param {Int} offsetY Relative pixel values to the splitters original position. Can be negative
+ *
+ * @returns {void}
+ */
+ _onSplitterDrag: function (splitter, offsetX, offsetY) {
+ var offset = this._isColumn ? offsetY : offsetX;
+
+ if (offset > this._splitterMinPosition && offset < this._splitterMaxPosition) {
+ this._splitterPosition = offset;
+ splitter.element.css(this._isColumn ? 'top' : 'left', offset);
+ }
+ },
+
+ /**
+ * Invoked when a splitter's DragListener fires dragStop. Resets the splitters DOM position,
+ * and applies the new sizes to the elements before and after the splitter and their children
+ * on the next animation frame
+ *
+ * @param {lm.controls.Splitter} splitter
+ *
+ * @returns {void}
+ */
+ _onSplitterDragStop: function (splitter) {
+
+ var items = this._getItemsForSplitter(splitter),
+ sizeBefore = items.before.element[this._dimension](),
+ sizeAfter = items.after.element[this._dimension](),
+ splitterPositionInRange = (this._splitterPosition + sizeBefore) / (sizeBefore + sizeAfter),
+ totalRelativeSize = items.before.config[this._dimension] + items.after.config[this._dimension];
+
+ items.before.config[this._dimension] = splitterPositionInRange * totalRelativeSize;
+ items.after.config[this._dimension] = (1 - splitterPositionInRange) * totalRelativeSize;
+
+ splitter.element.css({
+ 'top': 0,
+ 'left': 0
+ });
+
+ lm.utils.animFrame(lm.utils.fnBind(this.callDownwards, this, ['setSize']));
+ }
+ });
+
+ lm.items.Stack = function (layoutManager, config, parent) {
+ lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+ this.element = $('<div class="lm_item lm_stack"></div>');
+ this._activeContentItem = null;
+ var cfg = layoutManager.config;
+ this._header = { // defaults' reconstruction from old configuration style
+ show: cfg.settings.hasHeaders === true && config.hasHeaders !== false,
+ popout: cfg.settings.showPopoutIcon && cfg.labels.popout,
+ maximise: cfg.settings.showMaximiseIcon && cfg.labels.maximise,
+ close: cfg.settings.showCloseIcon && cfg.labels.close,
+ minimise: cfg.labels.minimise,
+ };
+ if (cfg.header) // load simplified version of header configuration (https://github.com/deepstreamIO/golden-layout/pull/245)
+ lm.utils.copy(this._header, cfg.header);
+ if (config.header) // load from stack
+ lm.utils.copy(this._header, config.header);
+ if (config.content && config.content[0] && config.content[0].header) // load from component if stack omitted
+ lm.utils.copy(this._header, config.content[0].header);
+
+ this._dropZones = {};
+ this._dropSegment = null;
+ this._contentAreaDimensions = null;
+ this._dropIndex = null;
+
+ this.isStack = true;
+
+ this.childElementContainer = $('<div class="lm_items"></div>');
+ this.header = new lm.controls.Header(layoutManager, this);
+
+ this.element.append(this.header.element);
+ this.element.append(this.childElementContainer);
+ this._setupHeaderPosition();
+ this._$validateClosability();
+ };
+
+ lm.utils.extend(lm.items.Stack, lm.items.AbstractContentItem);
+
+ lm.utils.copy(lm.items.Stack.prototype, {
+
+ setSize: function () {
+ var i,
+ headerSize = this._header.show ? this.layoutManager.config.dimensions.headerHeight : 0,
+ contentWidth = this.element.width() - (this._sided ? headerSize : 0),
+ contentHeight = this.element.height() - (!this._sided ? headerSize : 0);
+
+ this.childElementContainer.width(contentWidth);
+ this.childElementContainer.height(contentHeight);
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.contentItems[i].element.width(contentWidth).height(contentHeight);
+ }
+ this.emit('resize');
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ _$init: function () {
+ var i, initialItem;
+
+ if (this.isInitialised === true) return;
+
+ lm.items.AbstractContentItem.prototype._$init.call(this);
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.header.createTab(this.contentItems[i]);
+ this.contentItems[i]._$hide();
+ }
+
+ if (this.contentItems.length > 0) {
+ initialItem = this.contentItems[Math.min(this.contentItems.length - 1, this.config.activeItemIndex || 0)];
+
+ if (!initialItem) {
+ throw new Error('Configured activeItemIndex out of bounds');
+ }
+
+ this.setActiveContentItem(initialItem);
+ }
+ },
+
+ setActiveContentItem: function (contentItem) {
+ if (lm.utils.indexOf(contentItem, this.contentItems) === -1) {
+ throw new Error('contentItem is not a child of this stack');
+ }
+
+ if (this._activeContentItem !== null) {
+ this._activeContentItem._$hide();
+ }
+
+ this._activeContentItem = contentItem;
+ this.header.setActiveContentItem(contentItem);
+ contentItem._$show();
+ this.emit('activeContentItemChanged', contentItem);
+ this.layoutManager.emit('activeContentItemChanged', contentItem);
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ getActiveContentItem: function () {
+ return this.header.activeContentItem;
+ },
+
+ addChild: function (contentItem, index) {
+ contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+ lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem, index);
+ this.childElementContainer.append(contentItem.element);
+ this.header.createTab(contentItem, index);
+ this.setActiveContentItem(contentItem);
+ this.callDownwards('setSize');
+ this._$validateClosability();
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ removeChild: function (contentItem, keepChild) {
+ var index = lm.utils.indexOf(contentItem, this.contentItems);
+ lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
+ this.header.removeTab(contentItem);
+ if (this.header.activeContentItem === contentItem) {
+ if (this.contentItems.length > 0) {
+ this.setActiveContentItem(this.contentItems[Math.max(index - 1, 0)]);
+ } else {
+ this._activeContentItem = null;
+ }
+ }
+
+ this._$validateClosability();
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Validates that the stack is still closable or not. If a stack is able
+ * to close, but has a non closable component added to it, the stack is no
+ * longer closable until all components are closable.
+ *
+ * @returns {void}
+ */
+ _$validateClosability: function () {
+ var contentItem,
+ isClosable,
+ len,
+ i;
+
+ isClosable = this.header._isClosable();
+
+ for (i = 0, len = this.contentItems.length; i < len; i++) {
+ if (!isClosable) {
+ break;
+ }
+
+ isClosable = this.contentItems[i].config.isClosable;
+ }
+
+ this.header._$setClosable(isClosable);
+ },
+
+ _$destroy: function () {
+ lm.items.AbstractContentItem.prototype._$destroy.call(this);
+ this.header._$destroy();
+ },
+
+
+ /**
+ * Ok, this one is going to be the tricky one: The user has dropped {contentItem} onto this stack.
+ *
+ * It was dropped on either the stacks header or the top, right, bottom or left bit of the content area
+ * (which one of those is stored in this._dropSegment). Now, if the user has dropped on the header the case
+ * is relatively clear: We add the item to the existing stack... job done (might be good to have
+ * tab reordering at some point, but lets not sweat it right now)
+ *
+ * If the item was dropped on the content part things are a bit more complicated. If it was dropped on either the
+ * top or bottom region we need to create a new column and place the items accordingly.
+ * Unless, of course if the stack is already within a column... in which case we want
+ * to add the newly created item to the existing column...
+ * either prepend or append it, depending on wether its top or bottom.
+ *
+ * Same thing for rows and left / right drop segments... so in total there are 9 things that can potentially happen
+ * (left, top, right, bottom) * is child of the right parent (row, column) + header drop
+ *
+ * @param {lm.item} contentItem
+ *
+ * @returns {void}
+ */
+ _$onDrop: function (contentItem) {
+
+ /*
+ * The item was dropped on the header area. Just add it as a child of this stack and
+ * get the hell out of this logic
+ */
+ if (this._dropSegment === 'header') {
+ this._resetHeaderDropZone();
+ this.addChild(contentItem, this._dropIndex);
+ return;
+ }
+
+ /*
+ * The stack is empty. Let's just add the element.
+ */
+ if (this._dropSegment === 'body') {
+ this.addChild(contentItem);
+ return;
+ }
+
+ /*
+ * The item was dropped on the top-, left-, bottom- or right- part of the content. Let's
+ * aggregate some conditions to make the if statements later on more readable
+ */
+ var isVertical = this._dropSegment === 'top' || this._dropSegment === 'bottom',
+ isHorizontal = this._dropSegment === 'left' || this._dropSegment === 'right',
+ insertBefore = this._dropSegment === 'top' || this._dropSegment === 'left',
+ hasCorrectParent = (isVertical && this.parent.isColumn) || (isHorizontal && this.parent.isRow),
+ type = isVertical ? 'column' : 'row',
+ dimension = isVertical ? 'height' : 'width',
+ index,
+ stack,
+ rowOrColumn;
+
+ /*
+ * The content item can be either a component or a stack. If it is a component, wrap it into a stack
+ */
+ if (contentItem.isComponent) {
+ stack = this.layoutManager.createContentItem({
+ type: 'stack',
+ header: contentItem.config.header || {}
+ }, this);
+ stack._$init();
+ stack.addChild(contentItem);
+ contentItem = stack;
+ }
+
+ /*
+ * If the item is dropped on top or bottom of a column or left and right of a row, it's already
+ * layd out in the correct way. Just add it as a child
+ */
+ if (hasCorrectParent) {
+ index = lm.utils.indexOf(this, this.parent.contentItems);
+ this.parent.addChild(contentItem, insertBefore ? index : index + 1, true);
+ this.config[dimension] *= 0.5;
+ contentItem.config[dimension] = this.config[dimension];
+ this.parent.callDownwards('setSize');
+ /*
+ * This handles items that are dropped on top or bottom of a row or left / right of a column. We need
+ * to create the appropriate contentItem for them to live in
+ */
+ } else {
+ type = isVertical ? 'column' : 'row';
+ rowOrColumn = this.layoutManager.createContentItem({ type: type }, this);
+ this.parent.replaceChild(this, rowOrColumn);
+
+ rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
+ rowOrColumn.addChild(this, insertBefore ? undefined : 0, true);
+
+ this.config[dimension] = 50;
+ contentItem.config[dimension] = 50;
+ rowOrColumn.callDownwards('setSize');
+ }
+ },
+
+ /**
+ * If the user hovers above the header part of the stack, indicate drop positions for tabs.
+ * otherwise indicate which segment of the body the dragged item would be dropped on
+ *
+ * @param {Int} x Absolute Screen X
+ * @param {Int} y Absolute Screen Y
+ *
+ * @returns {void}
+ */
+ _$highlightDropZone: function (x, y) {
+ var segment, area;
+
+ for (segment in this._contentAreaDimensions) {
+ area = this._contentAreaDimensions[segment].hoverArea;
+
+ if (area.x1 < x && area.x2 > x && area.y1 < y && area.y2 > y) {
+
+ if (segment === 'header') {
+ this._dropSegment = 'header';
+ this._highlightHeaderDropZone(this._sided ? y : x);
+ } else {
+ this._resetHeaderDropZone();
+ this._highlightBodyDropZone(segment);
+ }
+
+ return;
+ }
+ }
+ },
+
+ _$getArea: function () {
+ if (this.element.is(':visible') === false) {
+ return null;
+ }
+
+ var getArea = lm.items.AbstractContentItem.prototype._$getArea,
+ headerArea = getArea.call(this, this.header.element),
+ contentArea = getArea.call(this, this.childElementContainer),
+ contentWidth = contentArea.x2 - contentArea.x1,
+ contentHeight = contentArea.y2 - contentArea.y1;
+
+ this._contentAreaDimensions = {
+ header: {
+ hoverArea: {
+ x1: headerArea.x1,
+ y1: headerArea.y1,
+ x2: headerArea.x2,
+ y2: headerArea.y2
+ },
+ highlightArea: {
+ x1: headerArea.x1,
+ y1: headerArea.y1,
+ x2: headerArea.x2,
+ y2: headerArea.y2
+ }
+ }
+ };
+
+ /**
+ * If this Stack is a parent to rows, columns or other stacks only its
+ * header is a valid dropzone.
+ */
+ if (this._activeContentItem && this._activeContentItem.isComponent === false) {
+ return headerArea;
+ }
+
+ /**
+ * Highlight the entire body if the stack is empty
+ */
+ if (this.contentItems.length === 0) {
+
+ this._contentAreaDimensions.body = {
+ hoverArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ },
+ highlightArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ }
+ };
+
+ return getArea.call(this, this.element);
+ }
+
+ this._contentAreaDimensions.left = {
+ hoverArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x1 + contentWidth * 0.25,
+ y2: contentArea.y2
+ },
+ highlightArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x1 + contentWidth * 0.5,
+ y2: contentArea.y2
+ }
+ };
+
+ this._contentAreaDimensions.top = {
+ hoverArea: {
+ x1: contentArea.x1 + contentWidth * 0.25,
+ y1: contentArea.y1,
+ x2: contentArea.x1 + contentWidth * 0.75,
+ y2: contentArea.y1 + contentHeight * 0.5
+ },
+ highlightArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y1 + contentHeight * 0.5
+ }
+ };
+
+ this._contentAreaDimensions.right = {
+ hoverArea: {
+ x1: contentArea.x1 + contentWidth * 0.75,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ },
+ highlightArea: {
+ x1: contentArea.x1 + contentWidth * 0.5,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ }
+ };
+
+ this._contentAreaDimensions.bottom = {
+ hoverArea: {
+ x1: contentArea.x1 + contentWidth * 0.25,
+ y1: contentArea.y1 + contentHeight * 0.5,
+ x2: contentArea.x1 + contentWidth * 0.75,
+ y2: contentArea.y2
+ },
+ highlightArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1 + contentHeight * 0.5,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ }
+ };
+
+ return getArea.call(this, this.element);
+ },
+
+ _highlightHeaderDropZone: function (x) {
+ var i,
+ tabElement,
+ tabsLength = this.header.tabs.length,
+ isAboveTab = false,
+ tabTop,
+ tabLeft,
+ offset,
+ placeHolderLeft,
+ headerOffset,
+ tabWidth,
+ halfX;
+
+ // Empty stack
+ if (tabsLength === 0) {
+ headerOffset = this.header.element.offset();
+
+ this.layoutManager.dropTargetIndicator.highlightArea({
+ x1: headerOffset.left,
+ x2: headerOffset.left + 100,
+ y1: headerOffset.top + this.header.element.height() - 20,
+ y2: headerOffset.top + this.header.element.height()
+ });
+
+ return;
+ }
+
+ for (i = 0; i < tabsLength; i++) {
+ tabElement = this.header.tabs[i].element;
+ offset = tabElement.offset();
+ if (this._sided) {
+ tabLeft = offset.top;
+ tabTop = offset.left;
+ tabWidth = tabElement.height();
+ } else {
+ tabLeft = offset.left;
+ tabTop = offset.top;
+ tabWidth = tabElement.width();
+ }
+
+ if (x > tabLeft && x < tabLeft + tabWidth) {
+ isAboveTab = true;
+ break;
+ }
+ }
+
+ if (isAboveTab === false && x < tabLeft) {
+ return;
+ }
+
+ halfX = tabLeft + tabWidth / 2;
+
+ if (x < halfX) {
+ this._dropIndex = i;
+ tabElement.before(this.layoutManager.tabDropPlaceholder);
+ } else {
+ this._dropIndex = Math.min(i + 1, tabsLength);
+ tabElement.after(this.layoutManager.tabDropPlaceholder);
+ }
+
+
+ if (this._sided) {
+ placeHolderTop = this.layoutManager.tabDropPlaceholder.offset().top;
+ this.layoutManager.dropTargetIndicator.highlightArea({
+ x1: tabTop,
+ x2: tabTop + tabElement.innerHeight(),
+ y1: placeHolderTop,
+ y2: placeHolderTop + this.layoutManager.tabDropPlaceholder.width()
+ });
+ return;
+ }
+ placeHolderLeft = this.layoutManager.tabDropPlaceholder.offset().left;
+
+ this.layoutManager.dropTargetIndicator.highlightArea({
+ x1: placeHolderLeft,
+ x2: placeHolderLeft + this.layoutManager.tabDropPlaceholder.width(),
+ y1: tabTop,
+ y2: tabTop + tabElement.innerHeight()
+ });
+ },
+
+ _resetHeaderDropZone: function () {
+ this.layoutManager.tabDropPlaceholder.remove();
+ },
+
+ _setupHeaderPosition: function () {
+ var side = ['right', 'left', 'bottom'].indexOf(this._header.show) >= 0 && this._header.show;
+ this.header.element.toggle(!!this._header.show);
+ this._side = side;
+ this._sided = ['right', 'left'].indexOf(this._side) >= 0;
+ this.element.removeClass('lm_left lm_right lm_bottom');
+ if (this._side)
+ this.element.addClass('lm_' + this._side);
+ if (this.element.find('.lm_header').length && this.childElementContainer) {
+ var headerPosition = ['right', 'bottom'].indexOf(this._side) >= 0 ? 'before' : 'after';
+ this.header.element[headerPosition](this.childElementContainer);
+ this.callDownwards('setSize');
+ }
+ },
+
+ _highlightBodyDropZone: function (segment) {
+ var highlightArea = this._contentAreaDimensions[segment].highlightArea;
+ this.layoutManager.dropTargetIndicator.highlightArea(highlightArea);
+ this._dropSegment = segment;
+ }
+ });
+
+ lm.utils.BubblingEvent = function (name, origin) {
+ this.name = name;
+ this.origin = origin;
+ this.isPropagationStopped = false;
+ };
+
+ lm.utils.BubblingEvent.prototype.stopPropagation = function () {
+ this.isPropagationStopped = true;
+ };
+ /**
+ * Minifies and unminifies configs by replacing frequent keys
+ * and values with one letter substitutes. Config options must
+ * retain array position/index, add new options at the end.
+ *
+ * @constructor
+ */
+ lm.utils.ConfigMinifier = function () {
+ this._keys = [
+ 'settings',
+ 'hasHeaders',
+ 'constrainDragToContainer',
+ 'selectionEnabled',
+ 'dimensions',
+ 'borderWidth',
+ 'minItemHeight',
+ 'minItemWidth',
+ 'headerHeight',
+ 'dragProxyWidth',
+ 'dragProxyHeight',
+ 'labels',
+ 'close',
+ 'maximise',
+ 'minimise',
+ 'popout',
+ 'content',
+ 'componentName',
+ 'componentState',
+ 'id',
+ 'width',
+ 'type',
+ 'height',
+ 'isClosable',
+ 'title',
+ 'popoutWholeStack',
+ 'openPopouts',
+ 'parentId',
+ 'activeItemIndex',
+ 'reorderEnabled',
+ 'borderGrabWidth',
+
+
+
+
+ //Maximum 36 entries, do not cross this line!
+ ];
+ if (this._keys.length > 36) {
+ throw new Error('Too many keys in config minifier map');
+ }
+
+ this._values = [
+ true,
+ false,
+ 'row',
+ 'column',
+ 'stack',
+ 'component',
+ 'close',
+ 'maximise',
+ 'minimise',
+ 'open in new window'
+ ];
+ };
+
+ lm.utils.copy(lm.utils.ConfigMinifier.prototype, {
+
+ /**
+ * Takes a GoldenLayout configuration object and
+ * replaces its keys and values recursively with
+ * one letter counterparts
+ *
+ * @param {Object} config A GoldenLayout config object
+ *
+ * @returns {Object} minified config
+ */
+ minifyConfig: function (config) {
+ var min = {};
+ this._nextLevel(config, min, '_min');
+ return min;
+ },
+
+ /**
+ * Takes a configuration Object that was previously minified
+ * using minifyConfig and returns its original version
+ *
+ * @param {Object} minifiedConfig
+ *
+ * @returns {Object} the original configuration
+ */
+ unminifyConfig: function (minifiedConfig) {
+ var orig = {};
+ this._nextLevel(minifiedConfig, orig, '_max');
+ return orig;
+ },
+
+ /**
+ * Recursive function, called for every level of the config structure
+ *
+ * @param {Array|Object} orig
+ * @param {Array|Object} min
+ * @param {String} translationFn
+ *
+ * @returns {void}
+ */
+ _nextLevel: function (from, to, translationFn) {
+ var key, minKey;
+
+ for (key in from) {
+
+ /**
+ * For in returns array indices as keys, so let's cast them to numbers
+ */
+ if (from instanceof Array) key = parseInt(key, 10);
+
+ /**
+ * In case something has extended Object prototypes
+ */
+ if (!from.hasOwnProperty(key)) continue;
+
+ /**
+ * Translate the key to a one letter substitute
+ */
+ minKey = this[translationFn](key, this._keys);
+
+ /**
+ * For Arrays and Objects, create a new Array/Object
+ * on the minified object and recurse into it
+ */
+ if (typeof from[key] === 'object') {
+ to[minKey] = from[key] instanceof Array ? [] : {};
+ this._nextLevel(from[key], to[minKey], translationFn);
+
+ /**
+ * For primitive values (Strings, Numbers, Boolean etc.)
+ * minify the value
+ */
+ } else {
+ to[minKey] = this[translationFn](from[key], this._values);
+ }
+ }
+ },
+
+ /**
+ * Minifies value based on a dictionary
+ *
+ * @param {String|Boolean} value
+ * @param {Array<String|Boolean>} dictionary
+ *
+ * @returns {String} The minified version
+ */
+ _min: function (value, dictionary) {
+ /**
+ * If a value actually is a single character, prefix it
+ * with ___ to avoid mistaking it for a minification code
+ */
+ if (typeof value === 'string' && value.length === 1) {
+ return '___' + value;
+ }
+
+ var index = lm.utils.indexOf(value, dictionary);
+
+ /**
+ * value not found in the dictionary, return it unmodified
+ */
+ if (index === -1) {
+ return value;
+
+ /**
+ * value found in dictionary, return its base36 counterpart
+ */
+ } else {
+ return index.toString(36);
+ }
+ },
+
+ _max: function (value, dictionary) {
+ /**
+ * value is a single character. Assume that it's a translation
+ * and return the original value from the dictionary
+ */
+ if (typeof value === 'string' && value.length === 1) {
+ return dictionary[parseInt(value, 36)];
+ }
+
+ /**
+ * value originally was a single character and was prefixed with ___
+ * to avoid mistaking it for a translation. Remove the prefix
+ * and return the original character
+ */
+ if (typeof value === 'string' && value.substr(0, 3) === '___') {
+ return value[3];
+ }
+ /**
+ * value was not minified
+ */
+ return value;
+ }
+ });
+
+ /**
+ * An EventEmitter singleton that propagates events
+ * across multiple windows. This is a little bit trickier since
+ * windows are allowed to open childWindows in their own right
+ *
+ * This means that we deal with a tree of windows. Hence the rules for event propagation are:
+ *
+ * - Propagate events from this layout to both parents and children
+ * - Propagate events from parent to this and children
+ * - Propagate events from children to the other children (but not the emitting one) and the parent
+ *
+ * @constructor
+ *
+ * @param {lm.LayoutManager} layoutManager
+ */
+ lm.utils.EventHub = function (layoutManager) {
+ lm.utils.EventEmitter.call(this);
+ this._layoutManager = layoutManager;
+ this._dontPropagateToParent = null;
+ this._childEventSource = null;
+ this.on(lm.utils.EventEmitter.ALL_EVENT, lm.utils.fnBind(this._onEventFromThis, this));
+ this._boundOnEventFromChild = lm.utils.fnBind(this._onEventFromChild, this);
+ $(window).on('gl_child_event', this._boundOnEventFromChild);
+ };
+
+ /**
+ * Called on every event emitted on this eventHub, regardles of origin.
+ *
+ * @private
+ *
+ * @param {Mixed}
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._onEventFromThis = function () {
+ var args = Array.prototype.slice.call(arguments);
+
+ if (this._layoutManager.isSubWindow && args[0] !== this._dontPropagateToParent) {
+ this._propagateToParent(args);
+ }
+ this._propagateToChildren(args);
+
+ //Reset
+ this._dontPropagateToParent = null;
+ this._childEventSource = null;
+ };
+
+ /**
+ * Called by the parent layout.
+ *
+ * @param {Array} args Event name + arguments
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._$onEventFromParent = function (args) {
+ this._dontPropagateToParent = args[0];
+ this.emit.apply(this, args);
+ };
+
+ /**
+ * Callback for child events raised on the window
+ *
+ * @param {DOMEvent} event
+ * @private
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._onEventFromChild = function (event) {
+ this._childEventSource = event.originalEvent.__gl;
+ this.emit.apply(this, event.originalEvent.__glArgs);
+ };
+
+ /**
+ * Propagates the event to the parent by emitting
+ * it on the parent's DOM window
+ *
+ * @param {Array} args Event name + arguments
+ * @private
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._propagateToParent = function (args) {
+ var event,
+ eventName = 'gl_child_event';
+
+ if (document.createEvent) {
+ event = window.opener.document.createEvent('HTMLEvents');
+ event.initEvent(eventName, true, true);
+ } else {
+ event = window.opener.document.createEventObject();
+ event.eventType = eventName;
+ }
+
+ event.eventName = eventName;
+ event.__glArgs = args;
+ event.__gl = this._layoutManager;
+
+ if (document.createEvent) {
+ window.opener.dispatchEvent(event);
+ } else {
+ window.opener.fireEvent('on' + event.eventType, event);
+ }
+ };
+
+ /**
+ * Propagate events to children
+ *
+ * @param {Array} args Event name + arguments
+ * @private
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._propagateToChildren = function (args) {
+ var childGl, i;
+
+ for (i = 0; i < this._layoutManager.openPopouts.length; i++) {
+ childGl = this._layoutManager.openPopouts[i].getGlInstance();
+
+ if (childGl && childGl !== this._childEventSource) {
+ childGl.eventHub._$onEventFromParent(args);
+ }
+ }
+ };
+
+
+ /**
+ * Destroys the EventHub
+ *
+ * @public
+ * @returns {void}
+ */
+
+ lm.utils.EventHub.prototype.destroy = function () {
+ $(window).off('gl_child_event', this._boundOnEventFromChild);
+ };
+ /**
+ * A specialised GoldenLayout component that binds GoldenLayout container
+ * lifecycle events to react components
+ *
+ * @constructor
+ *
+ * @param {lm.container.ItemContainer} container
+ * @param {Object} state state is not required for react components
+ */
+ lm.utils.ReactComponentHandler = function (container, state) {
+ this._reactComponent = null;
+ this._originalComponentWillUpdate = null;
+ this._container = container;
+ this._initialState = state;
+ this._reactClass = this._getReactClass();
+ this._container.on('open', this._render, this);
+ this._container.on('destroy', this._destroy, this);
+ };
+
+ lm.utils.copy(lm.utils.ReactComponentHandler.prototype, {
+
+ /**
+ * Creates the react class and component and hydrates it with
+ * the initial state - if one is present
+ *
+ * By default, react's getInitialState will be used
+ *
+ * @private
+ * @returns {void}
+ */
+ _render: function () {
+ this._reactComponent = ReactDOM.render(this._getReactComponent(), this._container.getElement()[0]);
+ this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function () {
+ };
+ this._reactComponent.componentWillUpdate = this._onUpdate.bind(this);
+ if (this._container.getState()) {
+ this._reactComponent.setState(this._container.getState());
+ }
+ },
+
+ /**
+ * Removes the component from the DOM and thus invokes React's unmount lifecycle
+ *
+ * @private
+ * @returns {void}
+ */
+ _destroy: function () {
+ ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
+ this._container.off('open', this._render, this);
+ this._container.off('destroy', this._destroy, this);
+ },
+
+ /**
+ * Hooks into React's state management and applies the componentstate
+ * to GoldenLayout
+ *
+ * @private
+ * @returns {void}
+ */
+ _onUpdate: function (nextProps, nextState) {
+ this._container.setState(nextState);
+ this._originalComponentWillUpdate.call(this._reactComponent, nextProps, nextState);
+ },
+
+ /**
+ * Retrieves the react class from GoldenLayout's registry
+ *
+ * @private
+ * @returns {React.Class}
+ */
+ _getReactClass: function () {
+ var componentName = this._container._config.component;
+ var reactClass;
+
+ if (!componentName) {
+ throw new Error('No react component name. type: react-component needs a field `component`');
+ }
+
+ reactClass = this._container.layoutManager.getComponent(componentName);
+
+ if (!reactClass) {
+ throw new Error('React component "' + componentName + '" not found. ' +
+ 'Please register all components with GoldenLayout using `registerComponent(name, component)`');
+ }
+
+ return reactClass;
+ },
+
+ /**
+ * Copies and extends the properties array and returns the React element
+ *
+ * @private
+ * @returns {React.Element}
+ */
+ _getReactComponent: function () {
+ var defaultProps = {
+ glEventHub: this._container.layoutManager.eventHub,
+ glContainer: this._container,
+ };
+ var props = $.extend(defaultProps, this._container._config.props);
+ return React.createElement(this._reactClass, props);
+ }
+ });
+})(window.$); \ No newline at end of file
diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
index f01f08487..e6f32272e 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -3,13 +3,15 @@ import { custom, serializable } from "serializr";
import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel";
import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel";
import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation";
-import { ObjectField, Copy } from "../../../new_fields/ObjectField";
+import { ObjectField } from "../../../new_fields/ObjectField";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { OmitKeys } from "../../../Utils";
import { Deserializable } from "../../util/SerializationHelper";
+import { Copy, ToScriptString } from "../../../new_fields/FieldSymbols";
function serialize(field: HistogramField) {
- return OmitKeys(field.HistoOp, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit;
+ let obj = OmitKeys(field, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit;
+ return obj;
}
function deserialize(jp: any) {
@@ -31,10 +33,10 @@ function deserialize(jp: any) {
}
});
if (X && Y && V) {
- return new HistogramField(new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization));
+ return new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization);
}
}
- return new HistogramField(HistogramOperation.Empty);
+ return HistogramOperation.Empty;
}
@Deserializable("histogramField")
@@ -50,6 +52,12 @@ export class HistogramField extends ObjectField {
}
[Copy]() {
- return new HistogramField(this.HistoOp.Copy());
+ let y = this.HistoOp;
+ let z = this.HistoOp.Copy;
+ return new HistogramField(HistogramOperation.Duplicate(this.HistoOp));
+ }
+
+ [ToScriptString]() {
+ return this.toString();
}
} \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
index 6291ec1fc..3e9145a1b 100644
--- a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
+++ b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
@@ -198,8 +198,8 @@ export class HistogramBinPrimitiveCollection {
var marginParams = new MarginAggregateParameters();
marginParams.aggregateFunction = axis.AggregateFunction;
var marginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, axis, this.histoResult, brush.brushIndex!, marginParams);
- var marginResult = ModelHelpers.GetAggregateResult(bin, marginAggregateKey) as MarginAggregateResult;
- return !marginResult ? 0 : marginResult.absolutMargin!;
+ let aggResult = ModelHelpers.GetAggregateResult(bin, marginAggregateKey);
+ return aggResult instanceof MarginAggregateResult && aggResult.absolutMargin ? aggResult.absolutMargin : 0;
}
private createBinPrimitive(barAxis: number, brush: Brush, marginRect: PIXIRectangle,
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index 765ecf8f0..d7732ee86 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -17,10 +17,9 @@ import "./HistogramBox.scss";
import { HistogramBoxPrimitives } from './HistogramBoxPrimitives';
import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives";
import { StyleConstants } from "../utils/StyleContants";
-import { NumCast, Cast } from "../../../new_fields/Types";
-import { listSpec } from "../../../new_fields/Schema";
-import { Doc } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/RefField";
+import { Cast } from "../../../new_fields/Types";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
@observer
@@ -117,17 +116,20 @@ export class HistogramBox extends React.Component<FieldViewProps> {
runInAction(() => {
this.HistoOp = histoOp ? histoOp.HistoOp : HistogramOperation.Empty;
if (this.HistoOp !== HistogramOperation.Empty) {
- reaction(() => Cast(this.props.Document.linkedFromDocs, listSpec(Doc), []), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
- reaction(() => Cast(this.props.Document.brushingDocs, listSpec(Doc), []).length,
- () => {
- let brushingDocs = Cast(this.props.Document.brushingDocs, listSpec(Doc), []);
+ reaction(() => DocListCast(this.props.Document.linkedFromDocs), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
+ reaction(() => DocListCast(this.props.Document.brushingDocs).length,
+ async () => {
+ let brushingDocs = await DocListCastAsync(this.props.Document.brushingDocs);
const proto = this.props.Document.proto;
- if (proto) {
- this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => {
- brush.bckgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length];
- let brushed = Cast(brush.brushingDocs, listSpec(Doc), []);
+ if (proto && brushingDocs) {
+ let mapped = brushingDocs.map((brush, i) => {
+ brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length];
+ let brushed = DocListCast(brush.brushingDocs);
+ if (!brushed.length)
+ return null;
return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] };
- }));
+ });
+ runInAction(() => this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...mapped.filter(m => m) as { l: Doc, b: Doc }[]));
}
}, { fireImmediately: true });
reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index 721bf6a89..350987695 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -11,9 +11,6 @@ import { StyleConstants } from "../../northstar/utils/StyleContants";
import { HistogramBinPrimitiveCollection, HistogramBinPrimitive } from "./HistogramBinPrimitiveCollection";
import { HistogramBox } from "./HistogramBox";
import "./HistogramBoxPrimitives.scss";
-import { JSXElement } from "babel-types";
-import { Utils } from "../utils/Utils";
-import { all } from "bluebird";
export interface HistogramPrimitivesProps {
HistoBox: HistogramBox;
diff --git a/src/client/northstar/manager/Gateway.ts b/src/client/northstar/manager/Gateway.ts
index d26f2724f..c541cce6a 100644
--- a/src/client/northstar/manager/Gateway.ts
+++ b/src/client/northstar/manager/Gateway.ts
@@ -23,6 +23,18 @@ export class Gateway {
}
}
+ public async PostSchema(csvdata: string, schemaname: string): Promise<string> {
+ try {
+ const json = await this.MakePostJsonRequest("postSchema", { csv: csvdata, schema: schemaname });
+ // const cat = Catalog.fromJS(json);
+ // return cat;
+ return json;
+ }
+ catch (error) {
+ throw new Error("can not reach northstar's backend");
+ }
+ }
+
public async GetSchema(pathname: string, schemaname: string): Promise<Catalog> {
try {
const json = await this.MakeGetRequest("schema", undefined, { path: pathname, schema: schemaname });
diff --git a/src/client/northstar/model/ModelHelpers.ts b/src/client/northstar/model/ModelHelpers.ts
index ac807b41f..88e6e72b8 100644
--- a/src/client/northstar/model/ModelHelpers.ts
+++ b/src/client/northstar/model/ModelHelpers.ts
@@ -31,7 +31,12 @@ export class ModelHelpers {
}
public static GetAggregateParametersIndex(histogramResult: HistogramResult, aggParameters?: AggregateParameters): number {
- return ArrayUtil.IndexOfWithEqual(histogramResult.aggregateParameters!, aggParameters);
+ return Array.from(histogramResult.aggregateParameters!).findIndex((value, i, set) => {
+ if (set[i] instanceof CountAggregateParameters && value instanceof CountAggregateParameters) return true;
+ if (set[i] instanceof MarginAggregateParameters && value instanceof MarginAggregateParameters) return true;
+ if (set[i] instanceof SumAggregateParameters && value instanceof SumAggregateParameters) return true;
+ return false;
+ });
}
public static GetAggregateParameter(distinctAttributeParameters: AttributeParameters | undefined, atm: AttributeTransformationModel): AggregateParameters | undefined {
diff --git a/src/client/northstar/model/idea/idea.ts b/src/client/northstar/model/idea/idea.ts
index 9d9d60678..c73a822c7 100644
--- a/src/client/northstar/model/idea/idea.ts
+++ b/src/client/northstar/model/idea/idea.ts
@@ -22,6 +22,9 @@ export abstract class AggregateParameters implements IAggregateParameters {
protected _discriminator: string;
+ public Equals(other: Object): boolean {
+ return this == other;
+ }
constructor(data?: IAggregateParameters) {
if (data) {
for (var property in data) {
@@ -204,6 +207,9 @@ export interface IAverageAggregateParameters extends ISingleDimensionAggregatePa
export abstract class AttributeParameters implements IAttributeParameters {
visualizationHints?: VisualizationHint[] | undefined;
rawName?: string | undefined;
+ public Equals(other: Object): boolean {
+ return this == other;
+ }
protected _discriminator: string;
diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts
index c6d5f0a15..0d1361ebf 100644
--- a/src/client/northstar/operations/BaseOperation.ts
+++ b/src/client/northstar/operations/BaseOperation.ts
@@ -25,23 +25,6 @@ export abstract class BaseOperation {
@computed
public get FilterString(): string {
-
- // let filterModels: FilterModel[] = [];
- // return FilterModel.GetFilterModelsRecursive(this, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, true)
- // if (this.OverridingFilters.length > 0) {
- // return "(" + this.OverridingFilters.filter(fm => fm !== null).map(fm => fm.ToPythonString()).join(" || ") + ")";
- // }
- // let rdg = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph;
- // let sliceModel = this.TypedViewModel.IncomingSliceModel;
- // if (sliceModel !== null && sliceModel.Source !== null && instanceOfIBaseFilterProvider(sliceModel.Source) && rdg.has(sliceModel.Source)) {
- // let filterModels = sliceModel.Source.FilterModels.map(f => f);
- // return FilterModel.GetFilterModelsRecursive(rdg.get(sliceModel.Source), new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, false);
- // }
-
- // if (rdg.has(this.TypedViewModel)) {
- // let filterModels = [];
- // return FilterModel.GetFilterModelsRecursive(rdg.get(this.TypedViewModel), new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, true)
- // }
return "";
}
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index 5c9c832c0..74e23ea48 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -30,7 +30,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
@observable public V: AttributeTransformationModel;
@observable public SchemaName: string;
@observable public QRange: QuantitativeBinRange | undefined;
- @computed public get Schema() { return CurrentUserUtils.GetNorthstarSchema(this.SchemaName); }
+ public get Schema() { return CurrentUserUtils.GetNorthstarSchema(this.SchemaName); }
constructor(schemaName: string, x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel, normalized?: number) {
super();
@@ -41,7 +41,11 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
this.SchemaName = schemaName;
}
- Copy(): HistogramOperation {
+ public static Duplicate(op: HistogramOperation) {
+
+ return new HistogramOperation(op.SchemaName, op.X, op.Y, op.V, op.Normalization);
+ }
+ public Copy(): HistogramOperation {
return new HistogramOperation(this.SchemaName, this.X, this.Y, this.V, this.Normalization);
}
@@ -50,7 +54,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
}
- @computed public get FilterModels() {
+ public get FilterModels() {
return this.BarFilterModels;
}
@action
@@ -65,15 +69,13 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
@computed
public get FilterString(): string {
if (this.OverridingFilters.length > 0) {
- return "(" + this.OverridingFilters.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
+ return "(" + this.OverridingFilters.filter(fm => fm !== null).map(fm => fm.ToPythonString()).join(" || ") + ")";
}
let filterModels: FilterModel[] = [];
return FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, true);
}
- @computed
public get BrushString(): string[] {
- trace();
let brushes: string[] = [];
this.BrushLinks.map(brushLink => {
let brushHistogram = Cast(brushLink.b.data, HistogramField);
@@ -89,8 +91,9 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
@action
public DrillDown(up: boolean) {
if (!up) {
- if (!this.BarFilterModels.length)
+ if (!this.BarFilterModels.length) {
return;
+ }
this._stackedFilters.push(this.BarFilterModels.map(f => f));
this.OverridingFilters.length = 0;
this.OverridingFilters.push(...this._stackedFilters[this._stackedFilters.length - 1]);
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 69964e2c9..65c4b9e4b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,8 +1,14 @@
import { computed, observable } from 'mobx';
import { DocumentView } from '../views/nodes/DocumentView';
-import { Doc } from '../../new_fields/Doc';
-import { FieldValue, Cast } from '../../new_fields/Types';
+import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
+import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
import { listSpec } from '../../new_fields/Schema';
+import { undoBatch } from './UndoManager';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { CollectionView } from '../views/collections/CollectionView';
+import { CollectionPDFView } from '../views/collections/CollectionPDFView';
+import { CollectionVideoView } from '../views/collections/CollectionVideoView';
+import { Id } from '../../new_fields/FieldSymbols';
export class DocumentManager {
@@ -24,28 +30,35 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
- public getDocumentView(toFind: Doc): DocumentView | null {
+ public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
let toReturn: DocumentView | null = null;
+ let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
- //gets document view that is in a freeform canvas collection
- DocumentManager.Instance.DocumentViews.map(view => {
- if (view.props.Document === toFind) {
- toReturn = view;
- return;
- }
- });
- if (!toReturn) {
+ for (let i = 0; i < passes.length; i++) {
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document.proto;
- if (doc && Object.is(doc, toFind)) {
+ if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
toReturn = view;
+ return;
}
});
+ if (!toReturn) {
+ DocumentManager.Instance.DocumentViews.map(view => {
+ let doc = view.props.Document.proto;
+ if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
+ toReturn = view;
+ }
+ });
+ }
}
return toReturn;
}
+
+ public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+ return this.getDocumentViewById(toFind[Id], preferredCollection);
+ }
+
public getDocumentViews(toFind: Doc): DocumentView[] {
let toReturn: DocumentView[] = [];
@@ -70,8 +83,8 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
- return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
- let linksList = Cast(dv.props.Document.linkedToDocs, listSpec(Doc));
+ return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+ let linksList = DocListCast(dv.props.Document.linkedToDocs);
if (linksList && linksList.length) {
pairs.push(...linksList.reduce((pairs, link) => {
if (link) {
@@ -84,7 +97,54 @@ export class DocumentManager {
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
}
+ linksList = DocListCast(dv.props.Document.linkedFromDocs);
+ if (linksList && linksList.length) {
+ pairs.push(...linksList.reduce((pairs, link) => {
+ if (link) {
+ let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
+ if (linkFromDoc) {
+ DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
+ pairs.push({ a: dv, b: docView1, l: link }));
+ }
+ }
+ return pairs;
+ }, pairs));
+ }
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
}
+
+ @undoBatch
+ public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number): Promise<void> => {
+ let doc = Doc.GetProto(docDelegate);
+ const contextDoc = await Cast(doc.annotationOn, Doc);
+ if (contextDoc) {
+ const page = NumCast(doc.page, linkPage || 0);
+ const curPage = NumCast(contextDoc.curPage, page);
+ if (page !== curPage) contextDoc.curPage = page;
+ }
+ let docView: DocumentView | null;
+ // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed
+ if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
+ docView.props.Document.libraryBrush = true;
+ if (linkPage !== undefined) docView.props.Document.curPage = linkPage;
+ docView.props.focus(docView.props.Document);
+ } else {
+ if (!contextDoc) {
+ const actualDoc = Doc.MakeAlias(docDelegate);
+ actualDoc.libraryBrush = true;
+ if (linkPage !== undefined) actualDoc.curPage = linkPage;
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc);
+ } else {
+ let contextView: DocumentView | null;
+ docDelegate.libraryBrush = true;
+ if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) {
+ contextDoc.panTransformType = "Ease";
+ contextView.props.focus(contextDoc);
+ } else {
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc);
+ }
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index a3dbe6e43..1e84a0db0 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,35 +1,33 @@
-import { action } from "mobx";
+import { action, runInAction } from "mobx";
+import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { Cast } from "../../new_fields/Types";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import * as globalCssVariables from "../views/globalCssVariables.scss";
-import { MainOverlayTextBox } from "../views/MainOverlayTextBox";
-import { Doc } from "../../new_fields/Doc";
-import { Cast } from "../../new_fields/Types";
-import { listSpec } from "../../new_fields/Schema";
export type dropActionType = "alias" | "copy" | undefined;
-export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Doc, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
- let onRowMove = action((e: PointerEvent): void => {
+export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
+ let onRowMove = async (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
- var dragData = new DragManager.DocumentDragData([docFunc()]);
+ var dragData = new DragManager.DocumentDragData([await docFunc()]);
dragData.dropAction = dropAction;
dragData.moveDocument = moveFunc;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
- });
- let onRowUp = action((e: PointerEvent): void => {
+ };
+ let onRowUp = (): void => {
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
- });
- let onItemDown = (e: React.PointerEvent) => {
+ };
+ let onItemDown = async (e: React.PointerEvent) => {
// if (this.props.isSelected() || this.props.isTopMost) {
if (e.button === 0) {
e.stopPropagation();
- if (e.shiftKey) {
- CollectionDockingView.Instance.StartOtherDrag([docFunc()], e);
+ if (e.shiftKey && CollectionDockingView.Instance) {
+ CollectionDockingView.Instance.StartOtherDrag([await docFunc()], e);
} else {
document.addEventListener("pointermove", onRowMove);
document.addEventListener("pointerup", onRowUp);
@@ -42,12 +40,14 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
let srcTarg = sourceDoc.proto;
- let draggedDocs = srcTarg ?
- Cast(srcTarg.linkedToDocs, listSpec(Doc), []).map(linkDoc =>
- Cast(linkDoc.linkedTo, Doc) as Doc) : [];
- let draggedFromDocs = srcTarg ?
- Cast(srcTarg.linkedFromDocs, listSpec(Doc), []).map(linkDoc =>
- Cast(linkDoc.linkedFrom, Doc) as Doc) : [];
+ let draggedDocs: Doc[] = [];
+ let draggedFromDocs: Doc[] = [];
+ if (srcTarg) {
+ let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs);
+ let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
+ if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
+ if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+ }
draggedDocs.push(...draggedFromDocs);
if (draggedDocs.length) {
let moddrag: Doc[] = [];
@@ -105,7 +105,8 @@ export namespace DragManager {
constructor(
readonly x: number,
readonly y: number,
- readonly data: { [id: string]: any }
+ readonly data: { [id: string]: any },
+ readonly mods: string
) { }
}
@@ -152,12 +153,15 @@ export namespace DragManager {
[id: string]: any;
}
+ export let StartDragFunctions: (() => void)[] = [];
+
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ runInAction(() => StartDragFunctions.map(func => func()));
StartDrag(eles, dragData, downX, downY, options,
(dropData: { [id: string]: any }) =>
- (dropData.droppedDocuments = dragData.userDropAction == "alias" || (!dragData.userDropAction && dragData.dropAction == "alias") ?
+ (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
- dragData.userDropAction == "copy" || (!dragData.userDropAction && dragData.dropAction == "copy") ?
+ dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
dragData.draggedDocuments));
}
@@ -170,6 +174,7 @@ export namespace DragManager {
droppedDocuments: Doc[] = [];
linkSourceDocument: Doc;
blacklist: Doc[];
+ dontClearTextBox?: boolean;
[id: string]: any;
}
@@ -186,7 +191,6 @@ export namespace DragManager {
dragDiv.style.pointerEvents = "none";
DragManager.Root().appendChild(dragDiv);
}
- MainOverlayTextBox.Instance.SetTextDoc();
let scaleXs: number[] = [];
let scaleYs: number[] = [];
@@ -214,6 +218,7 @@ export namespace DragManager {
dragElement.style.top = "0";
dragElement.style.bottom = "";
dragElement.style.left = "0";
+ dragElement.style.color = "black";
dragElement.style.transformOrigin = "0 0";
dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000";
dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
@@ -259,7 +264,7 @@ export namespace DragManager {
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
}
- if (e.shiftKey) {
+ if (e.shiftKey && CollectionDockingView.Instance) {
AbortDrag();
CollectionDockingView.Instance.StartOtherDrag(docs, {
pageX: e.pageX,
@@ -274,13 +279,12 @@ export namespace DragManager {
lastX = e.pageX;
lastY = e.pageY;
dragElements.map((dragElement, i) => (dragElement.style.transform =
- `translate(${(xs[i] += moveX)}px, ${(ys[i] += moveY)}px)
- scale(${scaleXs[i]}, ${scaleYs[i]})`)
+ `translate(${(xs[i] += moveX)}px, ${(ys[i] += moveY)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
);
};
let hideDragElements = () => {
- dragElements.map(dragElement => dragElement.parentNode == dragDiv && dragDiv.removeChild(dragElement));
+ dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
eles.map(ele => (ele.hidden = false));
};
let endDrag = () => {
@@ -289,7 +293,7 @@ export namespace DragManager {
if (options) {
options.handlers.dragComplete({});
}
- }
+ };
AbortDrag = () => {
hideDragElements();
@@ -330,7 +334,8 @@ export namespace DragManager {
detail: {
x: e.x,
y: e.y,
- data: dragData
+ data: dragData,
+ mods: e.altKey ? "AltKey" : ""
}
})
);
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
new file mode 100644
index 000000000..545ea8629
--- /dev/null
+++ b/src/client/util/History.ts
@@ -0,0 +1,122 @@
+import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { DocServer } from "../DocServer";
+import { RouteStore } from "../../server/RouteStore";
+import { MainView } from "../views/MainView";
+
+export namespace HistoryUtil {
+ export interface DocInitializerList {
+ [key: string]: string | number;
+ }
+
+ export interface DocUrl {
+ type: "doc";
+ docId: string;
+ initializers: {
+ [docId: string]: DocInitializerList;
+ };
+ }
+
+ export type ParsedUrl = DocUrl;
+
+ // const handlers: ((state: ParsedUrl | null) => void)[] = [];
+ function onHistory(e: PopStateEvent) {
+ if (window.location.pathname !== RouteStore.home) {
+ const url = e.state as ParsedUrl || parseUrl(window.location.pathname);
+ if (url) {
+ switch (url.type) {
+ case "doc":
+ onDocUrl(url);
+ break;
+ }
+ }
+ }
+ // for (const handler of handlers) {
+ // handler(e.state);
+ // }
+ }
+
+ export function pushState(state: ParsedUrl) {
+ history.pushState(state, "", createUrl(state));
+ }
+
+ export function replaceState(state: ParsedUrl) {
+ history.replaceState(state, "", createUrl(state));
+ }
+
+ function copyState(state: ParsedUrl): ParsedUrl {
+ return JSON.parse(JSON.stringify(state));
+ }
+
+ export function getState(): ParsedUrl {
+ return copyState(history.state);
+ }
+
+ // export function addHandler(handler: (state: ParsedUrl | null) => void) {
+ // handlers.push(handler);
+ // }
+
+ // export function removeHandler(handler: (state: ParsedUrl | null) => void) {
+ // const index = handlers.indexOf(handler);
+ // if (index !== -1) {
+ // handlers.splice(index, 1);
+ // }
+ // }
+
+ export function parseUrl(pathname: string): ParsedUrl | undefined {
+ let pathnameSplit = pathname.split("/");
+ if (pathnameSplit.length !== 2) {
+ return undefined;
+ }
+ const type = pathnameSplit[0];
+ const data = pathnameSplit[1];
+
+ if (type === "doc") {
+ const s = data.split("?");
+ if (s.length < 1 || s.length > 2) {
+ return undefined;
+ }
+ const docId = s[0];
+ const initializers = s.length === 2 ? JSON.parse(decodeURIComponent(s[1])) : {};
+ return {
+ type: "doc",
+ docId,
+ initializers
+ };
+ }
+
+ return undefined;
+ }
+
+ export function createUrl(params: ParsedUrl): string {
+ let baseUrl = DocServer.prepend(`/${params.type}`);
+ switch (params.type) {
+ case "doc":
+ const initializers = encodeURIComponent(JSON.stringify(params.initializers));
+ const id = params.docId;
+ let url = baseUrl + `/${id}`;
+ if (Object.keys(params.initializers).length) {
+ url += `?${initializers}`;
+ }
+ return url;
+ }
+ return "";
+ }
+
+ export async function initDoc(id: string, initializer: DocInitializerList) {
+ const doc = await DocServer.GetRefField(id);
+ if (!(doc instanceof Doc)) {
+ return;
+ }
+ Doc.assign(doc, initializer);
+ }
+
+ async function onDocUrl(url: DocUrl) {
+ const field = await DocServer.GetRefField(url.docId);
+ await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id])));
+ if (field instanceof Doc) {
+ MainView.Instance.openWorkspace(field, true);
+ }
+ }
+
+ window.onpopstate = onHistory;
+}
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 9ef71e305..3e3e98206 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -84,6 +84,7 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
src: {},
+ width: { default: "100px" },
alt: { default: null },
title: { default: null }
},
@@ -94,11 +95,16 @@ export const nodes: { [index: string]: NodeSpec } = {
return {
src: dom.getAttribute("src"),
title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt")
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
};
}
}],
- toDOM(node: any) { return ["img", node.attrs]; }
+ // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["img", { ...node.attrs, ...attrs }];
+ }
},
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
@@ -290,6 +296,13 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
+ p14: {
+ parseDOM: [{ style: 'font-size: 14px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 14px;'
+ }]
+ },
+
p16: {
parseDOM: [{ style: 'font-size: 16px;' }],
toDOM: () => ['span', {
@@ -325,7 +338,75 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
};
+function getFontSize(element: any) {
+ return parseFloat((getComputedStyle(element) as any).fontSize);
+}
+
+export class ImageResizeView {
+ _handle: HTMLElement;
+ _img: HTMLElement;
+ _outer: HTMLElement;
+ constructor(node: any, view: any, getPos: any) {
+ this._handle = document.createElement("span");
+ this._img = document.createElement("img");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.display = "inline-block";
+ this._outer.style.overflow = "hidden";
+
+ this._img.setAttribute("src", node.attrs.src);
+ this._img.style.width = "100%";
+ this._handle.style.position = "absolute";
+ this._handle.style.width = "20px";
+ this._handle.style.height = "20px";
+ this._handle.style.backgroundColor = "blue";
+ this._handle.style.borderRadius = "15px";
+ this._handle.style.display = "none";
+ this._handle.style.bottom = "-10px";
+ this._handle.style.right = "-10px";
+ let self = this;
+ this._handle.onpointerdown = function (e: any) {
+ e.preventDefault();
+ e.stopPropagation();
+ const startX = e.pageX;
+ const startWidth = parseFloat(node.attrs.width);
+ const onpointermove = (e: any) => {
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ self._outer.style.width = `${startWidth + diffInPx}`;
+ };
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ view.dispatch(
+ view.state.tr.setNodeMarkup(getPos(), null,
+ { src: node.attrs.src, width: self._outer.style.width })
+ .setSelection(view.state.selection));
+ };
+
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ };
+
+ this._outer.appendChild(this._handle);
+ this._outer.appendChild(this._img);
+ (this as any).dom = this._outer;
+ }
+ selectNode() {
+ this._img.classList.add("ProseMirror-selectednode");
+
+ this._handle.style.display = "";
+ }
+
+ deselectNode() {
+ this._img.classList.remove("ProseMirror-selectednode");
+
+ this._handle.style.display = "none";
+ }
+}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index e45f61c11..beaf5cb03 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -41,7 +41,7 @@ export type CompileResult = CompiledScript | CompileError;
function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
- if (errors || !script) {
+ if ((options.typecheck !== false && errors) || !script) {
return { compiled: false, errors: diagnostics };
}
@@ -131,10 +131,11 @@ export interface ScriptOptions {
addReturn?: boolean;
params?: { [name: string]: string };
capturedVariables?: { [name: string]: Field };
+ typecheck?: boolean;
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
- const { requiredType = "", addReturn = false, params = {}, capturedVariables = {} } = options;
+ const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
let host = new ScriptingCompilerHost;
let paramNames: string[] = [];
if ("this" in params || "this" in capturedVariables) {
@@ -158,7 +159,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
${addReturn ? `return ${script};` : script}
})`;
host.writeFile("file.ts", funcScript);
- host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
+ if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
let program = ts.createProgram(["file.ts"], {}, host);
let testResult = program.emit();
let outputText = host.readFile("file.js");
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
new file mode 100644
index 000000000..28ec8ca14
--- /dev/null
+++ b/src/client/util/SearchUtil.ts
@@ -0,0 +1,26 @@
+import * as rp from 'request-promise';
+import { DocServer } from '../DocServer';
+import { Doc } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/FieldSymbols';
+
+export namespace SearchUtil {
+ export function Search(query: string, returnDocs: true): Promise<Doc[]>;
+ export function Search(query: string, returnDocs: false): Promise<string[]>;
+ export async function Search(query: string, returnDocs: boolean) {
+ const ids = JSON.parse(await rp.get(DocServer.prepend("/search"), {
+ qs: { query }
+ }));
+ if (!returnDocs) {
+ return ids;
+ }
+ const docMap = await DocServer.GetRefFields(ids);
+ return ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
+ }
+
+ export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]> {
+ const proto = await Doc.GetT(doc, "proto", Doc, true);
+ const protoId = (proto || doc)[Id];
+ return Search(`proto_i:"${protoId}"`, true);
+ // return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
+ }
+} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index fe5acf4b4..8c92c2023 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,7 +1,8 @@
import { observable, action } from "mobx";
import { Doc } from "../../new_fields/Doc";
-import { MainOverlayTextBox } from "../views/MainOverlayTextBox";
import { DocumentView } from "../views/nodes/DocumentView";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { NumCast } from "../../new_fields/Types";
export namespace SelectionManager {
class Manager {
@@ -25,7 +26,7 @@ export namespace SelectionManager {
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
- MainOverlayTextBox.Instance.SetTextDoc();
+ FormattedTextBox.InputBoxOverlay = undefined;
}
@action
ReselectAll() {
@@ -68,4 +69,20 @@ export namespace SelectionManager {
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments;
}
+ export function ViewsSortedVertically(): DocumentView[] {
+ let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
+ if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ return 0;
+ });
+ return sorted;
+ }
+ export function ViewsSortedHorizontally(): DocumentView[] {
+ let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
+ if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
+ return 0;
+ });
+ return sorted;
+ }
}
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
index 70d9ad772..437da0d63 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/util/TooltipTextMenu.scss
@@ -162,19 +162,6 @@
.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;
@@ -255,6 +242,19 @@
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
pointer-events: all;
+ .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;
+ }
}
.tooltipMenu:before {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 68a73375e..f517f757a 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -6,19 +6,28 @@ import { keymap } from "prosemirror-keymap";
import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType } from "prosemirror-model";
+import { Schema, NodeType, MarkType, Mark } 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, liftListItem, listItem, } from 'prosemirror-schema-list';
-import { liftTarget } from 'prosemirror-transform';
+import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform';
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";
+import { View } from "@react-pdf/renderer";
+import { DragManager } from "./DragManager";
+import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { Utils } from "../northstar/utils/Utils";
+import { DocServer } from "../DocServer";
+import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { DocumentManager } from "./DocumentManager";
+import { Id } from "../../new_fields/FieldSymbols";
const SVG = "http://www.w3.org/2000/svg";
@@ -37,6 +46,9 @@ export class TooltipTextMenu {
private fontStylesToName: Map<MarkType, string>;
private listTypeToIcon: Map<NodeType, string>;
private fontSizeIndicator: HTMLSpanElement = document.createElement("span");
+ private linkEditor?: HTMLDivElement;
+ private linkText?: HTMLDivElement;
+ private linkDrag?: HTMLImageElement;
//dropdown doms
private fontSizeDom?: Node;
private fontStyleDom?: Node;
@@ -94,6 +106,7 @@ export class TooltipTextMenu {
this.fontSizeToNum = new Map();
this.fontSizeToNum.set(schema.marks.p10, 10);
this.fontSizeToNum.set(schema.marks.p12, 12);
+ this.fontSizeToNum.set(schema.marks.p14, 14);
this.fontSizeToNum.set(schema.marks.p16, 16);
this.fontSizeToNum.set(schema.marks.p24, 24);
this.fontSizeToNum.set(schema.marks.p32, 32);
@@ -149,6 +162,97 @@ export class TooltipTextMenu {
this.tooltip.appendChild(this.fontStyleDom);
}
+ updateLinkMenu() {
+ if (!this.linkEditor || !this.linkText) {
+ this.linkEditor = document.createElement("div");
+ this.linkEditor.style.color = "white";
+ this.linkText = document.createElement("div");
+ this.linkText.style.cssFloat = "left";
+ this.linkText.style.marginRight = "5px";
+ this.linkText.style.marginLeft = "5px";
+ this.linkText.setAttribute("contenteditable", "true");
+ this.linkText.style.whiteSpace = "nowrap";
+ this.linkText.style.width = "150px";
+ this.linkText.style.overflow = "hidden";
+ this.linkText.style.color = "white";
+ this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); };
+ let linkBtn = document.createElement("div");
+ linkBtn.textContent = ">>";
+ linkBtn.style.width = "20px";
+ linkBtn.style.height = "20px";
+ linkBtn.style.color = "white";
+ linkBtn.style.cssFloat = "left";
+ linkBtn.onpointerdown = (e: PointerEvent) => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ let href: string = link.attrs.href;
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "");
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
+ if (DocumentManager.Instance.getDocumentView(f)) {
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ }
+ else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f);
+ }
+ }));
+ }
+ // TODO This should have an else to handle external links
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+ this.linkDrag = document.createElement("img");
+ this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png";
+ this.linkDrag.style.width = "20px";
+ this.linkDrag.style.height = "20px";
+ this.linkDrag.style.color = "white";
+ this.linkDrag.style.background = "black";
+ this.linkDrag.style.cssFloat = "left";
+ this.linkDrag.onpointerdown = (e: PointerEvent) => {
+ let dragData = new DragManager.LinkDragData(this.editorProps.Document);
+ dragData.dontClearTextBox = true;
+ DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY,
+ {
+ handlers: {
+ dragComplete: action(() => {
+ let m = dragData.droppedDocuments;
+ this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
+ }),
+ },
+ hideSource: false
+ });
+ };
+ this.linkEditor.appendChild(this.linkDrag);
+ this.linkEditor.appendChild(this.linkText);
+ this.linkEditor.appendChild(linkBtn);
+ this.tooltip.appendChild(this.linkEditor);
+ }
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ this.linkText.textContent = link ? link.attrs.href : "-empty-";
+
+ this.linkText.onkeydown = (e: KeyboardEvent) => {
+ if (e.key === "Enter") {
+ this.makeLink(this.linkText!.textContent!);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+ this.tooltip.appendChild(this.linkEditor);
+ }
+
+ makeLink = (target: string) => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ node = this.view.state.selection.$from.nodeAfter;
+ link = node && node.marks.find(m => m.type.name === "link");
+ }
+
//will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
updateListItemDropdown(label: string, listTypeBtn: Node) {
//remove old btn
@@ -347,6 +451,8 @@ export class TooltipTextMenu {
} else { //multiple font sizes selected
this.updateFontSizeDropdown("Various");
}
+
+ this.updateLinkMenu();
}
//finds all active marks on selection in given group
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 0b5280c4a..c0ed015bd 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,7 +1,6 @@
import { observable, action, runInAction } from "mobx";
import 'source-map-support/register';
import { Without } from "../../Utils";
-import { string } from "prop-types";
function getBatchName(target: any, key: string | symbol): string {
let keyName = key.toString();
@@ -94,6 +93,10 @@ export namespace UndoManager {
return redoStack.length > 0;
}
+ export function PrintBatches(): void {
+ GetOpenBatches().forEach(batch => console.log(batch.batchName));
+ }
+
let openBatches: Batch[] = [];
export function GetOpenBatches(): Without<Batch, 'end'>[] {
return openBatches;
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 47c3481b2..557f6f574 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -119,104 +119,71 @@ interface URL {
username: string;
toJSON(): string;
}
-
-declare type FieldId = string;
-
-declare abstract class Field {
- Id: FieldId;
- abstract ToScriptString(): string;
- abstract TrySetValue(value: any): boolean;
- abstract GetValue(): any;
- abstract Copy(): Field;
+interface PromiseLike<T> {
+ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
}
-
-declare abstract class BasicField<T> extends Field {
- constructor(data: T);
- Data: T;
- TrySetValue(value: any): boolean;
- GetValue(): any;
+interface Promise<T> {
+ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
+ catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}
-declare class TextField extends BasicField<string>{
- constructor();
- constructor(data: string);
- ToScriptString(): string;
- Copy(): Field;
-}
-declare class ImageField extends BasicField<URL>{
+declare const Update: unique symbol;
+declare const Self: unique symbol;
+declare const SelfProxy: unique symbol;
+declare const HandleUpdate: unique symbol;
+declare const Id: unique symbol;
+declare const OnUpdate: unique symbol;
+declare const Parent: unique symbol;
+declare const Copy: unique symbol;
+declare const ToScriptString: unique symbol;
+
+declare abstract class RefField {
+ readonly [Id]: FieldId;
+
constructor();
- constructor(data: URL);
- ToScriptString(): string;
- Copy(): Field;
+ // protected [HandleUpdate]?(diff: any): void;
+
+ // abstract [ToScriptString](): string;
}
-declare class HtmlField extends BasicField<string>{
- constructor();
- constructor(data: string);
- ToScriptString(): string;
- Copy(): Field;
+
+declare abstract class ObjectField {
+ protected [OnUpdate](diff?: any): void;
+ private [Parent]?: RefField | ObjectField;
+ // abstract [Copy](): ObjectField;
+
+ // abstract [ToScriptString](): string;
}
-declare class NumberField extends BasicField<number>{
- constructor();
- constructor(data: number);
- ToScriptString(): string;
- Copy(): Field;
+
+declare abstract class URLField extends ObjectField {
+ readonly url: URL;
+
+ constructor(url: string);
+ constructor(url: URL);
}
-declare class WebField extends BasicField<URL>{
+
+declare class AudioField extends URLField { }
+declare class VideoField extends URLField { }
+declare class ImageField extends URLField { }
+declare class WebField extends URLField { }
+declare class PdfField extends URLField { }
+
+declare type FieldId = string;
+
+declare type Field = number | string | boolean | ObjectField | RefField;
+
+declare type Opt<T> = T | undefined;
+declare class Doc extends RefField {
constructor();
- constructor(data: URL);
- ToScriptString(): string;
- Copy(): Field;
+
+ [key: string]: Field | undefined;
+ // [ToScriptString](): string;
}
-declare class ListField<T> extends BasicField<T[]>{
- constructor();
- constructor(data: T[]);
- ToScriptString(): string;
- Copy(): Field;
-}
-declare class Key extends Field {
- constructor(name:string);
- Name: string;
- TrySetValue(value: any): boolean;
- GetValue(): any;
- Copy(): Field;
- ToScriptString(): string;
-}
-declare type FIELD_WAITING = null;
-declare type Opt<T> = T | undefined;
-declare type FieldValue<T> = Opt<T> | FIELD_WAITING;
-// @ts-ignore
-declare class Document extends Field {
- TrySetValue(value: any): boolean;
- GetValue(): any;
- Copy(): Field;
- ToScriptString(): string;
-
- Width(): number;
- Height(): number;
- Scale(): number;
- Title: string;
-
- Get(key: Key): FieldValue<Field>;
- GetAsync(key: Key, callback: (field: Field) => void): boolean;
- GetOrCreateAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: T) => void): void;
- GetT<T extends Field>(key: Key, ctor: { new(): T }): FieldValue<T>;
- GetOrCreate<T extends Field>(key: Key, ctor: { new(): T }): T;
- GetData<T, U extends Field & { Data: T }>(key: Key, ctor: { new(): U }, defaultVal: T): T;
- GetHtml(key: Key, defaultVal: string): string;
- GetNumber(key: Key, defaultVal: number): number;
- GetText(key: Key, defaultVal: string): string;
- GetList<T extends Field>(key: Key, defaultVal: T[]): T[];
- Set(key: Key, field: Field | undefined): void;
- SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }): void;
- SetText(key: Key, value: string): void;
- SetNumber(key: Key, value: number): void;
- GetPrototype(): FieldValue<Document>;
- GetAllPrototypes(): Document[];
- MakeDelegate(): Document;
-}
-
-declare const KeyStore: {
- [name: string]: Key;
+
+declare class ListImpl<T extends Field> extends ObjectField {
+ constructor(fields?: T[]);
+ [index: number]: T | (T extends RefField ? Promise<T> : never);
+ // [ToScriptString](): string;
+ // [Copy](): ObjectField;
}
// @ts-ignore
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index fe884ca85..7e066d53a 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -1,55 +1,73 @@
@import "globalCssVariables";
+
.contextMenu-cont {
- position: absolute;
- display: flex;
- z-index: $contextMenu-zindex;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
- flex-direction: column;
+ position: absolute;
+ display: flex;
+ z-index: $contextMenu-zindex;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ flex-direction: column;
}
.contextMenu-item:first-child {
- background: $intermediate-color;
- color: $light-color;
+ background: $intermediate-color;
+ color: $light-color;
}
.contextMenu-item:first-child::placeholder {
- color: $light-color;
+ color: $light-color;
}
.contextMenu-item:first-child:hover {
- background: $intermediate-color;
- color: $light-color;
+ background: $intermediate-color;
+ color: $light-color;
+}
+
+.contextMenu-subMenu-cont {
+ position: absolute;
+ display: flex;
+ z-index: 1000;
+ box-shadow: #AAAAAA .2vw .2vw .4vw;
+ flex-direction: column;
}
.contextMenu-item {
- width: auto;
- height: auto;
- background: $light-color-secondary;
- display: flex;
- justify-content: left;
- align-items: center;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- transition: all 0.1s;
- border-width: 0.11px;
- border-style: none;
- border-color: $intermediate-color;
- border-bottom-style: solid;
- padding: 10px;
- white-space: nowrap;
- font-size: 13px;
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: #DDDDDD;
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color; // rgb(187, 186, 186);
+ border-bottom-style: solid;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 20px;
}
.contextMenu-item:hover {
- transition: all 0.1s;
- background: $lighter-alt-accent;
+ transition: all 0.1s;
+ background: $lighter-alt-accent;
}
.contextMenu-description {
- text-align: left;
- width: 8vw;
+ font-size: 20px;
+ text-align: left;
+ display: inline; //need this?
}
+
+.icon-background {
+ pointer-events: none;
+ background-color: #DDDDDD;
+ width: 35px;
+ text-align: center;
+ font-size: 22px;
+} \ No newline at end of file
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 615a928ad..da374455e 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,14 +1,20 @@
import React = require("react");
import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem";
import { observable, action } from "mobx";
-import { observer } from "mobx-react";
-import "./ContextMenu.scss";
+import { observer } from "mobx-react"
+import "./ContextMenu.scss"
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons';
+
+library.add(faSearch);
+library.add(faCircle);
@observer
export class ContextMenu extends React.Component {
static Instance: ContextMenu;
- @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault() }];
+ @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }];
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
@observable private _display: string = "none";
@@ -75,6 +81,11 @@ export class ContextMenu extends React.Component {
return false;
}
+ @action
+ closeMenu = () => {
+ this.clearItems();
+ }
+
render() {
let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } :
{ left: this._pageX, bottom: this._pageY, display: this._display };
@@ -82,9 +93,14 @@ export class ContextMenu extends React.Component {
return (
<div className="contextMenu-cont" style={style} ref={this.ref}>
- <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange}></input>
+ <span>
+ <span className="icon-background">
+ <FontAwesomeIcon icon="search" size="lg" />
+ </span>
+ <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} />
+ </span>
{this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1).
- map(prop => <ContextMenuItem {...prop} key={prop.description} />)}
+ map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.closeMenu} />)}
</div>
);
}
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 70813f0dd..fcda0db89 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,25 +1,72 @@
import React = require("react");
+import { observable, action } from "mobx";
+import { observer } from "mobx-react";
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-export interface ContextMenuProps {
+export interface OriginalMenuProps {
description: string;
event: (e: React.MouseEvent<HTMLDivElement>) => void;
+ icon?: IconProp; //maybe should be optional (icon?)
+ closeMenu?: () => void;
}
export interface SubmenuProps {
description: string;
subitems: ContextMenuProps[];
+ closeMenu?: () => void;
}
export interface ContextMenuItemProps {
type: ContextMenuProps | SubmenuProps;
}
+export type ContextMenuProps = OriginalMenuProps | SubmenuProps;
+@observer
export class ContextMenuItem extends React.Component<ContextMenuProps> {
+ @observable private _items: Array<ContextMenuProps> = [];
+ @observable private overItem = false;
+
+ constructor(props: ContextMenuProps | SubmenuProps) {
+ super(props);
+ if ("subitems" in this.props) {
+ this.props.subitems.forEach(i => this._items.push(i));
+ }
+ }
+
+ handleEvent = (e: React.MouseEvent<HTMLDivElement>) => {
+ if ("event" in this.props) {
+ this.props.event(e);
+ this.props.closeMenu && this.props.closeMenu();
+ }
+ }
+
render() {
- return (
- <div className="contextMenu-item" onClick={this.props.event}>
- <div className="contextMenu-description">{this.props.description}</div>
- </div>
- );
+ if ("event" in this.props) {
+ return (
+ <div className="contextMenu-item" onClick={this.handleEvent}>
+ <span className="icon-background">
+ {this.props.icon ? <FontAwesomeIcon icon={this.props.icon} size="sm" /> : <FontAwesomeIcon icon="circle" size="sm" />}
+ </span>
+ <div className="contextMenu-description">
+ {this.props.description}
+ </div>
+ </div>
+ );
+ }
+ else {
+ let submenu = !this.overItem ? (null) :
+ <div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}>
+ {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
+ </div>;
+ return (
+ <div className="contextMenu-item" onMouseEnter={action(() => { this.overItem = true; })} onMouseLeave={action(() => this.overItem = false)}>
+ <div className="contextMenu-description">
+ {this.props.description}
+ </div>
+ {submenu}
+ </div>
+ );
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 158b02b5a..ba9f32d7d 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -65,7 +65,7 @@ $linkGap : 3px;
cursor: ew-resize;
}
.title{
- background: lightblue;
+ background: $alt-accent;
grid-column-start: 3;
grid-column-end: 4;
pointer-events: auto;
@@ -210,14 +210,15 @@ $linkGap : 3px;
position: absolute;
top: 0;
left: 30px;
- width: 150px;
- line-height: 25px;
- max-height: 175px;
+ width: max-content;
font-family: $sans-serif;
font-size: 12px;
background-color: $light-color-secondary;
padding: 2px 12px;
list-style: none;
+ .templateToggle {
+ text-align: left;
+ }
input {
margin-right: 10px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 693d6ec55..da9b1253e 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -5,7 +5,6 @@ import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import './DocumentDecorations.scss';
-import { MainOverlayTextBox } from "./MainOverlayTextBox";
import { DocumentView, PositionDocument } from "./nodes/DocumentView";
import { LinkMenu } from "./nodes/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
@@ -25,9 +24,9 @@ import { faLink } from '@fortawesome/free-solid-svg-icons';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
-import { CollectionFreeFormView } from "./collections/collectionFreeForm/CollectionFreeFormView";
import { CollectionView } from "./collections/CollectionView";
-import { createCipher } from "crypto";
+import { DocumentManager } from "../util/DocumentManager";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
import { FieldView } from "./nodes/FieldView";
library.add(faLink);
@@ -78,11 +77,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (SelectionManager.SelectedDocuments().length > 0) {
let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
if (typeof field === "number") {
- SelectionManager.SelectedDocuments().forEach(d =>
- d.props.Document[this._fieldKey] = +this._title);
+ SelectionManager.SelectedDocuments().forEach(d => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = +this._title;
+ });
} else {
- SelectionManager.SelectedDocuments().forEach(d =>
- d.props.Document[this._fieldKey] = this._title);
+ SelectionManager.SelectedDocuments().forEach(d => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = this._title;
+ });
}
}
}
@@ -93,7 +96,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this._downX = e.clientX;
this._downY = e.clientY;
e.stopPropagation();
- this.onBackgroundDown(e);
document.removeEventListener("pointermove", this.onTitleMove);
document.removeEventListener("pointerup", this.onTitleUp);
document.addEventListener("pointermove", this.onTitleMove);
@@ -236,6 +238,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this._removeIcon = snapped;
}
}
+ @undoBatch
@action
onMinimizeUp = (e: PointerEvent): void => {
e.stopPropagation();
@@ -246,26 +249,43 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) {
selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
}
- !this._removeIcon && selectedDocs.length === 1 && this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized());
+ if (!this._removeIcon) {
+ if (selectedDocs.length === 1) {
+ this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].toggleMinimized());
+ } else if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD &&
+ Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) {
+ let docViews = SelectionManager.ViewsSortedVertically();
+ let topDocView = docViews[0];
+ let ind = topDocView.templates.indexOf(Templates.Bullet.Layout);
+ if (ind !== -1) {
+ topDocView.templates.splice(ind, 1);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ topDocView.addTemplate(Templates.Bullet);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
+ }
+ }
+ }
this._removeIcon = false;
}
runInAction(() => this._minimizedX = this._minimizedY = 0);
}
+ @undoBatch
@action createIcon = (selected: DocumentView[], layoutString: string): Doc => {
let doc = selected[0].props.Document;
let iconDoc = Docs.IconDocument(layoutString);
iconDoc.isButton = true;
- iconDoc.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title);
- iconDoc.labelField = this._fieldKey;
- iconDoc[this._fieldKey] = selected.length > 1 ? "collection" : undefined;
- iconDoc.isMinimized = false;
+ iconDoc.proto!.title = selected.length > 1 ? "-multiple-.icon" : StrCast(doc.title) + ".icon";
+ iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey;
+ //iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined;
+ iconDoc.proto!.isMinimized = false;
iconDoc.width = Number(MINIMIZED_ICON_SIZE);
iconDoc.height = Number(MINIMIZED_ICON_SIZE);
iconDoc.x = NumCast(doc.x);
iconDoc.y = NumCast(doc.y) - 24;
- iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));
- doc.minimizedDoc = iconDoc;
+ iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document.proto!));
+ selected.length === 1 && (doc.minimizedDoc = iconDoc);
selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);
return iconDoc;
}
@@ -273,13 +293,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => {
let doc = docView.props.Document;
let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
- if (!iconDoc) {
+
+ if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
const layout = StrCast(doc.backgroundLayout, StrCast(doc.layout, FieldView.LayoutString(DocumentView)));
iconDoc = this.createIcon([docView], layout);
}
- if (SelectionManager.SelectedDocuments()[0].props.addDocument !== undefined) {
- SelectionManager.SelectedDocuments()[0].props.addDocument!(iconDoc!);
- }
return iconDoc;
}
moveIconDoc(iconDoc: Doc) {
@@ -318,6 +336,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
}
+ @action
onLinkerButtonMoved = (e: PointerEvent): void => {
if (this._linkerButton.current !== null) {
document.removeEventListener("pointermove", this.onLinkerButtonMoved);
@@ -325,6 +344,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let selDoc = SelectionManager.SelectedDocuments()[0];
let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined;
let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
+ FormattedTextBox.InputBoxOverlay = undefined;
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
dragComplete: action(emptyFunction),
@@ -407,35 +427,34 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
break;
}
- MainOverlayTextBox.Instance.SetTextDoc();
+ runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect();
- if (rect.width !== 0) {
+ if (rect.width !== 0 && (dX != 0 || dY != 0 || dW != 0 || dH != 0)) {
let doc = PositionDocument(element.props.Document);
- let width = FieldValue(doc.width, 0);
- let nwidth = FieldValue(doc.nativeWidth, 0);
- let nheight = FieldValue(doc.nativeHeight, 0);
- let height = FieldValue(doc.height, nwidth ? nheight / nwidth * width : 0);
- let x = FieldValue(doc.x, 0);
- let y = FieldValue(doc.y, 0);
+ let docHeightBefore = doc.height;
+ let nwidth = doc.nativeWidth || 0;
+ let nheight = doc.nativeHeight || 0;
+ let zoomBasis = NumCast(doc.zoomBasis, 1);
+ let width = (doc.width || 0) / zoomBasis;
+ let height = (doc.height || (nheight / nwidth * width)) / zoomBasis;
let scale = width / rect.width;
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
- x += dX * (actualdW - width);
- y += dY * (actualdH - height);
- doc.x = x;
- doc.y = y;
- var nativeWidth = FieldValue(doc.nativeWidth, 0);
- var nativeHeight = FieldValue(doc.nativeHeight, 0);
- if (nativeWidth > 0 && nativeHeight > 0) {
+ doc.x = (doc.x || 0) + dX * (actualdW - width);
+ doc.y = (doc.y || 0) + dY * (actualdH - height);
+ if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
- actualdH = nativeHeight / nativeWidth * actualdW;
+ doc.zoomBasis = zoomBasis * width / actualdW;
}
- else actualdW = nativeWidth / nativeHeight * actualdH;
+ else {
+ doc.zoomBasis = zoomBasis * height / actualdH;
+ }
+ } else {
+ doc.width = zoomBasis * actualdW;
+ if (docHeightBefore === doc.height) doc.height = zoomBasis * actualdH;
}
- doc.width = actualdW;
- doc.height = actualdH;
}
});
}
@@ -503,9 +522,23 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
let templates: Map<Template, boolean> = new Map();
- let doc = SelectionManager.SelectedDocuments()[0];
Array.from(Object.values(Templates.TemplateList)).map(template => {
- let docTemps = doc.templates;
+ let sorted = SelectionManager.ViewsSortedVertically().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
+ if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ return 0;
+ });
+ let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
+ let temps = doc.props.Document.templates;
+ if (temps instanceof List) {
+ temps.map(temp => {
+ if (temp !== Templates.Bullet.Layout || i === 0) {
+ res.push(temp);
+ }
+ });
+ }
+ return res;
+ }, [] as string[]);
let checked = false;
docTemps.forEach(temp => {
if (template.Layout === temp) {
@@ -556,7 +589,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
<FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" />
</div>
</div>
- <TemplateMenu doc={doc} templates={templates} />
+ <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
</div>
</div >
</div>
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index ea401eaf9..dfa110f8d 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -1,5 +1,20 @@
-.editableView-container-editing {
+.editableView-container-editing, .editableView-container-editing-oneLine {
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
+ overflow: hidden;
+}
+.editableView-container-editing-oneLine {
+ span {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display:block;
+ }
+ input {
+ display:block;
+ }
+}
+.editableView-input {
+ width: 100%;
} \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 73467eb9d..c946d68e1 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -22,8 +22,9 @@ export interface EditableProps {
* The contents to render when not editing
*/
contents: any;
- height: number;
+ height?: number;
display?: string;
+ oneLine?: boolean;
}
/**
@@ -52,14 +53,20 @@ export class EditableView extends React.Component<EditableProps> {
}
}
+ @action
+ onClick = (e: React.MouseEvent) => {
+ this.editing = true;
+ e.stopPropagation();
+ }
+
render() {
if (this.editing) {
- return <input defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)}
- style={{ display: this.props.display }}></input>;
+ return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)}
+ style={{ display: this.props.display }} />;
} else {
return (
- <div className="editableView-container-editing" style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
- onClick={action(() => this.editing = true)} >
+ <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
+ onClick={this.onClick} >
<span>{this.props.contents}</span>
</div>
);
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index 2c550051c..d95398f17 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -1,35 +1,50 @@
@import "globalCssVariables";
.inkingCanvas {
- opacity:0.99;
+ opacity: 0.99;
+
+ .jsx-parser {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: -1; // allows annotations to appear on videos when screen is full-size & ...
+ }
}
-.inkingCanvas-paths-ink, .inkingCanvas-paths-markers, .inkingCanvas-noSelect, .inkingCanvas-canSelect {
+
+.inkingCanvas-paths-ink,
+.inkingCanvas-paths-markers,
+.inkingCanvas-noSelect,
+.inkingCanvas-canSelect {
position: absolute;
top: 0;
- left:0;
+ left: 0;
width: 8192px;
height: 8192px;
- cursor:"crosshair";
+ cursor: "crosshair";
pointer-events: auto;
-
+
}
-.inkingCanvas-canSelect,
-.inkingCanvas-noSelect {
- top:-50000px;
- left:-50000px;
+
+.inkingCanvas-canSelect,
+.inkingCanvas-noSelect {
+ top: -50000px;
+ left: -50000px;
width: 100000px;
height: 100000px;
}
-.inkingCanvas-noSelect {
+
+.inkingCanvas-noSelect {
pointer-events: none;
cursor: "arrow";
}
-.inkingCanvas-paths-ink, .inkingCanvas-paths-markers {
+
+.inkingCanvas-paths-ink,
+.inkingCanvas-paths-markers {
pointer-events: none;
z-index: 10000; // overlays ink on top of everything
cursor: "arrow";
}
+
.inkingCanvas-paths-markers {
mix-blend-mode: multiply;
-}
-
+} \ No newline at end of file
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 1c0d13545..42ab08001 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -30,6 +30,14 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
selRect.top < val.y && selRect.top + selRect.height > val.y)
, false);
}
+ public static StrokeRect(stroke: StrokeData): { left: number, top: number, right: number, bottom: number } {
+ return stroke.pathData.reduce((bounds: { left: number, top: number, right: number, bottom: number }, val) =>
+ ({
+ left: Math.min(bounds.left, val.x), top: Math.min(bounds.top, val.y),
+ right: Math.max(bounds.right, val.x), bottom: Math.max(bounds.bottom, val.y)
+ })
+ , { left: Number.MAX_VALUE, top: Number.MAX_VALUE, right: -Number.MAX_VALUE, bottom: -Number.MAX_VALUE });
+ }
componentDidMount() {
PromiseValue(Cast(this.props.Document.ink, InkField)).then(ink => runInAction(() => {
@@ -138,7 +146,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
get drawnPaths() {
let curPage = NumCast(this.props.Document.curPage, -1);
let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
- if (strokeData.page === -1 || strokeData.page === curPage) {
+ if (strokeData.page === -1 || Math.round(strokeData.page) === Math.round(curPage)) {
paths.push(<InkingStroke key={id} id={id}
line={strokeData.pathData}
count={strokeData.pathData.length}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 4b3dbd4e0..d456f531f 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -35,12 +35,7 @@ export class InkingControl extends React.Component {
@action
switchColor = (color: ColorResult): void => {
this._selectedColor = color.hex;
- if (SelectionManager.SelectedDocuments().length === 1) {
- var sdoc = SelectionManager.SelectedDocuments()[0];
- if (sdoc.props.ContainingCollectionView) {
- Doc.SetOnPrototype(sdoc.props.Document, "backgroundColor", color.hex);
- }
- }
+ SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = color.hex);
}
@action
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index cbf920793..57a53c999 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -15,6 +15,9 @@ body {
div {
user-select: none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
}
#dash-title {
@@ -27,27 +30,8 @@ div {
z-index: 9999;
}
-h1 {
- font-size: 50px;
- position: fixed;
- top: 30px;
- left: 50%;
- transform: translateX(-50%);
- color: $dark-color;
- text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
- z-index: 9999;
- font-family: $sans-serif;
- font-weight: 700;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
.jsx-parser {
- width:100%;
+ width: 100%;
pointer-events: none;
border-radius: inherit;
}
@@ -59,7 +43,7 @@ p {
::-webkit-scrollbar {
-webkit-appearance: none;
- height: 5px;
+ height: 10px;
width: 10px;
}
@@ -116,6 +100,21 @@ button:hover {
right: 0px;
}
+.main-notifs-badge {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ color: white;
+ background: #f44b42;
+ font-weight: 300;
+ border-radius: 100%;
+ width: 25px;
+ height: 25px;
+ text-align: center;
+ padding-top: 4px;
+ font-size: 12px;
+}
+
//toolbar stuff
#toolbar {
position: absolute;
@@ -182,7 +181,9 @@ button:hover {
top: 0;
left: 0;
overflow: scroll;
+ z-index: 1;
}
+
#mainContent-div {
width: 100%;
height: 100%;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index c3b48d20f..3d9750a85 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,342 +1,11 @@
-import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import "normalize.css";
-import * as React from 'react';
+import { MainView } from "./MainView";
+import { Docs } from "../documents/Documents";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
import * as ReactDOM from 'react-dom';
-import Measure from 'react-measure';
-import * as request from 'request';
-import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
-import { Docs } from '../documents/Documents';
-import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel';
-import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel';
-import { Gateway, NorthstarSettings } from '../northstar/manager/Gateway';
-import { AggregateFunction, Catalog } from '../northstar/model/idea/idea';
-import '../northstar/model/ModelExtensions';
-import { HistogramOperation } from '../northstar/operations/HistogramOperation';
-import '../northstar/utils/Extensions';
-import { SetupDrag, DragManager } from '../util/DragManager';
-import { Transform } from '../util/Transform';
-import { UndoManager } from '../util/UndoManager';
-import { PresentationView } from './PresentationView';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import { ContextMenu } from './ContextMenu';
-import { DocumentDecorations } from './DocumentDecorations';
-import { InkingControl } from './InkingControl';
-import "./Main.scss";
-import { MainOverlayTextBox } from './MainOverlayTextBox';
-import { DocumentView } from './nodes/DocumentView';
-import { PreviewCursor } from './PreviewCursor';
-import { SelectionManager } from '../util/SelectionManager';
-import { FieldResult, Field, Doc, Opt } from '../../new_fields/Doc';
-import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
-import { DocServer } from '../DocServer';
-import { listSpec } from '../../new_fields/Schema';
-import { Id } from '../../new_fields/RefField';
-
-
-@observer
-export class Main extends React.Component {
- public static Instance: Main;
- @observable private _workspacesShown: boolean = false;
- @observable public pwidth: number = 0;
- @observable public pheight: number = 0;
-
- @computed private get mainContainer(): Opt<Doc> {
- return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
- }
- private set mainContainer(doc: Opt<Doc>) {
- if (doc) {
- if (!("presentationView" in doc)) {
- doc.presentationView = new Doc();
- }
- CurrentUserUtils.UserDocument.activeWorkspace = doc;
- }
- }
-
- constructor(props: Readonly<{}>) {
- super(props);
- Main.Instance = this;
- // causes errors to be generated when modifying an observable outside of an action
- configure({ enforceActions: "observed" });
- if (window.location.pathname !== RouteStore.home) {
- let pathname = window.location.pathname.split("/");
- if (pathname.length > 1 && pathname[pathname.length - 2] === 'doc') {
- CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
- }
- }
-
- CurrentUserUtils.loadCurrentUser();
-
- library.add(faFont);
- library.add(faImage);
- library.add(faFilePdf);
- library.add(faObjectGroup);
- library.add(faTable);
- library.add(faGlobeAsia);
- library.add(faUndoAlt);
- library.add(faRedoAlt);
- library.add(faPenNib);
- library.add(faFilm);
- library.add(faMusic);
- library.add(faTree);
-
- this.initEventListeners();
- this.initAuthenticationRouters();
-
- // try {
- // this.initializeNorthstar();
- // } catch (e) {
-
- // }
- }
-
- componentDidMount() { window.onpopstate = this.onHistory; }
-
- componentWillUnmount() { window.onpopstate = null; }
-
- onHistory = () => {
- if (window.location.pathname !== RouteStore.home) {
- let pathname = window.location.pathname.split("/");
- DocServer.GetRefField(pathname[pathname.length - 1]).then(action((field: Opt<Field>) => {
- if (field instanceof Doc) {
- this.openWorkspace(field, true);
- }
- }));
- }
- }
-
- initEventListeners = () => {
- // window.addEventListener("pointermove", (e) => this.reportLocation(e))
- window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
- window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
- window.addEventListener("keydown", (e) => {
- if (e.key === "Escape") {
- DragManager.AbortDrag();
- SelectionManager.DeselectAll();
- }
- }, false); // drag event handler
- // click interactions for the context menu
- document.addEventListener("pointerdown", action(function (e: PointerEvent) {
- if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) {
- ContextMenu.Instance.clearItems();
- }
- }), true);
- }
-
- initAuthenticationRouters = async () => {
- // Load the user's active workspace, or create a new one if initial session after signup
- if (!CurrentUserUtils.MainDocId) {
- const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
- if (doc) {
- this.openWorkspace(doc);
- } else {
- this.createNewWorkspace();
- }
- } else {
- DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
- field instanceof Doc ? this.openWorkspace(field) :
- this.createNewWorkspace(CurrentUserUtils.MainDocId));
- }
- }
-
- @action
- createNewWorkspace = async (id?: string) => {
- const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
- if (list) {
- let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` });
- var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
- let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` });
- list.push(mainDoc);
- // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
- setTimeout(() => {
- this.openWorkspace(mainDoc);
- let pendingDocument = Docs.SchemaDocument([], { title: "New Mobile Uploads" });
- mainDoc.optionalRightCollection = pendingDocument;
- }, 0);
- }
- }
-
- @action
- openWorkspace = async (doc: Doc, fromHistory = false) => {
- CurrentUserUtils.MainDocId = doc[Id];
- this.mainContainer = doc;
- fromHistory || window.history.pushState(null, StrCast(doc.title), "/doc/" + doc[Id]);
- const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
- // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
- setTimeout(async () => {
- if (col) {
- const l = Cast(col.data, listSpec(Doc));
- if (l && l.length > 0) {
- CollectionDockingView.Instance.AddRightSplit(col);
- }
- }
- }, 100);
- }
-
- @computed
- get presentationView() {
- if (this.mainContainer) {
- let presentation = FieldValue(Cast(this.mainContainer.presentationView, Doc));
- return presentation ? <PresentationView Document={presentation} key="presentation" /> : (null);
- }
- return (null);
- }
-
- @computed
- get mainContent() {
- let pwidthFunc = () => this.pwidth;
- let pheightFunc = () => this.pheight;
- let noScaling = () => 1;
- let mainCont = this.mainContainer;
- let pcontent = this.presentationView;
- return <Measure onResize={action((r: any) => { this.pwidth = r.entry.width; this.pheight = r.entry.height; })}>
- {({ measureRef }) =>
- <div ref={measureRef} id="mainContent-div">
- {!mainCont ? (null) :
- <DocumentView Document={mainCont}
- toggleMinimized={emptyFunction}
- addDocument={undefined}
- removeDocument={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={noScaling}
- PanelWidth={pwidthFunc}
- PanelHeight={pheightFunc}
- isTopMost={true}
- selectOnLoad={false}
- focus={emptyFunction}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- ContainingCollectionView={undefined} />}
- {pcontent}
- </div>
- }
- </Measure>;
- }
-
- /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
- nodesMenu() {
-
- let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
- let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";
- let weburl = "https://cs.brown.edu/courses/cs166/";
- let audiourl = "http://techslides.com/demos/samples/sample.mp3";
- let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
-
- let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
- let addColNode = action(() => Docs.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
- let addSchemaNode = action(() => Docs.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" }));
- let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
- // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
- let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" }));
- let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
- let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
- let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
- let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
-
- let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
- [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
- [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
- [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
- [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode],
- [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode],
- [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode],
- [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
- [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
- [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
- ];
-
- return < div id="add-nodes-menu" >
- <input type="checkbox" id="add-menu-toggle" />
- <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
-
- <div id="add-options-content">
- <ul id="add-options-list">
- {btns.map(btn =>
- <li key={btn[1]} ><div ref={btn[0]}>
- <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
- <FontAwesomeIcon icon={btn[1]} size="sm" />
- </button>
- </div></li>)}
- </ul>
- </div>
- </div >;
- }
-
- /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
- @computed
- get miscButtons() {
- let logoutRef = React.createRef<HTMLDivElement>();
-
- return [
- <button className="clear-db-button" key="clear-db" onClick={DocServer.DeleteDatabase}>Clear Database</button>,
- <div id="toolbar" key="toolbar">
- <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
- <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
- <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
- </div >,
- <div className="main-buttonDiv" key="logout" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}>
- <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
- ];
- }
-
- render() {
- return (
- <div id="main-div">
- <DocumentDecorations />
- {this.mainContent}
- <PreviewCursor />
- <ContextMenu />
- {this.nodesMenu()}
- {this.miscButtons}
- <InkingControl />
- <MainOverlayTextBox />
- </div>
- );
- }
-
- // --------------- Northstar hooks ------------- /
- private _northstarSchemas: Doc[] = [];
-
- @action SetNorthstarCatalog(ctlog: Catalog) {
- CurrentUserUtils.NorthstarDBCatalog = ctlog;
- if (ctlog && ctlog.schemas) {
- ctlog.schemas.map(schema => {
- let schemaDocuments: Doc[] = [];
- let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema);
- Promise.all(attributesToBecomeDocs.reduce((promises, attr) => {
- promises.push(DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
- if (field instanceof Doc) {
- schemaDocuments.push(field);
- } else {
- var atmod = new ColumnAttributeModel(attr);
- let histoOp = new HistogramOperation(schema.displayName!,
- new AttributeTransformationModel(atmod, AggregateFunction.None),
- new AttributeTransformationModel(atmod, AggregateFunction.Count),
- new AttributeTransformationModel(atmod, AggregateFunction.Count));
- schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
- }
- })));
- return promises;
- }, [] as Promise<void>[])).finally(() =>
- this._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })));
- });
- }
- }
- async initializeNorthstar(): Promise<void> {
- const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" });
- NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json());
- Gateway.Instance.ClearCatalog().then(async () => this.SetNorthstarCatalog(await Gateway.Instance.GetCatalog()));
- }
-}
+import * as React from 'react';
(async () => {
await Docs.initProtos();
await CurrentUserUtils.loadCurrentUser();
- ReactDOM.render(<Main />, document.getElementById('root'));
+ ReactDOM.render(<MainView />, document.getElementById('root'));
})();
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index d32e3f21b..24327b995 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -1,16 +1,14 @@
-import { action, observable, trace } from 'mobx';
+import { action, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import "normalize.css";
import * as React from 'react';
import { emptyFunction, returnTrue, returnZero } from '../../Utils';
-import '../northstar/model/ModelExtensions';
-import '../northstar/utils/Extensions';
import { DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
+import "normalize.css";
import "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
+import { CollectionDockingView } from './collections/CollectionDockingView';
import { Doc } from '../../new_fields/Doc';
-import { NumCast } from '../../new_fields/Types';
interface MainOverlayTextBoxProps {
}
@@ -18,11 +16,10 @@ interface MainOverlayTextBoxProps {
@observer
export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
public static Instance: MainOverlayTextBox;
- @observable public TextDoc?: Doc = undefined;
- public TextScroll: number = 0;
@observable _textXf: () => Transform = () => Transform.Identity();
private _textFieldKey: string = "data";
private _textColor: string | null = null;
+ private _textHideOnLeave?: boolean;
private _textTargetDiv: HTMLDivElement | undefined;
private _textProxyDiv: React.RefObject<HTMLDivElement>;
@@ -30,30 +27,32 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
super(props);
this._textProxyDiv = React.createRef();
MainOverlayTextBox.Instance = this;
+ reaction(() => FormattedTextBox.InputBoxOverlay,
+ (box?: FormattedTextBox) => {
+ if (box) this.setTextDoc(box.props.fieldKey, box.CurrentDiv, box.props.ScreenToLocalTransform);
+ else this.setTextDoc();
+ });
}
@action
- SetTextDoc(textDoc?: Doc, textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) {
+ private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) {
if (this._textTargetDiv) {
this._textTargetDiv.style.color = this._textColor;
}
-
- this.TextDoc = textDoc;
this._textFieldKey = textFieldKey!;
this._textXf = tx ? tx : () => Transform.Identity();
this._textTargetDiv = div;
+ this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
if (div) {
- this._textColor = div.style.color;
+ this._textColor = (getComputedStyle(div) as any).color;
div.style.color = "transparent";
- this.TextScroll = div.scrollTop;
}
}
@action
textScroll = (e: React.UIEvent) => {
if (this._textProxyDiv.current && this._textTargetDiv) {
- this.TextScroll = (e as any)._targetInst.stateNode.scrollTop;// this._textProxyDiv.current.children[0].scrollTop;
- this._textTargetDiv.scrollTop = this.TextScroll;
+ this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop;
}
}
@@ -63,11 +62,12 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
document.addEventListener('pointerup', this.textBoxUp);
}
}
+ @action
textBoxMove = (e: PointerEvent) => {
if (e.movementX > 1 || e.movementY > 1) {
document.removeEventListener("pointermove", this.textBoxMove);
document.removeEventListener('pointerup', this.textBoxUp);
- let dragData = new DragManager.DocumentDragData([this.TextDoc!]);
+ let dragData = new DragManager.DocumentDragData(FormattedTextBox.InputBoxOverlay ? [FormattedTextBox.InputBoxOverlay.props.Document] : []);
const [left, top] = this._textXf().inverse().transformPoint(0, 0);
dragData.xOffset = e.clientX - left;
dragData.yOffset = e.clientY - top;
@@ -84,19 +84,24 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
document.removeEventListener('pointerup', this.textBoxUp);
}
+ addDocTab = (doc: Doc, location: string) => {
+ if (true) { // location === "onRight") { need to figure out stack to add "inTab"
+ CollectionDockingView.Instance.AddRightSplit(doc);
+ }
+ }
render() {
- if (this.TextDoc && this._textTargetDiv) {
+ if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
let textRect = this._textTargetDiv.getBoundingClientRect();
let s = this._textXf().Scale;
return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${textRect.top}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} >
<div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
style={{ width: `${textRect.width * s}px`, height: `${textRect.height * s}px` }}>
- <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={this.TextDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
+ <FormattedTextBox fieldKey={this._textFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
- ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} />
+ ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} />
</div>
</ div>;
}
- else return (null);
+ else return (null); Z
}
} \ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
new file mode 100644
index 000000000..a093ffdec
--- /dev/null
+++ b/src/client/views/MainView.tsx
@@ -0,0 +1,316 @@
+import { IconName, library } from '@fortawesome/fontawesome-svg-core';
+import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, configure, observable, runInAction, trace } from 'mobx';
+import { observer } from 'mobx-react';
+import "normalize.css";
+import * as React from 'react';
+import Measure from 'react-measure';
+import * as request from 'request';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
+import { RouteStore } from '../../server/RouteStore';
+import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
+import { Docs } from '../documents/Documents';
+import { SetupDrag, DragManager } from '../util/DragManager';
+import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
+import { PresentationView } from './PresentationView';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+import { ContextMenu } from './ContextMenu';
+import { DocumentDecorations } from './DocumentDecorations';
+import { InkingControl } from './InkingControl';
+import "./Main.scss";
+import { MainOverlayTextBox } from './MainOverlayTextBox';
+import { DocumentView } from './nodes/DocumentView';
+import { PreviewCursor } from './PreviewCursor';
+import { SearchBox } from './SearchBox';
+import { SelectionManager } from '../util/SelectionManager';
+import { FieldResult, Field, Doc, Opt, DocListCast } from '../../new_fields/Doc';
+import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { DocServer } from '../DocServer';
+import { listSpec } from '../../new_fields/Schema';
+import { Id } from '../../new_fields/FieldSymbols';
+import { HistoryUtil } from '../util/History';
+import { CollectionBaseView } from './collections/CollectionBaseView';
+
+
+@observer
+export class MainView extends React.Component {
+ public static Instance: MainView;
+ @observable private _workspacesShown: boolean = false;
+ @observable public pwidth: number = 0;
+ @observable public pheight: number = 0;
+ @computed private get mainContainer(): Opt<Doc> {
+ return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
+ }
+ private set mainContainer(doc: Opt<Doc>) {
+ if (doc) {
+ if (!("presentationView" in doc)) {
+ doc.presentationView = Docs.TreeDocument([], { title: "Presentation" });
+ }
+ CurrentUserUtils.UserDocument.activeWorkspace = doc;
+ }
+ }
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ MainView.Instance = this;
+ // causes errors to be generated when modifying an observable outside of an action
+ configure({ enforceActions: "observed" });
+ if (window.location.search.includes("readonly")) {
+ DocServer.makeReadOnly();
+ }
+ if (window.location.search.includes("safe")) {
+ if (!window.location.search.includes("nro")) {
+ DocServer.makeReadOnly();
+ }
+ CollectionBaseView.SetSafeMode(true);
+ }
+ if (window.location.pathname !== RouteStore.home) {
+ let pathname = window.location.pathname.substr(1).split("/");
+ if (pathname.length > 1) {
+ let type = pathname[0];
+ if (type === "doc") {
+ CurrentUserUtils.MainDocId = pathname[1];
+ }
+ }
+ }
+
+ library.add(faFont);
+ library.add(faImage);
+ library.add(faFilePdf);
+ library.add(faObjectGroup);
+ library.add(faTable);
+ library.add(faGlobeAsia);
+ library.add(faUndoAlt);
+ library.add(faRedoAlt);
+ library.add(faPenNib);
+ library.add(faFilm);
+ library.add(faMusic);
+ library.add(faTree);
+ this.initEventListeners();
+ this.initAuthenticationRouters();
+ }
+
+ initEventListeners = () => {
+ // window.addEventListener("pointermove", (e) => this.reportLocation(e))
+ window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
+ window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
+ window.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ DragManager.AbortDrag();
+ SelectionManager.DeselectAll();
+ }
+ }, false); // drag event handler
+ // click interactions for the context menu
+ document.addEventListener("pointerdown", action(function (e: PointerEvent) {
+
+ const targets = document.elementsFromPoint(e.x, e.y);
+ if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
+ ContextMenu.Instance.clearItems();
+ }
+ }), true);
+ }
+
+ initAuthenticationRouters = async () => {
+ // Load the user's active workspace, or create a new one if initial session after signup
+ if (!CurrentUserUtils.MainDocId) {
+ const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
+ if (doc) {
+ this.openWorkspace(doc);
+ } else {
+ this.createNewWorkspace();
+ }
+ } else {
+ DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
+ field instanceof Doc ? this.openWorkspace(field) :
+ this.createNewWorkspace(CurrentUserUtils.MainDocId));
+ }
+ }
+
+ @action
+ createNewWorkspace = async (id?: string) => {
+ const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
+ if (list) {
+ let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
+ var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
+ let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
+ list.push(mainDoc);
+ // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
+ setTimeout(() => {
+ this.openWorkspace(mainDoc);
+ // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" });
+ // mainDoc.optionalRightCollection = pendingDocument;
+ }, 0);
+ }
+ }
+
+ @observable _notifsCol: Opt<Doc>;
+
+ @action
+ openWorkspace = async (doc: Doc, fromHistory = false) => {
+ CurrentUserUtils.MainDocId = doc[Id];
+ this.mainContainer = doc;
+ fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} });
+ const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
+ // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
+ setTimeout(async () => {
+ if (col) {
+ const l = Cast(col.data, listSpec(Doc));
+ if (l) {
+ runInAction(() => this._notifsCol = col);
+ }
+ }
+ }, 100);
+ }
+
+ openNotifsCol = () => {
+ if (this._notifsCol && CollectionDockingView.Instance) {
+ CollectionDockingView.Instance.AddRightSplit(this._notifsCol);
+ }
+ }
+
+ @action
+ onResize = (r: any) => {
+ this.pwidth = r.offset.width;
+ this.pheight = r.offset.height;
+ }
+ getPWidth = () => {
+ return this.pwidth;
+ }
+ getPHeight = () => {
+ return this.pheight;
+ }
+ @computed
+ get mainContent() {
+ let mainCont = this.mainContainer;
+ let content = !mainCont ? (null) :
+ <DocumentView Document={mainCont}
+ addDocument={undefined}
+ addDocTab={emptyFunction}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.getPWidth}
+ PanelHeight={this.getPHeight}
+ isTopMost={true}
+ selectOnLoad={false}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined} />;
+ const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined;
+ return <Measure offset onResize={this.onResize}>
+ {({ measureRef }) =>
+ <div ref={measureRef} id="mainContent-div">
+ {content}
+ {pres ? <PresentationView Document={pres} key="presentation" /> : null}
+ </div>
+ }
+ </Measure>;
+ }
+
+ /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
+ nodesMenu() {
+
+ let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
+ let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";
+ let weburl = "https://cs.brown.edu/courses/cs166/";
+ let audiourl = "http://techslides.com/demos/samples/sample.mp3";
+ let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
+
+ let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
+ let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
+ let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
+ let addTreeNode = action(() => CurrentUserUtils.UserDocument);
+ //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
+ // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
+ let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" }));
+ let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
+ let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
+ let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
+ let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
+
+ let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
+ [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
+ [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
+ [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
+ [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode],
+ [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode],
+ [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode],
+ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
+ [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
+ [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
+ ];
+
+ return < div id="add-nodes-menu" >
+ <input type="checkbox" id="add-menu-toggle" />
+ <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
+
+ <div id="add-options-content">
+ <ul id="add-options-list">
+ {btns.map(btn =>
+ <li key={btn[1]} ><div ref={btn[0]}>
+ <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
+ <FontAwesomeIcon icon={btn[1]} size="sm" />
+ </button>
+ </div></li>)}
+ </ul>
+ </div>
+ </div >;
+ }
+
+ /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
+ @computed
+ get miscButtons() {
+ const length = this._notifsCol ? DocListCast(this._notifsCol.data).length : 0;
+ const notifsRef = React.createRef<HTMLDivElement>();
+ const dragNotifs = action(() => this._notifsCol!);
+ let logoutRef = React.createRef<HTMLDivElement>();
+
+ return [
+ <button className="clear-db-button" key="clear-db" onClick={e => e.shiftKey && e.altKey && DocServer.DeleteDatabase()}>Clear Database</button>,
+ <div id="toolbar" key="toolbar">
+ <div ref={notifsRef}>
+ <button className="toolbar-button round-button" title="Notifs"
+ onClick={this.openNotifsCol} onPointerDown={this._notifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
+ <FontAwesomeIcon icon={faBell} size="sm" />
+ </button>
+ <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}>
+ {length}
+ </div>
+ </div>
+ <button className="toolbar-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
+ </div >,
+ this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div> : null,
+ <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
+ <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
+ ];
+
+ }
+
+ @observable isSearchVisible = false;
+ @action
+ toggleSearch = () => {
+ this.isSearchVisible = !this.isSearchVisible;
+ }
+
+ render() {
+ return (
+ <div id="main-div">
+ <DocumentDecorations />
+ {this.mainContent}
+ <PreviewCursor />
+ <ContextMenu />
+ {this.nodesMenu()}
+ {this.miscButtons}
+ <InkingControl />
+ <MainOverlayTextBox />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/PresentationView.scss b/src/client/views/PresentationView.scss
index 7c5677f0d..fb4a851c4 100644
--- a/src/client/views/PresentationView.scss
+++ b/src/client/views/PresentationView.scss
@@ -4,15 +4,14 @@
z-index: 1;
box-shadow: #AAAAAA .2vw .2vw .4vw;
right: 0;
- top:0;
- bottom:0;
+ top: 0;
+ bottom: 0;
}
.presentationView-item {
- width: 220px;
- height: 40px;
- vertical-align: center;
- padding-top: 15px;
+ padding: 10px;
+ display: inline-block;
+ width: 100%;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
@@ -22,47 +21,59 @@
transition: all .1s;
}
+.presentationView-listCont {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
.presentationView-item:hover {
transition: all .1s;
background: #AAAAAA
}
+.presentationView-selected {
+ background: gray;
+}
+
.presentationView-heading {
- margin-top: 0px;
- height: 40px;
background: lightseagreen;
- padding: 30px;
+ padding: 10px;
+ display: inline-block;
+ width: 100%;
}
+
.presentationView-title {
- padding-top: 3px;
- padding-bottom: 3px;
- font-size: 25px;
- float:left;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ font-size: 25px;
+ display: inline-block;
}
-.presentation-icon{
+
+.presentation-icon {
float: right;
- display: inline;
- width: 10px;
- margin-top: 7px;
}
-.presentationView-header {
- padding-top: 1px;
- padding-bottom: 1px;
+
+.presentationView-name {
font-size: 15px;
- float:left;
- }
+ display: inline-block;
+}
+
+.presentation-button {
+ margin-right: 12.5%;
+ margin-left: 12.5%;
+ width: 25%;
+}
- .presentation-next{
- float: right;
- }
- .presentation-back{
- float: left;
- }
- .presentation-next:hover{
+.presentation-buttons {
+ padding: 10px;
+}
+
+.presentation-next:hover {
transition: all .1s;
background: #AAAAAA
}
-.presentation-back:hover{
+
+.presentation-back:hover {
transition: all .1s;
background: #AAAAAA
} \ No newline at end of file
diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx
index b4c12d057..9d5798ff1 100644
--- a/src/client/views/PresentationView.tsx
+++ b/src/client/views/PresentationView.tsx
@@ -1,86 +1,74 @@
import { observer } from "mobx-react";
-import React = require("react")
-import { observable, action } from "mobx";
-import "./PresentationView.scss"
-import "./Main.tsx";
+import React = require("react");
+import { observable, action, runInAction, reaction } from "mobx";
+import "./PresentationView.scss";
import { DocumentManager } from "../util/DocumentManager";
import { Utils } from "../../Utils";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, DocListCast, DocListCastAsync } from "../../new_fields/Doc";
import { listSpec } from "../../new_fields/Schema";
-import { Cast, NumCast, FieldValue } from "../../new_fields/Types";
-import { Id } from "../../new_fields/RefField";
+import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../new_fields/Types";
+import { Id } from "../../new_fields/FieldSymbols";
import { List } from "../../new_fields/List";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
export interface PresViewProps {
Document: Doc;
}
+interface PresListProps extends PresViewProps {
+ deleteDocument(index: number): void;
+ gotoDocument(index: number): void;
+}
+
@observer
/**
* Component that takes in a document prop and a boolean whether it's collapsed or not.
*/
-class PresentationViewItem extends React.Component<PresViewProps> {
-
- //look at CollectionFreeformView.focusDocument(d)
- @action
- openDoc = (doc: Doc) => {
- let docView = DocumentManager.Instance.getDocumentView(doc);
- if (docView) {
- docView.props.focus(docView.props.Document);
- }
- }
+class PresentationViewList extends React.Component<PresListProps> {
- /**
- * Removes a document from the presentation view
- **/
- @action
- public RemoveDoc(doc: Doc) {
- const value = Cast(this.props.Document.data, listSpec(Doc), []);
- let index = -1;
- for (let i = 0; i < value.length; i++) {
- if (value[i][Id] === doc[Id]) {
- index = i;
- break;
- }
- }
- if (index !== -1) {
- value.splice(index, 1);
- }
- }
/**
* Renders a single child document. It will just append a list element.
* @param document The document to render.
*/
- renderChild(document: Doc) {
+ renderChild = (document: Doc, index: number) => {
let title = document.title;
//to get currently selected presentation doc
let selected = NumCast(this.props.Document.selectedDoc, 0);
- // finally, if it's a normal document, then render it as such.
- const children = Cast(this.props.Document.data, listSpec(Doc));
- const styles: any = {};
- if (children && children[selected] === document) {
+ let className = "presentationView-item";
+ if (selected === index) {
//this doc is selected
- styles.background = "gray";
+ className += " presentationView-selected";
}
+ let onEnter = (e: React.PointerEvent) => { document.libraryBrush = true; }
+ let onLeave = (e: React.PointerEvent) => { document.libraryBrush = false; }
return (
- <li className="presentationView-item" style={styles} key={Utils.GenerateGuid()}>
- <div className="presentationView-header" onClick={() => this.openDoc(document)}>{title}</div>
- <div className="presentation-icon" onClick={() => this.RemoveDoc(document)}>X</div>
- </li>
+ <div className={className} key={document[Id] + index}
+ onPointerEnter={onEnter} onPointerLeave={onLeave}
+ style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(document.libraryBrush, false) || BoolCast(document.protoBrush, false) ? `1px` : "0px",
+ }}
+ onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}>
+ <strong className="presentationView-name">
+ {`${index + 1}. ${title}`}
+ </strong>
+ <button className="presentation-icon" onClick={e => { this.props.deleteDocument(index); e.stopPropagation(); }}>X</button>
+ </div>
);
}
render() {
- const children = Cast(this.props.Document.data, listSpec(Doc), []);
+ const children = DocListCast(this.props.Document.data);
return (
- <div>
- {children.map(value => this.renderChild(value))}
+ <div className="presentationView-listCont">
+ {children.map(this.renderChild)}
</div>
);
}
@@ -97,33 +85,35 @@ export class PresentationView extends React.Component<PresViewProps> {
closePresentation = action(() => this.props.Document.width = 0);
next = () => {
const current = NumCast(this.props.Document.selectedDoc);
- const allDocs = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
- if (allDocs && current < allDocs.length + 1) {
- //can move forwards
- this.props.Document.selectedDoc = current + 1;
- const doc = allDocs[current + 1];
- let docView = DocumentManager.Instance.getDocumentView(doc);
- if (docView) {
- docView.props.focus(docView.props.Document);
- }
- }
+ this.gotoDocument(current + 1);
}
back = () => {
const current = NumCast(this.props.Document.selectedDoc);
- const allDocs = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
- if (allDocs && current - 1 >= 0) {
- //can move forwards
- this.props.Document.selectedDoc = current - 1;
- const doc = allDocs[current - 1];
- let docView = DocumentManager.Instance.getDocumentView(doc);
- if (docView) {
- docView.props.focus(docView.props.Document);
- }
+ this.gotoDocument(current - 1);
+ }
+
+ @action
+ public RemoveDoc = (index: number) => {
+ const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
+ if (value) {
+ value.splice(index, 1);
}
}
- private ref = React.createRef<HTMLDivElement>();
+ public gotoDocument = async (index: number) => {
+ const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
+ if (!list) {
+ return;
+ }
+ if (index < 0 || index >= list.length) {
+ return;
+ }
+
+ this.props.Document.selectedDoc = index;
+ const doc = await list[index];
+ DocumentManager.Instance.jumpToDocument(doc);
+ }
//initilize class variables
constructor(props: PresViewProps) {
@@ -148,7 +138,7 @@ export class PresentationView extends React.Component<PresViewProps> {
}
render() {
- let titleStr = this.props.Document.Title;
+ let titleStr = StrCast(this.props.Document.title);
let width = NumCast(this.props.Document.width);
//TODO: next and back should be icons
@@ -156,17 +146,13 @@ export class PresentationView extends React.Component<PresViewProps> {
<div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}>
<div className="presentationView-heading">
<div className="presentationView-title">{titleStr}</div>
- <div className='presentation-icon' onClick={this.closePresentation}>X</div></div>
- <div>
- <div className="presentation-back" onClick={this.back}>back</div>
- <div className="presentation-next" onClick={this.next}>next</div>
-
+ <button className='presentation-icon' onClick={this.closePresentation}>X</button>
+ </div>
+ <div className="presentation-buttons">
+ <button className="presentation-button" onClick={this.back}>back</button>
+ <button className="presentation-button" onClick={this.next}>next</button>
</div>
- <ul>
- <PresentationViewItem
- Document={this.props.Document}
- />
- </ul>
+ <PresentationViewList Document={this.props.Document} deleteDocument={this.RemoveDoc} gotoDocument={this.gotoDocument} />
</div>
);
}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 4359ba093..7c1d00eb0 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -17,7 +17,16 @@ export class PreviewCursor extends React.Component<{}> {
constructor(props: any) {
super(props);
- document.addEventListener("keydown", this.onKeyPress)
+ document.addEventListener("keydown", this.onKeyPress);
+ document.addEventListener("paste", this.paste);
+ }
+ paste = (e: ClipboardEvent) => {
+ console.log(e.clipboardData);
+ if (e.clipboardData) {
+ console.log(e.clipboardData.getData("text/html"));
+ console.log(e.clipboardData.getData("text/csv"));
+ console.log(e.clipboardData.getData("text/plain"));
+ }
}
@action
@@ -27,8 +36,8 @@ export class PreviewCursor extends React.Component<{}> {
// the keyPress here.
//if not these keys, make a textbox if preview cursor is active!
if (e.key.startsWith("F") && !e.key.endsWith("F")) {
- } else if (e.key != "Escape" && e.key != "Alt" && e.key != "Shift" && e.key != "Meta" && e.key != "Control" && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
- if ((!e.ctrlKey && !e.metaKey) || e.key === "v") {
+ } else if (e.key !== "Escape" && e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
+ if ((!e.ctrlKey && !e.metaKey) || (e.key >= "a" && e.key <= "z")) {
PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e);
PreviewCursor.Visible = false;
}
diff --git a/src/client/views/SearchBox.scss b/src/client/views/SearchBox.scss
new file mode 100644
index 000000000..b38e6091d
--- /dev/null
+++ b/src/client/views/SearchBox.scss
@@ -0,0 +1,102 @@
+@import "globalCssVariables";
+
+.searchBox-bar {
+ height: 32px;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding-left: 2px;
+ padding-right: 2px;
+
+ .searchBox-input {
+ width: 130px;
+ -webkit-transition: width 0.4s;
+ transition: width 0.4s;
+ align-self: stretch;
+ }
+
+ .searchBox-input:focus {
+ width: 500px;
+ outline: 3px solid lightblue;
+ }
+
+ .searchBox-barChild {
+ flex: 0 1 auto;
+ margin-left: 2px;
+ margin-right: 2px;
+ }
+
+ .searchBox-filter {
+ align-self: stretch;
+ }
+
+ .searchBox-submit {
+ color: $dark-color;
+ }
+
+ .searchBox-submit:hover {
+ color: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+ }
+}
+
+.searchBox-results {
+ margin-left: 27px; //Is there a better way to do this?
+}
+
+.filter-form {
+ background: $dark-color;
+ height: 400px;
+ width: 400px;
+ position: relative;
+ right: 1px;
+ color: $light-color;
+ padding: 10px;
+ flex-direction: column;
+}
+
+#header {
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 100%;
+ height: 40px;
+}
+
+#option {
+ height: 20px;
+}
+
+.searchBox-results {
+ top: 300px;
+ display: flex;
+ flex-direction: column;
+
+ .search-item {
+ width: 500px;
+ height: 50px;
+ background: $light-color-secondary;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: all 0.1s;
+ border-width: 0.11px;
+ border-style: none;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ white-space: nowrap;
+ font-size: 13px;
+ }
+
+ .search-item:hover {
+ transition: all 0.1s;
+ background: $lighter-alt-accent;
+ }
+
+ .search-title {
+ text-transform: uppercase;
+ text-align: left;
+ width: 8vw;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
new file mode 100644
index 000000000..63d2065e2
--- /dev/null
+++ b/src/client/views/SearchBox.tsx
@@ -0,0 +1,207 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction } from 'mobx';
+import { Utils } from '../../Utils';
+import { MessageStore } from '../../server/Message';
+import "./SearchBox.scss";
+import { faSearch, faObjectGroup } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+// const app = express();
+// import * as express from 'express';
+import { Search } from '../../server/Search';
+import * as rp from 'request-promise';
+import { SearchItem } from './SearchItem';
+import { isString } from 'util';
+import { constant } from 'async';
+import { DocServer } from '../DocServer';
+import { Doc } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/FieldSymbols';
+import { DocumentManager } from '../util/DocumentManager';
+import { SetupDrag } from '../util/DragManager';
+import { Docs } from '../documents/Documents';
+import { RouteStore } from '../../server/RouteStore';
+import { NumCast } from '../../new_fields/Types';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+
+@observer
+export class SearchBox extends React.Component {
+ @observable
+ searchString: string = "";
+
+ @observable private _open: boolean = false;
+ @observable private _resultsOpen: boolean = false;
+
+ @observable
+ private _results: Doc[] = [];
+
+ @action.bound
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ this.searchString = e.target.value;
+ }
+
+ @action
+ submitSearch = async () => {
+ let query = this.searchString;
+ //gets json result into a list of documents that can be used
+ const results = await this.getResults(query);
+
+ runInAction(() => {
+ this._resultsOpen = true;
+ this._results = results;
+ });
+ }
+
+ @action
+ getResults = async (query: string) => {
+ let response = await rp.get(DocServer.prepend('/search'), {
+ qs: {
+ query
+ }
+ });
+ let res: string[] = JSON.parse(response);
+ const fields = await DocServer.GetRefFields(res);
+ const docs: Doc[] = [];
+ for (const id of res) {
+ const field = fields[id];
+ if (field instanceof Doc) {
+ docs.push(field);
+ }
+ }
+ return docs;
+ }
+ public static async convertDataUri(imageUri: string, returnedFilename: string) {
+ try {
+ let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ @action
+ handleClickFilter = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (className !== "filter-button" && className !== "filter-form") {
+ this._open = false;
+ }
+
+ }
+
+ @action
+ handleClickResults = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (id !== "result") {
+ this._resultsOpen = false;
+ this._results = [];
+ }
+
+ }
+
+ componentWillMount() {
+ document.addEventListener('mousedown', this.handleClickFilter, false);
+ document.addEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleClickFilter, false);
+ document.removeEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ @action
+ toggleFilterDisplay = () => {
+ this._open = !this._open;
+ }
+
+ enter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter") {
+ this.submitSearch();
+ }
+ }
+
+ collectionRef = React.createRef<HTMLSpanElement>();
+ startDragCollection = async () => {
+ const results = await this.getResults(this.searchString);
+ const docs = results.map(doc => {
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ });
+ let x = 0;
+ let y = 0;
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ const size = 200;
+ const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
+ if (aspect > 1) {
+ doc.height = size;
+ doc.width = size / aspect;
+ } else if (aspect > 0) {
+ doc.width = size;
+ doc.height = size * aspect;
+ } else {
+ doc.width = size;
+ doc.height = size;
+ }
+ doc.zoomBasis = 1;
+ x += 250;
+ if (x > 1000) {
+ x = 0;
+ y += 300;
+ }
+ }
+ return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
+ }
+
+ // Useful queries:
+ // Delegates of a document: {!join from=id to=proto_i}id:{protoId}
+ // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId}
+ render() {
+ return (
+ <div>
+ <div className="searchBox-container">
+ <div className="searchBox-bar">
+ <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
+ <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" />
+ </span>
+ <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..."
+ className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
+ style={{ width: this._resultsOpen ? "500px" : undefined }} />
+ {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */}
+ {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */}
+ </div>
+ {this._resultsOpen ? (
+ <div className="searchBox-results">
+ {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)}
+ </div>
+ ) : null}
+ </div>
+ {this._open ? (
+ <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}>
+ <div className="filter-form" id="header">Filter Search Results</div>
+ <div className="filter-form" id="option">
+ filter by collection, key, type of node
+ </div>
+
+ </div>
+ ) : null}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
new file mode 100644
index 000000000..01c7316d6
--- /dev/null
+++ b/src/client/views/SearchItem.tsx
@@ -0,0 +1,73 @@
+import React = require("react");
+import { Doc } from "../../new_fields/Doc";
+import { DocumentManager } from "../util/DocumentManager";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Cast } from "../../new_fields/Types";
+import { FieldView, FieldViewProps } from './nodes/FieldView';
+import { computed } from "mobx";
+import { IconField } from "../../new_fields/IconField";
+import { SetupDrag } from "../util/DragManager";
+
+
+export interface SearchProps {
+ doc: Doc;
+}
+
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+
+export class SearchItem extends React.Component<SearchProps> {
+
+ onClick = () => {
+ DocumentManager.Instance.jumpToDocument(this.props.doc);
+ }
+
+ //needs help
+ // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
+
+
+ public static DocumentIcon(layout: string) {
+ let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
+ layout.indexOf("ImageBox") !== -1 ? faImage :
+ layout.indexOf("Formatted") !== -1 ? faStickyNote :
+ layout.indexOf("Video") !== -1 ? faFilm :
+ layout.indexOf("Collection") !== -1 ? faObjectGroup :
+ faCaretUp;
+ return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
+ }
+ onPointerEnter = (e: React.PointerEvent) => {
+ this.props.doc.libraryBrush = true;
+ Doc.SetOnPrototype(this.props.doc, "protoBrush", true);
+ }
+ onPointerLeave = (e: React.PointerEvent) => {
+ this.props.doc.libraryBrush = false;
+ Doc.SetOnPrototype(this.props.doc, "protoBrush", false);
+ }
+
+ collectionRef = React.createRef<HTMLDivElement>();
+ startDocDrag = () => {
+ let doc = this.props.doc;
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ }
+ render() {
+ return (
+ <div className="search-item" ref={this.collectionRef} id="result"
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+ onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} >
+ <div className="search-title" id="result" >title: {this.props.doc.title}</div>
+ {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */}
+ {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index f29d9c8a1..e5b679e24 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -4,6 +4,9 @@ import { observer } from "mobx-react";
import './DocumentDecorations.scss';
import { Template } from "./Templates";
import { DocumentView } from "./nodes/DocumentView";
+import { List } from "../../new_fields/List";
+import { Doc } from "../../new_fields/Doc";
+import { NumCast } from "../../new_fields/Types";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -13,7 +16,7 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool
render() {
if (this.props.template) {
return (
- <li>
+ <li className="templateToggle">
<input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event, this.props.template)} />
{this.props.template.Name}
</li>
@@ -25,26 +28,38 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool
}
export interface TemplateMenuProps {
- doc: DocumentView;
+ docs: DocumentView[];
templates: Map<Template, boolean>;
}
@observer
export class TemplateMenu extends React.Component<TemplateMenuProps> {
-
@observable private _hidden: boolean = true;
+ constructor(props: TemplateMenuProps) {
+ super(props);
+ }
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
- this.props.doc.addTemplate(template);
+ if (template.Name === "Bullet") {
+ let topDocView = this.props.docs[0];
+ topDocView.addTemplate(template);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
+ } else {
+ this.props.docs.map(d => d.addTemplate(template));
+ }
this.props.templates.set(template, true);
- this.props.templates.forEach((checked, template) => console.log("Set Checked + " + checked + " " + this.props.templates.get(template)));
} else {
- this.props.doc.removeTemplate(template);
+ if (template.Name === "Bullet") {
+ let topDocView = this.props.docs[0];
+ topDocView.removeTemplate(template);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ this.props.docs.map(d => d.removeTemplate(template));
+ }
this.props.templates.set(template, false);
- this.props.templates.forEach((checked, template) => console.log("Unset Checked + " + checked + " " + this.props.templates.get(template)));
}
}
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index 5858ee014..0cd367bcb 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -5,6 +5,7 @@ export enum TemplatePosition {
InnerBottom,
InnerRight,
InnerLeft,
+ TopRight,
OutterTop,
OutterBottom,
OutterRight,
@@ -38,39 +39,50 @@ export class Template {
export namespace Templates {
// export const BasicLayout = new Template("Basic layout", "{layout}");
- export const OuterCaption = new Template("Outer caption", TemplatePosition.OutterBottom,
- `<div><div style="margin:auto; height:calc(100%); width:100%;">{layout}</div><div style="height:(100% + 50px); width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={"caption"} /></div></div>`
- );
+ export const Caption = new Template("Caption", TemplatePosition.OutterBottom,
+ `<div>
+ <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div style="bottom: 0; font-size:14px; width:100%; position:absolute">
+ <FormattedTextBox {...props} fieldKey={"caption"} hideOnLeave={"true"} />
+ </div>
+ </div>` );
- export const InnerCaption = new Template("Inner caption", TemplatePosition.InnerBottom,
- `<div><div style="margin:auto; height:calc(100% - 50px); width:100%;">{layout}</div><div style="height:50px; width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={"caption"}/></div></div>`
- );
-
- export const SideCaption = new Template("Side caption", TemplatePosition.OutterRight,
- `<div><div style="margin:auto; height:100%; width:100%;">{layout}</div><div style="height:100%; width:300px; position:absolute; top: 0; right: -300px;"><FormattedTextBox {...props} fieldKey={"caption"}/></div> </div>`
- );
+ export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop,
+ `<div>
+ <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div>
+ </div>` );
export const Title = new Template("Title", TemplatePosition.InnerTop,
- `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div><div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{props.Document.title}</div></div>`
- );
- export const Summary = new Template("Title", TemplatePosition.InnerTop,
- `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div><div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{props.Document.doc1.title}</div></div>`
+ `<div>
+ <div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
+ <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div>
+ </div>` );
+
+ export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
+ `<div>
+ <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
+ <img id="isExpander" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg=="
+ width="15px" height="15px"/>
+ </div>
+ </div>`
);
- // export const Summary = new Template("Title", TemplatePosition.InnerTop,
- // `<div style="height:100%; width:100%;position:absolute; margin:0; padding: 0">
- // <div style="height:60%; width:100%;position:absolute;background:yellow; padding:0; margin:0">
- // {layout}
- // </div>
- // <div style="bottom:0px; height:40%; width:50%; position:absolute; background-color: rgba(0, 0, 0, .4); color: white;">
- // <FieldView {...props} fieldKey={"doc1"} />
- // </div>
- // <div style="bottom:0; left: 50%; height:40%; width:50%; position:absolute; background-color: rgba(0, 0, 0, .4); color: white;">
- // <FieldView {...props} fieldKey={"doc2"} />
- // </div>
- // </div>`
- // );
-
- export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption];
+
+ export function ImageOverlay(width: number, height: number, field: string = "thumbnail") {
+ return (`<div>
+ <div style="height:100%; width:100%; position:absolute;">{layout}</div>
+ <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
+ <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} />
+ </div>
+ </div>`);
+ }
+
+ export const TemplateList: Template[] = [Title, TitleOverlay, Caption, Bullet];
export function sortTemplates(a: Template, b: Template) {
if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 76adfcdcd..734669893 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,14 +1,14 @@
-import { action, computed } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ContextMenu } from '../ContextMenu';
import { FieldViewProps } from '../nodes/FieldView';
import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types';
-import { Doc, FieldResult, Opt } from '../../../new_fields/Doc';
+import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc';
import { listSpec } from '../../../new_fields/Schema';
import { List } from '../../../new_fields/List';
-import { Id } from '../../../new_fields/RefField';
import { SelectionManager } from '../../util/SelectionManager';
+import { Id } from '../../../new_fields/FieldSymbols';
export enum CollectionViewType {
Invalid,
@@ -16,6 +16,7 @@ export enum CollectionViewType {
Schema,
Docking,
Tree,
+ Stacking
}
export interface CollectionRenderProps {
@@ -36,9 +37,20 @@ export interface CollectionViewProps extends FieldViewProps {
@observer
export class CollectionBaseView extends React.Component<CollectionViewProps> {
+ @observable private static _safeMode = false;
+ static InSafeMode() { return this._safeMode; }
+ static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
get collectionViewType(): CollectionViewType | undefined {
let Document = this.props.Document;
let viewField = Cast(Document.viewType, "number");
+ if (CollectionBaseView._safeMode) {
+ if (viewField === CollectionViewType.Freeform) {
+ return CollectionViewType.Tree;
+ }
+ if (viewField === CollectionViewType.Invalid) {
+ return CollectionViewType.Freeform;
+ }
+ }
if (viewField !== undefined) {
return viewField;
} else {
@@ -63,13 +75,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
if (!(documentToAdd instanceof Doc)) {
return false;
}
- let data = Cast(documentToAdd.data, listSpec(Doc), []);
- for (const doc of data.filter(d => d instanceof Document)) {
+ let data = DocListCast(documentToAdd.data);
+ for (const doc of data) {
if (this.createsCycle(doc, containerDocument)) {
return true;
}
}
- let annots = Cast(documentToAdd.annotations, listSpec(Doc), []);
+ let annots = DocListCast(documentToAdd.annotations);
for (const annot of annots) {
if (this.createsCycle(annot, containerDocument)) {
return true;
@@ -82,12 +94,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
}
return false;
}
- @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
+ @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; }
@action.bound
addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
let props = this.props;
- var curPage = Cast(props.Document.curPage, "number", -1);
+ var curPage = NumCast(props.Document.curPage, -1);
Doc.SetOnPrototype(doc, "page", curPage);
if (curPage >= 0) {
Doc.SetOnPrototype(doc, "annotationOn", props.Document);
@@ -95,17 +107,20 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
if (!this.createsCycle(doc, props.Document)) {
//TODO This won't create the field if it doesn't already exist
const value = Cast(props.Document[props.fieldKey], listSpec(Doc));
+ let alreadyAdded = true;
if (value !== undefined) {
- if (allowDuplicates || !value.some(v => v[Id] === doc[Id])) {
+ if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
+ alreadyAdded = false;
value.push(doc);
}
} else {
- this.props.Document[this.props.fieldKey] = new List([doc]);
+ alreadyAdded = false;
+ Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc]));
}
// set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument?
- if (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid) {
+ if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
let zoom = NumCast(this.props.Document.scale, 1);
- doc.zoomBasis = zoom;
+ // Doc.GetProto(doc).zoomBasis = zoom;
}
}
return true;
@@ -113,12 +128,14 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
removeDocument(doc: Doc): boolean {
+ SelectionManager.DeselectAll();
const props = this.props;
//TODO This won't create the field if it doesn't already exist
const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
let index = -1;
for (let i = 0; i < value.length; i++) {
- if (value[i][Id] === doc[Id]) {
+ let v = value[i];
+ if (v instanceof Doc && v[Id] === doc[Id]) {
index = i;
break;
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index cfb1aef7d..dcc1bd95d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,25 +1,28 @@
-import * as GoldenLayout from "golden-layout";
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, observable, reaction, trace, runInAction } from "mobx";
+import { action, observable, reaction, Lambda } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
import Measure from "react-measure";
-import { Utils, returnTrue, emptyFunction, returnOne, returnZero } from "../../../Utils";
+import * as GoldenLayout from "../../../client/goldenLayout";
+import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
+import { FieldId } from "../../../new_fields/RefField";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocumentView } from "../nodes/DocumentView";
import "./CollectionDockingView.scss";
-import React = require("react");
import { SubCollectionViewProps } from "./CollectionSubView";
-import { DragManager, DragLinksAsDocuments } from "../../util/DragManager";
-import { Transform } from '../../util/Transform';
-import { Doc, Opt, Field } from "../../../new_fields/Doc";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { DocServer } from "../../DocServer";
-import { listSpec } from "../../../new_fields/Schema";
-import { Id, FieldId } from "../../../new_fields/RefField";
-import { faSignInAlt } from "@fortawesome/free-solid-svg-icons";
+import React = require("react");
+import { ParentDocSelector } from './ParentDocumentSelector';
+import { DocumentManager } from '../../util/DocumentManager';
+import { CollectionViewType } from './CollectionBaseView';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@@ -72,11 +75,52 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this.stateChanged();
}
+ @undoBatch
+ @action
+ public CloseRightSplit = (document: Doc): boolean => {
+ let retVal = false;
+ if (this._goldenLayout.root.contentItems[0].isRow) {
+ retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+ if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+ Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
+ child.contentItems[0].remove();
+ this.layoutChanged(document);
+ return true;
+ } else {
+ Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
+ if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
+ child.contentItems[j].remove();
+ child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
+ return true;
+ }
+ return false;
+ });
+ }
+ return false;
+ });
+ }
+ if (retVal) {
+ this.stateChanged();
+ }
+ return retVal;
+ }
+
+ @action
+ layoutChanged(removed?: Doc) {
+ this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
+ this._goldenLayout.emit('stateChanged');
+ this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
+ if (removed) CollectionDockingView.Instance._removedDocs.push(removed);
+ this.stateChanged();
+ }
+
//
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
@action
- public AddRightSplit(document: Doc, minimize: boolean = false) {
+ public AddRightSplit = (document: Doc, minimize: boolean = false) => {
let docs = Cast(this.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
@@ -103,17 +147,26 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
newContentItem.config.width = 50;
}
if (minimize) {
- newContentItem.config.width = 10;
- newContentItem.config.height = 10;
+ // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes
+ // newContentItem.config.width = 10;
+ // newContentItem.config.height = 10;
}
newContentItem.callDownwards('_$init');
- this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
- this._goldenLayout.emit('stateChanged');
- this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
- this.stateChanged();
+ this.layoutChanged();
return newContentItem;
}
+ @action
+ public AddTab = (stack: any, document: Doc) => {
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ if (docs) {
+ docs.push(document);
+ }
+ let docContentConfig = CollectionDockingView.makeDocumentConfig(document);
+ var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout);
+ stack.addChild(newContentItem.contentItems[0], undefined);
+ this.layoutChanged();
+ }
setupGoldenLayout() {
var config = StrCast(this.props.Document.dockingConfig);
@@ -128,6 +181,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
try {
this._goldenLayout.unbind('itemDropped', this.itemDropped);
this._goldenLayout.unbind('tabCreated', this.tabCreated);
+ this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
this._goldenLayout.unbind('stackCreated', this.stackCreated);
} catch (e) { }
this._goldenLayout.destroy();
@@ -135,6 +189,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
this._goldenLayout.on('itemDropped', this.itemDropped);
this._goldenLayout.on('tabCreated', this.tabCreated);
+ this._goldenLayout.on('tabDestroyed', this.tabDestroyed);
this._goldenLayout.on('stackCreated', this.stackCreated);
this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer);
this._goldenLayout.container = this._containerRef.current;
@@ -148,12 +203,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this._goldenLayout.init();
}
}
+ reactionDisposer?: Lambda;
componentDidMount: () => void = () => {
if (this._containerRef.current) {
- reaction(
+ this.reactionDisposer = reaction(
() => StrCast(this.props.Document.dockingConfig),
() => {
if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) {
+ // Because this is in a set timeout, if this component unmounts right after mounting,
+ // we will leak a GoldenLayout, because we try to destroy it before we ever create it
setTimeout(() => this.setupGoldenLayout(), 1);
}
this._ignoreStateChange = "";
@@ -167,12 +225,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this._goldenLayout.unbind('itemDropped', this.itemDropped);
this._goldenLayout.unbind('tabCreated', this.tabCreated);
this._goldenLayout.unbind('stackCreated', this.stackCreated);
+ this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
} catch (e) {
}
if (this._goldenLayout) this._goldenLayout.destroy();
this._goldenLayout = null;
window.removeEventListener('resize', this.onResize);
+
+ if (this.reactionDisposer) {
+ this.reactionDisposer();
+ }
}
@action
onResize = (event: any) => {
@@ -209,6 +272,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
let y = e.clientY;
let docid = (e.target as any).DashDocId;
let tab = (e.target as any).parentElement as HTMLElement;
+ let glTab = (e.target as any).Tab;
+ if (glTab && glTab.contentItem && glTab.contentItem.parent) {
+ glTab.contentItem.parent.setActiveContentItem(glTab.contentItem);
+ }
DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
if (f instanceof Doc) {
DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
@@ -231,6 +298,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@undoBatch
stateChanged = () => {
+ let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc));
+ CollectionDockingView.Instance._removedDocs.map(theDoc =>
+ docs && docs.indexOf(theDoc) !== -1 &&
+ docs.splice(docs.indexOf(theDoc), 1));
+ CollectionDockingView.Instance._removedDocs.length = 0;
var json = JSON.stringify(this._goldenLayout.toConfig());
this.props.Document.dockingConfig = json;
if (this.undohack && !this.hack) {
@@ -251,44 +323,54 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
return template.content.firstChild;
}
- tabCreated = (tab: any) => {
+ tabCreated = async (tab: any) => {
if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") {
- DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async f => {
- if (f instanceof Doc) {
- const title = Cast(f.title, "string");
- if (title !== undefined) {
- tab.titleElement[0].textContent = title;
- }
- const lf = await Cast(f.linkedFromDocs, listSpec(Doc));
- const lt = await Cast(f.linkedToDocs, listSpec(Doc));
- let count = (lf ? lf.length : 0) + (lt ? lt.length : 0);
- let counter: any = this.htmlToElement(`<div class="messageCounter">${count}</div>`);
+ if (tab.contentItem.config.fixed) {
+ tab.contentItem.parent.config.fixed = true;
+ }
+ DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => {
+ if (doc instanceof Doc) {
+ let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`);
tab.element.append(counter);
+ let upDiv = document.createElement("span");
+ const stack = tab.contentItem.parent;
+ ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv);
+ tab.reactComponents = [upDiv];
+ tab.element.append(upDiv);
counter.DashDocId = tab.contentItem.config.props.documentId;
- tab.reactionDisposer = reaction((): [List<Field> | null | undefined, List<Field> | null | undefined] => [lf, lt],
- ([linkedFrom, linkedTo]) => {
- let count = (linkedFrom ? linkedFrom.length : 0) + (linkedTo ? linkedTo.length : 0);
- counter.innerHTML = count;
- });
+ tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
+ () => {
+ counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length;
+ tab.titleElement[0].textContent = doc.title;
+ }, { fireImmediately: true });
tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
}
});
}
+ tab.titleElement[0].Tab = tab;
tab.closeElement.off('click') //unbind the current click handler
- .click(function () {
+ .click(async function () {
if (tab.reactionDisposer) {
tab.reactionDisposer();
}
- DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async f => runInAction(() => {
- if (f instanceof Doc) {
- let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc));
- docs && docs.indexOf(f) !== -1 && docs.splice(docs.indexOf(f), 1);
- }
- }));
+ let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
+ if (doc instanceof Doc) {
+ let theDoc = doc;
+ CollectionDockingView.Instance._removedDocs.push(theDoc);
+ }
tab.contentItem.remove();
});
}
+ tabDestroyed = (tab: any) => {
+ if (tab.reactComponents) {
+ for (const ele of tab.reactComponents) {
+ ReactDOM.unmountComponentAtNode(ele);
+ }
+ }
+ }
+ _removedDocs: Doc[] = [];
+
stackCreated = (stack: any) => {
//stack.header.controlsContainer.find('.lm_popout').hide();
stack.header.controlsContainer.find('.lm_close') //get the close icon
@@ -296,13 +378,21 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
.click(action(function () {
//if (confirm('really close this?')) {
stack.remove();
+ stack.contentItems.map(async (contentItem: any) => {
+ let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
+ if (doc instanceof Doc) {
+ let theDoc = doc;
+ CollectionDockingView.Instance._removedDocs.push(theDoc);
+ }
+ });
//}
}));
stack.header.controlsContainer.find('.lm_popout') //get the close icon
.off('click') //unbind the current click handler
.click(action(function () {
- var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
- let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
+ stack.config.fixed = !stack.config.fixed;
+ // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+ // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
}));
}
@@ -312,6 +402,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
);
}
+
}
interface DockedFrameProps {
@@ -324,7 +415,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
@observable private _document: Opt<Doc>;
-
+ get _stack(): any {
+ let parent = (this.props as any).glContainer.parent.parent;
+ if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1)
+ return parent.parent.contentItems[1];
+ return parent;
+ }
constructor(props: any) {
super(props);
DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
@@ -343,20 +439,38 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
if (this._mainCont.current && this._mainCont.current.children) {
let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement);
scale = Utils.GetScreenTransform(this._mainCont.current).scale;
- return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this.contentScaling());
+ return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
}
return Transform.Identity();
}
+ get scaleToFitMultiplier() {
+ let docWidth = NumCast(this._document!.width);
+ let docHeight = NumCast(this._document!.height);
+ if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1;
+ if (StrCast(this._document!.layout).indexOf("Collection") === -1 ||
+ NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1;
+ let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ?
+ this._panelHeight / docHeight : this._panelWidth / docWidth);
+ return scaling;
+ }
get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
+ addDocTab = (doc: Doc, location: string) => {
+ if (location === "onRight") {
+ CollectionDockingView.Instance.AddRightSplit(doc);
+ } else {
+ CollectionDockingView.Instance.AddTab(this._stack, doc);
+ }
+ }
get content() {
- if (!this._document)
+ if (!this._document) {
return (null);
+ }
return (
<div className="collectionDockingView-content" ref={this._mainCont}
- style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
- <DocumentView key={this._document![Id]} Document={this._document!}
- toggleMinimized={emptyFunction}
+ style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}>
+ <DocumentView key={this._document[Id]} Document={this._document}
+ bringToFront={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
ContentScaling={this.contentScaling}
@@ -368,6 +482,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
focus={emptyFunction}
+ addDocTab={this.addDocTab}
ContainingCollectionView={undefined} />
</div >);
}
@@ -375,7 +490,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
render() {
let theContent = this.content;
return !this._document ? (null) :
- <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}>
+ <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
{({ measureRef }) => <div ref={measureRef}> {theContent} </div>}
</Measure>;
}
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
index f6fb79582..50201bae8 100644
--- a/src/client/views/collections/CollectionPDFView.scss
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -1,49 +1,56 @@
.collectionPdfView-buttonTray {
- top : 15px;
- left : 20px;
- position: relative;
+ top: 15px;
+ left: 20px;
+ position: relative;
transform-origin: left top;
position: absolute;
}
+
.collectionPdfView-thumb {
- width:25px;
- height:25px;
+ width: 25px;
+ height: 25px;
transform-origin: left top;
position: absolute;
background: darkgray;
}
+
.collectionPdfView-slider {
- width:25px;
- height:25px;
+ width: 25px;
+ height: 25px;
transform-origin: left top;
position: absolute;
background: lightgray;
}
-.collectionPdfView-cont{
+
+.collectionPdfView-cont {
width: 100%;
height: 100%;
- position: absolute;
+ position: absolute;
top: 0;
- left:0;
+ left: 0;
+ z-index: -1;
}
+
.collectionPdfView-cont-dragging {
span {
user-select: none;
}
}
+
.collectionPdfView-backward {
- color : white;
+ color: white;
font-size: 24px;
- top :0px;
- left : 0px;
+ top: 0px;
+ left: 0px;
position: absolute;
background-color: rgba(50, 50, 50, 0.2);
}
+
.collectionPdfView-forward {
- color : white;
+ color: white;
font-size: 24px;
- top :0px;
- left : 45px;
+ top: 0px;
+ left: 45px;
position: absolute;
background-color: rgba(50, 50, 50, 0.2);
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index b3762206a..5e51437a4 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -8,7 +8,7 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView";
import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView";
import { emptyFunction } from "../../../Utils";
import { NumCast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/RefField";
+import { Id } from "../../../new_fields/FieldSymbols";
@observer
@@ -61,7 +61,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
onContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction });
+ ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" });
}
}
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index cfdb3ab22..186e006f3 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -4,52 +4,63 @@
.collectionSchemaView-container {
border-width: $COLLECTION_BORDER_WIDTH;
- border-color : $intermediate-color;
+ border-color: $intermediate-color;
border-style: solid;
border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
width: 100%;
height: 100%;
+ overflow: hidden;
.collectionSchemaView-cellContents {
height: $MAX_ROW_HEIGHT;
+
+ img {
+ width: auto;
+ max-height: $MAX_ROW_HEIGHT;
+ }
}
-
+
.collectionSchemaView-previewRegion {
position: relative;
background: $light-color;
float: left;
height: 100%;
+
.collectionSchemaView-previewDoc {
height: 100%;
- width: 100%;
+ width: 100%;
position: absolute;
}
+
.collectionSchemaView-input {
position: absolute;
max-width: 150px;
width: 100%;
bottom: 0px;
}
+
.documentView-node:first-child {
position: relative;
background: $light-color;
}
}
+
.collectionSchemaView-previewHandle {
position: absolute;
height: 15px;
- width: 15px;
- z-index: 20;
- right: 0;
- top: 20px;
- background: Black ;
+ width: 15px;
+ z-index: 20;
+ right: 0;
+ top: 20px;
+ background: Black;
}
- .collectionSchemaView-dividerDragger{
- position: relative;
- background: black;
- float: left;
+
+ .collectionSchemaView-dividerDragger {
+ position: relative;
+ background: black;
+ float: left;
height: 37px;
width: 20px;
z-index: 20;
@@ -57,6 +68,7 @@
top: 0;
background: $main-accent;
}
+
.collectionSchemaView-columnsHandle {
position: absolute;
height: 37px;
@@ -66,6 +78,7 @@
bottom: 0;
background: $main-accent;
}
+
.collectionSchemaView-colDividerDragger {
position: relative;
box-sizing: border-box;
@@ -74,6 +87,7 @@
float: top;
width: 100%;
}
+
.collectionSchemaView-dividerDragger {
position: relative;
box-sizing: border-box;
@@ -82,11 +96,13 @@
float: left;
height: 100%;
}
+
.collectionSchemaView-tableContainer {
position: relative;
float: left;
height: 100%;
}
+
.ReactTable {
// position: absolute; // display: inline-block;
// overflow: auto;
@@ -95,6 +111,7 @@
background: $light-color;
box-sizing: border-box;
border: none !important;
+
.rt-table {
overflow-y: auto;
overflow-x: auto;
@@ -103,42 +120,50 @@
direction: ltr; // direction:rtl;
// display:block;
}
+
.rt-tbody {
//direction: ltr;
direction: rtl;
}
+
.rt-tr-group {
direction: ltr;
max-height: $MAX_ROW_HEIGHT;
}
+
.rt-td {
border-width: 1px;
border-right-color: $intermediate-color;
+
.imageBox-cont {
position: relative;
max-height: 100%;
}
+
.imageBox-cont img {
object-fit: contain;
max-width: 100%;
height: 100%;
}
- .videobox-cont {
+
+ .videoBox-cont {
object-fit: contain;
width: auto;
height: 100%;
}
}
}
+
.ReactTable .rt-thead.-header {
background: $intermediate-color;
color: $light-color;
- text-transform: uppercase;
+ // text-transform: uppercase;
letter-spacing: 2px;
font-size: 12px;
height: 30px;
padding-top: 4px;
}
+
.ReactTable .rt-th,
.ReactTable .rt-td {
max-height: $MAX_ROW_HEIGHT;
@@ -146,32 +171,36 @@
font-size: 13px;
text-align: center;
}
+
.ReactTable .rt-tbody .rt-tr-group:last-child {
border-bottom: $intermediate-color;
border-bottom-style: solid;
border-bottom-width: 1;
}
+
.documentView-node-topmost {
- text-align:left;
+ text-align: left;
transform-origin: center top;
display: inline-block;
}
+
.documentView-node:first-child {
background: $light-color;
}
}
+
//options menu styling
#schemaOptionsMenuBtn {
position: absolute;
height: 20px;
width: 20px;
border-radius: 50%;
- z-index: 21;
+ z-index: 21;
right: 4px;
- top: 4px;
+ top: 4px;
pointer-events: auto;
- background-color:black;
- display:inline-block;
+ background-color: black;
+ display: inline-block;
padding: 0px;
font-size: 100%;
}
@@ -185,10 +214,12 @@ ul {
padding: 0px;
margin: 0px;
}
+
.schema-options-subHeader {
color: $intermediate-color;
margin-bottom: 5px;
}
+
#schemaOptionsMenuBtn:hover {
transform: scale(1.15);
}
@@ -198,15 +229,15 @@ ul {
font-size: 12px;
}
- #options-flyout-div {
+#options-flyout-div {
text-align: left;
- padding:0px;
+ padding: 0px;
z-index: 100;
font-family: $sans-serif;
padding-left: 5px;
- }
+}
- #schema-col-checklist {
+#schema-col-checklist {
overflow: scroll;
text-align: left;
//background-color: $light-color-secondary;
@@ -214,8 +245,8 @@ ul {
max-height: 175px;
font-family: $sans-serif;
font-size: 12px;
- }
-
+}
+
.Resizer {
box-sizing: border-box;
@@ -223,6 +254,7 @@ ul {
opacity: 0.5;
z-index: 1;
background-clip: padding-box;
+
&.horizontal {
height: 11px;
margin: -5px 0;
@@ -230,22 +262,26 @@ ul {
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
+
&:hover {
border-top: 5px solid rgba(0, 0, 0, 0.5);
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
}
}
+
&.vertical {
width: 11px;
margin: 0 -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize;
+
&:hover {
border-left: 5px solid rgba(0, 0, 0, 0.5);
border-right: 5px solid rgba(0, 0, 0, 0.5);
}
}
+
&:hover {
-webkit-transition: all 2s ease;
transition: all 2s ease;
@@ -266,10 +302,12 @@ ul {
-ms-flex-direction: column;
flex-direction: column;
}
+
header {
padding: 1rem;
background: #eee;
}
+
footer {
padding: 1rem;
background: #eee;
@@ -283,10 +321,12 @@ ul {
display: flex;
flex-direction: column;
}
+
header {
padding: 1rem;
background: #eee;
}
+
footer {
padding: 1rem;
background: #eee;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 67784fa81..11d71d023 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -2,7 +2,7 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked, runInAction } from "mobx";
+import { action, computed, observable, untracked, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
@@ -19,13 +19,23 @@ import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { Opt, Field, Doc } from "../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
import { listSpec } from "../../../new_fields/Schema";
import { List } from "../../../new_fields/List";
-import { Id } from "../../../new_fields/RefField";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Gateway } from "../../northstar/manager/Gateway";
+import { Docs } from "../../documents/Documents";
+import { ContextMenu } from "../ContextMenu";
+import { CollectionView } from "./CollectionView";
+import { CollectionPDFView } from "./CollectionPDFView";
+import { CollectionVideoView } from "./CollectionVideoView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
+library.add(faCog);
+library.add(faPlus);
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@@ -48,7 +58,7 @@ class KeyToggle extends React.Component<{ keyName: string, checked: boolean, tog
@observer
export class CollectionSchemaView extends CollectionSubView(doc => doc) {
private _mainCont?: HTMLDivElement;
- private _startSplitPercent = 0;
+ private _startPreviewWidth = 0;
private DIVIDER_WIDTH = 4;
@observable _columns: Array<string> = ["title", "data", "author"];
@@ -56,15 +66,41 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@observable _columnsPercentage = 0;
@observable _keys: string[] = [];
@observable _newKeyName: string = "";
+ @observable previewScript: string = "";
- @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); }
+ @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
+ @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; }
+ @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); }
@computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); }
@computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
+ @computed get tableColumns() {
+ return this.columns.map(col => {
+ const ref = React.createRef<HTMLParagraphElement>();
+ return {
+ Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col), undefined, "copy")}>{col}</p>,
+ accessor: (doc: Doc) => doc ? doc[col] : 0,
+ id: col
+ };
+ });
+ }
+
+ onHeaderDrag = (columnName: string) => {
+ let schemaDoc = Cast(this.props.Document.schemaDoc, Doc);
+ if (schemaDoc instanceof Doc) {
+ let columnDocs = DocListCast(schemaDoc.data);
+ if (columnDocs) {
+ let ddoc = columnDocs.find(doc => doc.title === columnName);
+ if (ddoc)
+ return ddoc;
+ }
+ }
+ return this.props.Document;
+ }
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
- Document: rowProps.value[0],
- fieldKey: rowProps.value[1],
+ Document: rowProps.original,
+ fieldKey: rowProps.column.id as string,
ContainingCollectionView: this.props.CollectionView,
isSelected: returnFalse,
select: emptyFunction,
@@ -76,52 +112,48 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
whenActiveChanged: emptyFunction,
PanelHeight: returnZero,
PanelWidth: returnZero,
+ addDocTab: this.props.addDocTab,
};
- let contents = (
- <FieldView {...props} />
- );
+ let fieldContentView = <FieldView {...props} />;
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = SetupDrag(reference, () => props.Document, this.props.moveDocument);
+ let onItemDown = (e: React.PointerEvent) =>
+ (this.props.CollectionView.props.isSelected() ?
+ SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e) : undefined);
let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => {
const res = run({ this: doc });
if (!res.success) return false;
- const field = res.result;
- doc[props.fieldKey] = field;
+ doc[props.fieldKey] = res.result;
return true;
};
return (
<div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}>
<EditableView
display={"inline"}
- contents={contents}
+ contents={fieldContentView}
height={Number(MAX_ROW_HEIGHT)}
GetValue={() => {
let field = props.Document[props.fieldKey];
- if (field) {
- //TODO Types
- // return field.ToScriptString();
- return String(field);
+ if (Field.IsField(field)) {
+ return Field.toScriptString(field);
}
return "";
}}
SetValue={(value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
+ let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } });
if (!script.compiled) {
return false;
}
return applyToDoc(props.Document, script.run);
}}
- OnFillDown={(value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
+ OnFillDown={async (value: string) => {
+ let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } });
if (!script.compiled) {
return;
}
const run = script.run;
//TODO This should be able to be refactored to compile the script once
- const val = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc));
- if (val) {
- val.forEach(doc => applyToDoc(doc, run));
- }
+ const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+ val && val.forEach(doc => applyToDoc(doc, run));
}}>
</EditableView>
</div >
@@ -171,30 +203,31 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
//toggles preview side-panel of schema
@action
- toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
+ toggleExpander = () => {
+ this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0;
}
+ onDividerDown = (e: React.PointerEvent) => {
+ this._startPreviewWidth = this.previewWidth();
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", this.onDividerMove);
+ document.addEventListener('pointerup', this.onDividerUp);
+ }
@action
onDividerMove = (e: PointerEvent): void => {
let nativeWidth = this._mainCont!.getBoundingClientRect();
- this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
+ this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40,
+ this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]);
}
@action
onDividerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
- if (this._startSplitPercent === this.splitPercentage) {
- this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
+ if (this._startPreviewWidth === this.previewWidth()) {
+ this.toggleExpander();
}
}
- onDividerDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this.splitPercentage;
- e.stopPropagation();
- e.preventDefault();
- document.addEventListener("pointermove", this.onDividerMove);
- document.addEventListener('pointerup', this.onDividerUp);
- }
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
@@ -209,6 +242,33 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
}
+ onContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB });
+ }
+ }
+
+ @action
+ makeDB = async () => {
+ let csv: string = this.columns.reduce((val, col) => val + col + ",", "");
+ csv = csv.substr(0, csv.length - 1) + "\n";
+ let self = this;
+ DocListCast(this.props.Document.data).map(doc => {
+ csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "0") + ",", "");
+ csv = csv.substr(0, csv.length - 1) + "\n";
+ });
+ csv.substring(0, csv.length - 1);
+ let dbName = StrCast(this.props.Document.title);
+ let res = await Gateway.Instance.PostSchema(csv, dbName);
+ if (self.props.CollectionView.props.addDocument) {
+ let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
+ if (schemaDoc) {
+ //self.props.CollectionView.props.addDocument(schemaDoc, false);
+ self.props.Document.schemaDoc = schemaDoc;
+ }
+ }
+ }
+
@action
addColumn = () => {
this.columns.push(this._newKeyName);
@@ -220,64 +280,19 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
this._newKeyName = e.currentTarget.value;
}
- @observable previewScript: string = "";
- @action
- onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.previewScript = e.currentTarget.value;
- }
-
+ @computed
get previewDocument(): Doc | undefined {
- const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ const children = DocListCast(this.props.Document[this.props.fieldKey]);
const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined;
- return selected ? (this.previewScript ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
- }
- get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); }
- get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; }
- get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; }
-
- private previewDocNativeWidth = () => Cast(this.previewDocument!.nativeWidth, "number", this.previewRegionWidth);
- private previewDocNativeHeight = () => Cast(this.previewDocument!.nativeHeight, "number", this.previewRegionHeight);
- private previewContentScaling = () => {
- let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth);
- if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) {
- return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight);
- }
- return wscale;
+ return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
}
- private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling();
- private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling();
- get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; }
+
getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
- - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset,
- - this.borderWidth).scale(1 / this.previewContentScaling())
+ - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth);
- @computed
- get previewPanel() {
- // let doc = CompileScript(this.previewScript, { this: selected }, true)();
- const previewDoc = this.previewDocument;
- return !previewDoc ? (null) : (
- <div className="collectionSchemaView-previewRegion" style={{ width: `${this.previewRegionWidth}px` }}>
- <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
- <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false}
- toggleMinimized={emptyFunction}
- addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
- ScreenToLocalTransform={this.getPreviewTransform}
- ContentScaling={this.previewContentScaling}
- PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight}
- ContainingCollectionView={this.props.CollectionView}
- focus={emptyFunction}
- parentActive={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- />
- </div>
- <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange}
- style={{ left: `calc(50% - ${Math.min(75, this.previewPanelWidth() / 2)}px)` }} />
- </div>
- );
- }
get documentKeysCheckList() {
- const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ const docs = DocListCast(this.props.Document[this.props.fieldKey]);
let keys: { [key: string]: boolean } = {};
// bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
// then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
@@ -300,7 +315,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
<div id="schema-options-header"><h5><b>Options</b></h5></div>
<div id="options-flyout-div">
<h6 className="schema-options-subHeader">Preview Window</h6>
- <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div>
+ <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> Show Preview </div>
<h6 className="schema-options-subHeader" >Displayed Columns</h6>
<ul id="schema-col-checklist" >
{this.documentKeysCheckList}
@@ -315,34 +330,130 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
@computed
+ get reactTable() {
+ trace();
+ let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1;
+ return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false}
+ columns={this.tableColumns}
+ column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
+ getTrProps={this.getTrProps}
+ />
+ }
+
+ @computed
get dividerDragger() {
- return this.splitPercentage === 0 ? (null) :
+ return this.previewWidth() === 0 ? (null) :
<div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
}
+ @computed
+ get previewPanel() {
+ trace();
+ return <CollectionSchemaPreview
+ Document={this.previewDocument}
+ width={this.previewWidth}
+ height={this.previewHeight}
+ getTransform={this.getPreviewTransform}
+ CollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={this.setPreviewScript}
+ previewScript={this.previewScript}
+ />
+ }
+ @action
+ setPreviewScript = (script: string) => {
+ this.previewScript = script;
+ }
+
render() {
- library.add(faCog);
- library.add(faPlus);
- //This can't just pass FieldValue to filter because filter passes other arguments to the passed in function, which end up as default values in FieldValue
- const children = (this.children || []).filter(doc => FieldValue(doc));
+ trace();
return (
<div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
- onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
- <div className="collectionSchemaView-tableContainer" style={{ width: `${this.tableWidth}px` }}>
- <ReactTable data={children} page={0} pageSize={children.length} showPagination={false}
- columns={this.columns.map(col => ({
- Header: col,
- accessor: (doc: Doc) => [doc, col],
- id: col
- }))}
- column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
- getTrProps={this.getTrProps}
- />
- </div>
+ onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}>
+ {this.reactTable}
{this.dividerDragger}
- {this.previewPanel}
+ {!this.previewWidth() ? (null) : this.previewPanel}
{this.tableOptionsPanel}
</div>
);
}
+}
+interface CollectionSchemaPreviewProps {
+ Document?: Doc;
+ width: () => number;
+ height: () => number;
+ CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ getTransform: () => Transform;
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ active: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+ addDocTab: (document: Doc, where: string) => void;
+ setPreviewScript: (script: string) => void;
+ previewScript?: string;
+}
+
+@observer
+export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{
+ private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); }
+ private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); }
+ private contentScaling = () => {
+ let wscale = this.props.width() / (this.nativeWidth ? this.nativeWidth : this.props.width());
+ if (wscale * this.nativeHeight > this.props.height()) {
+ return this.props.height() / (this.nativeHeight ? this.nativeHeight : this.props.height());
+ }
+ return wscale;
+ }
+ private PanelWidth = () => this.nativeWidth * this.contentScaling();
+ private PanelHeight = () => this.nativeHeight * this.contentScaling();
+ private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling())
+ get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; }
+ @action
+ onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.setPreviewScript(e.currentTarget.value);
+ }
+ @undoBatch
+ @action
+ public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.isMinimized = !isMinimized;
+ });
+ }
+ }
+ render() {
+ trace();
+ console.log(this.props.Document);
+ let input = this.props.previewScript === undefined ? (null) :
+ <input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
+ style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} />;
+ return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width() }}>
+ {!this.props.Document || !this.props.width ? (null) : (
+ <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)` }}>
+ <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false}
+ addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
+ ScreenToLocalTransform={this.getTransform}
+ ContentScaling={this.contentScaling}
+ PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight}
+ ContainingCollectionView={this.props.CollectionView}
+ focus={emptyFunction}
+ parentActive={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction}
+ addDocTab={this.props.addDocTab}
+ collapseToPoint={this.collapseToPoint}
+ />
+ </div>)}
+ {input}
+ </div>);
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
new file mode 100644
index 000000000..4d84aaaa9
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -0,0 +1,51 @@
+@import "../globalCssVariables";
+
+.collectionStackingView {
+ top: 0;
+ left: 0;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ position: absolute;
+ overflow-y: auto;
+ border-width: 0;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ border-color: $light-color-secondary;
+ border-style: solid;
+ border-radius: 0 0 $border-radius $border-radius;
+ box-sizing: border-box;
+
+ .collectionStackingView-docView-container {
+ width: 45%;
+ margin: 5% 2.5%;
+ padding-left: 2.5%;
+ height: auto;
+ }
+
+ .collectionStackingView-flexCont {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+
+ .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{
+ width:100%;
+ height:100%;
+ position: absolute;
+ }
+ .collectionStackingView-masonryGrid {
+ display:grid;
+ }
+
+ .collectionStackingView-description {
+ font-size: 100%;
+ margin-bottom: 1vw;
+ padding: 10px;
+ height: 2vw;
+ width: 100%;
+ font-family: $sans-serif;
+ background: $dark-color;
+ color: $light-color;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
new file mode 100644
index 000000000..da7ea50c6
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -0,0 +1,179 @@
+import React = require("react");
+import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { BoolCast, NumCast } from "../../../new_fields/Types";
+import { emptyFunction, returnOne, Utils } from "../../../Utils";
+import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
+import { DocumentView } from "../nodes/DocumentView";
+import { CollectionSchemaPreview } from "./CollectionSchemaView";
+import "./CollectionStackingView.scss";
+import { CollectionSubView } from "./CollectionSubView";
+
+@observer
+export class CollectionStackingView extends CollectionSubView(doc => doc) {
+ _masonryGridRef: HTMLDivElement | null = null;
+ _heightDisposer?: IReactionDisposer;
+ get gridGap() { return 10; }
+ get gridSize() { return 20; }
+ get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); }
+ get columnWidth() { return this.singleColumn ? this.props.PanelWidth() - 4 * this.gridGap : NumCast(this.props.Document.columnWidth, 250); }
+
+ componentDidMount() {
+ this._heightDisposer = reaction(() => [this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized]), this.columnWidth, this.props.PanelHeight()],
+ () => {
+ if (this.singleColumn) {
+ this.props.Document.height = this.childDocs.filter(d => !d.isMinimized).reduce((height, d) => {
+ let hgt = d[HeightSym]();
+ let wid = d[WidthSym]();
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ if (nw && nh) hgt = nh / nw * Math.min(this.columnWidth, wid);
+ return height + hgt + 2 * this.gridGap;
+ }, this.gridGap * 2);
+ }
+ }, { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ if (this._heightDisposer) this._heightDisposer();
+ }
+
+ @action
+ moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => {
+ this.props.removeDocument(doc);
+ addDocument(doc);
+ return true;
+ }
+ getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
+ let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth);
+ }
+ createRef = (ele: HTMLDivElement | null) => {
+ this._masonryGridRef = ele;
+ this.createDropTarget(ele!);
+ }
+ @undoBatch
+ @action
+ public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.isMinimized = !isMinimized;
+ });
+ }
+ }
+
+ @computed
+ get singleColumnChildren() {
+ return this.childDocs.filter(d => !d.isMinimized).map((d, i) => {
+ let dref = React.createRef<HTMLDivElement>();
+ let script = undefined;
+ let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
+ let margin = colWidth() < this.columnWidth ? "auto" : undefined;
+ let rowHeight = () => {
+ let hgt = d[HeightSym]();
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ if (nw && nh) hgt = nh / nw * colWidth();
+ return hgt;
+ }
+ let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
+ return <div className="collectionStackingView-masonryDoc"
+ key={d[Id]}
+ ref={dref}
+ style={{ marginTop: `${i ? 2 * this.gridGap : 0}px`, width: colWidth(), height: rowHeight(), marginLeft: margin, marginRight: margin }} >
+ <CollectionSchemaPreview
+ Document={d}
+ width={colWidth}
+ height={rowHeight}
+ getTransform={dxf}
+ CollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={emptyFunction}
+ previewScript={script}>
+ </CollectionSchemaPreview>
+ </div>;
+ });
+ }
+ @computed
+ get children() {
+ return this.childDocs.filter(d => !d.isMinimized).map(d => {
+ let dref = React.createRef<HTMLDivElement>();
+ let dxf = () => this.getDocTransform(d, dref.current!);
+ let colSpan = Math.ceil(Math.min(d[WidthSym](), this.columnWidth + this.gridGap) / (this.gridSize + this.gridGap));
+ let rowSpan = Math.ceil((this.columnWidth / d[WidthSym]() * d[HeightSym]() + this.gridGap) / (this.gridSize + this.gridGap));
+ let childFocus = (doc: Doc) => {
+ doc.libraryBrush = true;
+ this.props.focus(this.props.Document); // just focus on this collection, not the underlying document because the API doesn't support adding an offset to focus on and we can't pan zoom our contents to be centered.
+ }
+ return (<div className="collectionStackingView-masonryDoc"
+ key={d[Id]}
+ ref={dref}
+ style={{
+ width: NumCast(d.nativeWidth, d[WidthSym]()),
+ height: NumCast(d.nativeHeight, d[HeightSym]()),
+ transformOrigin: "top left",
+ gridRowEnd: `span ${rowSpan}`,
+ gridColumnEnd: `span ${colSpan}`,
+ transform: `scale(${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())}, ${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())})`
+ }} >
+ <DocumentView key={d[Id]} Document={d}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ moveDocument={this.moveDocument}
+ ContainingCollectionView={this.props.CollectionView}
+ isTopMost={false}
+ ScreenToLocalTransform={dxf}
+ focus={childFocus}
+ ContentScaling={returnOne}
+ PanelWidth={d[WidthSym]}
+ PanelHeight={d[HeightSym]}
+ selectOnLoad={false}
+ parentActive={this.props.active}
+ addDocTab={this.props.addDocTab}
+ bringToFront={emptyFunction}
+ whenActiveChanged={this.props.whenActiveChanged}
+ collapseToPoint={this.collapseToPoint}
+ />
+ </div>);
+ })
+ }
+ render() {
+ let leftMargin = 2 * this.gridGap;
+ let topMargin = 2 * this.gridGap;
+ let itemCols = Math.ceil(this.columnWidth / (this.gridSize + this.gridGap));
+ let cells = Math.floor((this.props.PanelWidth() - leftMargin) / (itemCols * (this.gridSize + this.gridGap)));
+ return (
+ <div className="collectionStackingView" style={{ height: "100%" }}
+ ref={this.createRef} onWheel={(e: React.WheelEvent) => e.stopPropagation()}>
+ <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`}
+ style={{
+ padding: `${topMargin}px 0px 0px ${leftMargin}px`,
+ width: this.singleColumn ? "100%" : `${cells * itemCols * (this.gridSize + this.gridGap) + leftMargin}`,
+ height: "100%",
+ overflow: "hidden",
+ marginRight: "auto",
+ position: "relative",
+ gridGap: this.gridGap,
+ gridTemplateColumns: this.singleColumn ? undefined : `repeat(auto-fill, minmax(${this.gridSize}px,1fr))`,
+ gridAutoRows: this.singleColumn ? undefined : `${this.gridSize}px`
+ }}
+ >
+ {this.singleColumn ? this.singleColumnChildren : this.children}
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 828ac880a..fe9e12640 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -10,13 +10,13 @@ import * as rp from 'request-promise';
import { CollectionView } from "./CollectionView";
import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
-import { Doc, Opt } from "../../../new_fields/Doc";
+import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc";
import { DocComponent } from "../DocComponent";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, PromiseValue, FieldValue } from "../../../new_fields/Types";
+import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
import { DocServer } from "../../DocServer";
-import { ObjectField } from "../../../new_fields/ObjectField";
+import CursorField from "../../../new_fields/CursorField";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -30,8 +30,6 @@ export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
}
-export type CursorEntry = TupleField<[string, string], [number, number]>;
-
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
@@ -47,33 +45,32 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
this.createDropTarget(ele);
}
- get children() {
+ get childDocs() {
//TODO tfs: This might not be what we want?
//This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
- return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => FieldValue(doc));
+ return DocListCast(this.props.Document[this.props.fieldKey]);
}
@action
protected async setCursorPosition(position: [number, number]) {
- return;
let ind;
let doc = this.props.Document;
let id = CurrentUserUtils.id;
let email = CurrentUserUtils.email;
+ let pos = { x: position[0], y: position[1] };
if (id && email) {
- let textInfo: [string, string] = [id, email];
const proto = await doc.proto;
if (!proto) {
return;
}
- let cursors = await Cast(proto.cursors, listSpec(ObjectField));
+ let cursors = Cast(proto.cursors, listSpec(CursorField));
if (!cursors) {
- proto.cursors = cursors = new List<ObjectField>();
+ proto.cursors = cursors = new List<CursorField>();
}
- if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
- cursors[ind].Data[1] = position;
+ if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
+ cursors[ind].setPosition(pos);
} else {
- let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
+ let entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos });
cursors.push(entry);
}
}
@@ -132,7 +129,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
options.dropAction = "copy";
}
if (type.indexOf("html") !== -1) {
- if (path.includes('localhost')) {
+ if (path.includes(window.location.hostname)) {
let s = path.split('/');
let id = s[s.length - 1];
DocServer.GetRefField(id).then(field => {
@@ -156,6 +153,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
@action
protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ if (e.ctrlKey) {
+ e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
+ return;
+ }
let html = e.dataTransfer.getData("text/html");
let text = e.dataTransfer.getData("text/plain");
@@ -170,6 +171,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
this.props.addDocument(htmlDoc, false);
return;
}
+ if (text && text.indexOf("www.youtube.com/watch") !== -1) {
+ const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");
+ this.props.addDocument(Docs.WebDocument(url, { ...options, width: 300, height: 300 }));
+ return;
+ }
let batch = UndoManager.StartBatch("collection view drop");
let promises: Promise<void>[] = [];
@@ -206,7 +212,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
let path = window.location.origin + file;
- let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName });
+ let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName });
docPromise.then(doc => doc && this.props.addDocument(doc));
}));
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 19d4abc05..bb3be0a73 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -23,43 +23,44 @@
margin: 5px 0;
}
- .collection-child {
- margin-top: 10px;
- margin-bottom: 10px;
- }
.no-indent {
padding-left: 0;
}
.bullet {
- position: absolute;
- width: 1.5em;
- display: inline-block;
+ float: left;
+ position: relative;
+ width: 15px;
+ display: block;
color: $intermediate-color;
margin-top: 3px;
- transform: scale(1.3,1.3);
- }
-
- .coll-title {
- width:max-content;
- display: block;
- font-size: 24px;
+ transform: scale(1.3, 1.3);
}
.docContainer {
margin-left: 10px;
display: block;
- width: max-content;
+ // width:100%;//width: max-content;
}
.docContainer:hover {
- .delete-button {
- display: inline;
- // width: auto;
+ .treeViewItem-openRight {
+ display: inline-block;
+ // display: inline;
+ svg {
+ display:block;
+ padding:0px;
+ margin: 0px;
+ }
}
}
+
+ .editableView-container {
+ font-weight: bold;
+ }
+
.delete-button {
color: $intermediate-color;
// float: right;
@@ -67,4 +68,32 @@
// margin-top: 3px;
display: inline;
}
+
+ .treeViewItem-openRight {
+ margin-left: 5px;
+ display: none;
+ }
+
+ .docContainer:hover {
+ .delete-button {
+ display: inline;
+ // width: auto;
+ }
+ }
+
+ .coll-title {
+ width: max-content;
+ display: block;
+ font-size: 24px;
+ }
+
+ .collection-child {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ }
+
+ .collectionTreeView-keyHeader {
+ font-style: italic;
+ font-size: 8pt;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index b67d6f965..2814c0502 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,5 +1,5 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
@@ -9,15 +9,17 @@ import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
import { Document, listSpec } from '../../../new_fields/Schema';
-import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types';
-import { Doc } from '../../../new_fields/Doc';
-import { Id } from '../../../new_fields/RefField';
+import { Cast, StrCast, BoolCast, FieldValue, NumCast } from '../../../new_fields/Types';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
import { ContextMenu } from '../ContextMenu';
import { undoBatch } from '../../util/UndoManager';
-import { Main } from '../Main';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
import { CollectionDockingView } from './CollectionDockingView';
import { DocumentManager } from '../../util/DocumentManager';
+import { Docs } from '../../documents/Documents';
+import { MainView } from '../MainView';
+import { CollectionViewType } from './CollectionBaseView';
export interface TreeViewProps {
@@ -25,6 +27,7 @@ export interface TreeViewProps {
deleteDoc: (doc: Doc) => void;
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
+ addDocTab: (doc: Doc, where: string) => void;
}
export enum BulletType {
@@ -34,6 +37,7 @@ export enum BulletType {
}
library.add(faTrashAlt);
+library.add(faAngleRight);
library.add(faCaretDown);
library.add(faCaretRight);
@@ -45,15 +49,27 @@ class TreeView extends React.Component<TreeViewProps> {
@observable _collapsed: boolean = true;
- delete = () => this.props.deleteDoc(this.props.document);
+ @undoBatch delete = () => this.props.deleteDoc(this.props.document);
+
+ @undoBatch openRight = async () => {
+ if (this.props.document.dockingConfig) {
+ MainView.Instance.openWorkspace(this.props.document);
+ } else {
+ this.props.addDocTab(this.props.document, "openRight");
+ }
+ }
get children() {
return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc));
}
+ onPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ }
+
@action
- remove = (document: Document) => {
- let children = Cast(this.props.document.data, listSpec(Doc), []);
+ remove = (document: Document, key: string) => {
+ let children = Cast(this.props.document[key], listSpec(Doc), []);
if (children) {
children.splice(children.indexOf(document), 1);
}
@@ -65,7 +81,7 @@ class TreeView extends React.Component<TreeViewProps> {
return true;
}
//TODO This should check if it was removed
- this.remove(document);
+ this.remove(document, "data");
return addDoc(document);
}
@@ -79,6 +95,15 @@ class TreeView extends React.Component<TreeViewProps> {
return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>;
}
+ @action
+ onMouseEnter = () => {
+ this._isOver = true;
+ }
+ @observable _isOver: boolean = false;
+ @action
+ onMouseLeave = () => {
+ this._isOver = false;
+ }
/**
* Renders the EditableView title element for placement into the tree.
*/
@@ -87,7 +112,8 @@ class TreeView extends React.Component<TreeViewProps> {
let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction);
let editableView = (titleString: string) =>
(<EditableView
- display={"inline"}
+ oneLine={!this._isOver ? true : false}
+ display={"inline-block"}
contents={titleString}
height={36}
GetValue={() => StrCast(this.props.document.title)}
@@ -97,46 +123,73 @@ class TreeView extends React.Component<TreeViewProps> {
return true;
}}
/>);
+ let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : [];
+ let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
+ <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
+ <FontAwesomeIcon icon="angle-right" size="lg" />
+ {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */}
+ </div>);
return (
- <div className="docContainer" ref={reference} onPointerDown={onItemDown}>
+ <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
+ style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
{editableView(StrCast(this.props.document.title))}
- {/* <div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div> */}
+ {openRight}
+ {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */}
</div >);
}
onWorkspaceContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) });
- if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" });
+ if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
+ ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" });
+ if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ }
+ ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ } else {
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
}
- ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
e.stopPropagation();
}
}
- onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; }
- onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; }
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; };
render() {
let bulletType = BulletType.List;
- let contentElement: JSX.Element | null = (null);
- var children = Cast(this.props.document.data, listSpec(Doc));
- if (children) { // add children for a collection
- if (!this._collapsed) {
- bulletType = BulletType.Collapsible;
- contentElement = <ul>
- {TreeView.GetChildElements(children, this.remove, this.move, this.props.dropAction)}
- </ul >;
- }
- else bulletType = BulletType.Collapsed;
+ let contentElement: (JSX.Element | null)[] = [];
+ let keys = Array.from(Object.keys(this.props.document));
+ if (this.props.document.proto instanceof Doc) {
+ keys.push(...Array.from(Object.keys(this.props.document.proto)));
+ while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
}
+ keys.map(key => {
+ let docList = DocListCast(this.props.document[key]);
+ let doc = Cast(this.props.document[key], Doc);
+ if (doc instanceof Doc || docList.length) {
+ if (!this._collapsed) {
+ bulletType = BulletType.Collapsible;
+ let spacing = (key === "data") ? 0 : -10;
+ contentElement.push(<ul key={key + "more"}>
+ {(key === "data") ? (null) :
+ <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>}
+ <div style={{ display: "block", marginTop: `${spacing}px` }}>
+ {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction, this.props.addDocTab)}
+ </div>
+ </ul >);
+ } else {
+ bulletType = BulletType.Collapsed;
+ }
+ }
+ });
return <div className="treeViewItem-container"
- style={{ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
- onContextMenu={this.onWorkspaceContextMenu}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ onContextMenu={this.onWorkspaceContextMenu}>
<li className="collection-child">
{this.renderBullet(bulletType)}
{this.renderTitle()}
@@ -144,9 +197,9 @@ class TreeView extends React.Component<TreeViewProps> {
</li>
</div>;
}
- public static GetChildElements(docs: Doc[], remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
- return docs.filter(child => !child.excludeFromLibrary).filter(doc => FieldValue(doc)).map(child =>
- <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
+ public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => void) {
+ return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child =>
+ <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} />);
}
}
@@ -160,27 +213,24 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
}
onContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => Main.Instance.createNewWorkspace()) });
- }
- if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) {
- ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.remove(this.props.Document)) });
+ // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
+ if (!e.isPropagationStopped() && this.props.Document.excludeFromLibrary) { // excludeFromLibrary means this is the user document
+ ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) });
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) });
}
}
render() {
- trace();
- const children = this.children;
let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
- if (!children) {
+ if (!this.childDocs) {
return (null);
}
- let childElements = TreeView.GetChildElements(children, this.remove, this.props.moveDocument, dropAction);
+ let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction, this.props.addDocTab);
return (
<div id="body" className="collectionTreeView-dropTarget"
style={{ borderRadius: "inherit" }}
onContextMenu={this.onContextMenu}
- onWheel={(e: React.WheelEvent) => e.stopPropagation()}
+ onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()}
onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
<div className="coll-title">
<EditableView
diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss
index ed56ad268..9d2c23d3e 100644
--- a/src/client/views/collections/CollectionVideoView.scss
+++ b/src/client/views/collections/CollectionVideoView.scss
@@ -2,10 +2,10 @@
.collectionVideoView-cont{
width: 100%;
height: 100%;
- position: absolute;
+ position: inherit;
top: 0;
left:0;
-
+ z-index: -1;
}
.collectionVideoView-time{
color : white;
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 9dee217cb..7853544d5 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,4 +1,5 @@
import { action, observable, trace } from "mobx";
+import * as htmlToImage from "html-to-image";
import { observer } from "mobx-react";
import { ContextMenu } from "../ContextMenu";
import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView";
@@ -6,30 +7,33 @@ import React = require("react");
import "./CollectionVideoView.scss";
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { emptyFunction } from "../../../Utils";
-import { Id } from "../../../new_fields/RefField";
+import { emptyFunction, Utils } from "../../../Utils";
+import { Id } from "../../../new_fields/FieldSymbols";
import { VideoBox } from "../nodes/VideoBox";
+import { NumCast, Cast, StrCast } from "../../../new_fields/Types";
+import { VideoField } from "../../../new_fields/URLField";
+import { SearchBox } from "../SearchBox";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils } from "../../documents/Documents";
@observer
export class CollectionVideoView extends React.Component<FieldViewProps> {
- private _videoBox: VideoBox | undefined = undefined;
- @observable _playTimer?: NodeJS.Timeout = undefined;
-
- @observable _currentTimecode: number = 0;
+ private _videoBox?: VideoBox;
public static LayoutString(fieldKey: string = "data") {
return FieldView.LayoutString(CollectionVideoView, fieldKey);
}
private get uIButtons() {
let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
+ let curTime = NumCast(this.props.Document.curPage);
return ([
<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- <span>{"" + Math.round(this._currentTimecode)}</span>
- <span style={{ fontSize: 8 }}>{" " + Math.round((this._currentTimecode - Math.trunc(this._currentTimecode)) * 100)}</span>
+ <span>{"" + Math.round(curTime)}</span>
+ <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
</div>,
<div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- {this._playTimer ? "\"" : ">"}
+ {this._videoBox && this._videoBox.Playing ? "\"" : ">"}
</div>,
<div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
F
@@ -38,36 +42,20 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
@action
- updateTimecode = () => {
- if (this._videoBox && this._videoBox.player) {
- this._currentTimecode = this._videoBox.player.currentTime;
- this.props.Document.curPage = Math.round(this._currentTimecode);
- }
- }
-
- componentDidMount() { this.updateTimecode(); }
-
- componentWillUnmount() { if (this._playTimer) clearInterval(this._playTimer); }
-
- @action
onPlayDown = () => {
if (this._videoBox && this._videoBox.player) {
- if (this._videoBox.player.paused) {
- this._videoBox.player.play();
- if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000);
+ if (this._videoBox.Playing) {
+ this._videoBox.Pause();
} else {
- this._videoBox.player.pause();
- if (this._playTimer) clearInterval(this._playTimer);
- this._playTimer = undefined;
-
+ this._videoBox.Play();
}
}
}
@action
onFullDown = (e: React.PointerEvent) => {
- if (this._videoBox && this._videoBox.player) {
- this._videoBox.player.requestFullscreen();
+ if (this._videoBox) {
+ this._videoBox.FullScreen();
e.stopPropagation();
e.preventDefault();
}
@@ -75,22 +63,55 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
@action
onResetDown = () => {
- if (this._videoBox && this._videoBox.player) {
- this._videoBox.player.pause();
- this._videoBox.player.currentTime = 0;
- if (this._playTimer) clearInterval(this._playTimer);
- this._playTimer = undefined;
- this.updateTimecode();
+ if (this._videoBox) {
+ this._videoBox.Pause();
+ this.props.Document.curPage = 0;
}
}
onContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "VideoOptions", event: emptyFunction });
}
+
+ let field = Cast(this.props.Document[this.props.fieldKey], VideoField);
+ if (field) {
+ let url = field.url.href;
+ ContextMenu.Instance.addItem({
+ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt"
+ });
+ }
+ let width = NumCast(this.props.Document.width);
+ let height = NumCast(this.props.Document.height);
+ ContextMenu.Instance.addItem({
+ description: "Take Snapshot", event: async () => {
+ var canvas = document.createElement('canvas');
+ canvas.width = 640;
+ canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth);
+ var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+ ctx && ctx.drawImage(this._videoBox!.player!, 0, 0, canvas.width, canvas.height);
+
+ //convert to desired file format
+ var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // if you want to preview the captured image,
+
+ let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
+ SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ let imageSummary = Docs.ImageDocument(url, {
+ x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
+ width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-"
+ });
+ this.props.addDocument && this.props.addDocument(imageSummary, false);
+ DocUtils.MakeLink(imageSummary, this.props.Document);
+ }
+ });
+ },
+ icon: "expand-arrows-alt"
+ });
}
- setVideoBox = (player: VideoBox) => { this._videoBox = player; }
+ setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
@@ -101,6 +122,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
render() {
+ trace();
return (
<CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}>
{this.subView}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 8c1442d38..68eefab4c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,17 +1,27 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
+import { observer } from "mobx-react";
import * as React from 'react';
-import { FieldViewProps, FieldView } from '../nodes/FieldView';
-import { CollectionBaseView, CollectionViewType, CollectionRenderProps } from './CollectionBaseView';
-import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
-import { CollectionSchemaView } from './CollectionSchemaView';
-import { CollectionDockingView } from './CollectionDockingView';
-import { CollectionTreeView } from './CollectionTreeView';
-import { ContextMenu } from '../ContextMenu';
+import { Id } from '../../../new_fields/FieldSymbols';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { observer } from 'mobx-react';
import { undoBatch } from '../../util/UndoManager';
-import { trace } from 'mobx';
-import { Id } from '../../../new_fields/RefField';
-import { Main } from '../Main';
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from '../ContextMenuItem';
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView';
+import { CollectionDockingView } from "./CollectionDockingView";
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionSchemaView } from "./CollectionSchemaView";
+import { CollectionStackingView } from './CollectionStackingView';
+import { CollectionTreeView } from "./CollectionTreeView";
+export const COLLECTION_BORDER_WIDTH = 2;
+
+library.add(faTh);
+library.add(faTree);
+library.add(faSquare);
+library.add(faProjectDiagram);
+library.add(faSignature);
+library.add(faThList);
@observer
export class CollectionView extends React.Component<FieldViewProps> {
@@ -23,6 +33,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);
case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);
case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);
+ case CollectionViewType.Stacking: return (<CollectionStackingView {...props} CollectionView={this} />);
case CollectionViewType.Freeform:
default:
return (<CollectionFreeFormView {...props} CollectionView={this} />);
@@ -34,9 +45,15 @@ export class CollectionView extends React.Component<FieldViewProps> {
onContextMenu = (e: React.MouseEvent): void => {
if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform) });
- ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema) });
- ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree) });
+ let subItems: ContextMenuProps[] = [];
+ subItems.push({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "signature" });
+ if (CollectionBaseView.InSafeMode()) {
+ ContextMenu.Instance.addItem({ description: "Test Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Invalid), icon: "project-diagram" });
+ }
+ subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" });
+ subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });
+ subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" });
+ ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });
}
}
diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss
new file mode 100644
index 000000000..2dd3e49f2
--- /dev/null
+++ b/src/client/views/collections/ParentDocumentSelector.scss
@@ -0,0 +1,22 @@
+.PDS-flyout {
+ position: absolute;
+ z-index: 9999;
+ background-color: #eeeeee;
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
+ min-width: 150px;
+ color: black;
+ top: 12px;
+
+ padding: 10px;
+ border-radius: 3px;
+
+ hr {
+ height: 1px;
+ margin: 0px;
+ background-color: gray;
+ border-top: 0px;
+ border-bottom: 0px;
+ border-right: 0px;
+ border-left: 0px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
new file mode 100644
index 000000000..f11af04a3
--- /dev/null
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -0,0 +1,94 @@
+import * as React from "react";
+import './ParentDocumentSelector.scss';
+import { Doc } from "../../../new_fields/Doc";
+import { observer } from "mobx-react";
+import { observable, action, runInAction } from "mobx";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { SearchUtil } from "../../util/SearchUtil";
+import { CollectionDockingView } from "./CollectionDockingView";
+import { NumCast } from "../../../new_fields/Types";
+import { CollectionViewType } from "./CollectionBaseView";
+
+type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void };
+@observer
+export class SelectorContextMenu extends React.Component<SelectorProps> {
+ @observable private _docs: { col: Doc, target: Doc }[] = [];
+ @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
+
+ constructor(props: SelectorProps) {
+ super(props);
+
+ this.fetchDocuments();
+ }
+
+ async fetchDocuments() {
+ let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document);
+ const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true);
+ const map: Map<Doc, Doc> = new Map;
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true)));
+ allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
+ docs.forEach(doc => map.delete(doc));
+ runInAction(() => {
+ this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
+ this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+ });
+ }
+
+ getOnClick({ col, target }: { col: Doc, target: Doc }) {
+ return () => {
+ col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
+ if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2;
+ col.panX = newPanX;
+ col.panY = newPanY;
+ }
+ this.props.addDocTab(col, "inTab");
+ };
+ }
+
+ render() {
+ return (
+ <>
+ <p>Contexts:</p>
+ {this._docs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ {this._otherDocs.length ? <hr></hr> : null}
+ {this._otherDocs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ </>
+ );
+ }
+}
+
+@observer
+export class ParentDocSelector extends React.Component<SelectorProps> {
+ @observable hover = false;
+
+ @action
+ onMouseLeave = () => {
+ this.hover = false;
+ }
+
+ @action
+ onMouseEnter = () => {
+ this.hover = true;
+ }
+
+ render() {
+ let flyout;
+ if (this.hover) {
+ flyout = (
+ <div className="PDS-flyout" title=" ">
+ <SelectorContextMenu {...this.props} />
+ </div>
+ );
+ }
+ return (
+ <span style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}>
+ <p>^</p>
+ {flyout}
+ </span>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 3e8a8a442..737ffba7d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,12 +1,12 @@
.collectionfreeformlinkview-linkLine {
stroke: black;
- stroke-width: 3;
transform: translate(10000px,10000px);
+ opacity: 0.5;
pointer-events: all;
}
.collectionfreeformlinkview-linkCircle {
- stroke: black;
- stroke-width: 3;
+ stroke: rgb(0,0,0);
+ opacity: 0.5;
transform: translate(10000px,10000px);
pointer-events: all;
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 3b700b053..61de83f57 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -40,18 +40,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let l = this.props.LinkDocs;
let a = this.props.A;
let b = this.props.B;
- let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2);
- let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2);
- let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2);
- let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2);
+ let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
return (
<>
- <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
- style={{ strokeWidth: `${l.length * 5}` }}
+ <line key={"linkLine"} className="collectionfreeformlinkview-linkLine"
+ style={{ strokeWidth: `${35 * l.length / 2}` }}
x1={`${x1}`} y1={`${y1}`}
x2={`${x2}`} y2={`${y2}`} />
- <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
- cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={10} onPointerDown={this.onPointerDown} />
+ <circle key={"linkCircle"} className="collectionfreeformlinkview-linkCircle"
+ cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} />
</>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index b34e0856e..a43c5f241 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer, reaction, trace } from "mobx";
import { observer } from "mobx-react";
import { Utils } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
@@ -7,70 +7,68 @@ import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
-import { Doc } from "../../../../new_fields/Doc";
+import { Doc, DocListCastAsync, DocListCast } from "../../../../new_fields/Doc";
import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
import { listSpec } from "../../../../new_fields/Schema";
import { List } from "../../../../new_fields/List";
-import { Id } from "../../../../new_fields/RefField";
+import { Id } from "../../../../new_fields/FieldSymbols";
@observer
export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
_brushReactionDisposer?: IReactionDisposer;
componentDidMount() {
- this._brushReactionDisposer = reaction(() => Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).map(doc => NumCast(doc.x)),
+ this._brushReactionDisposer = reaction(
() => {
- let views = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => StrCast(doc.backgroundLayout, "").indexOf("istogram") !== -1);
- for (let i = 0; i < views.length; i++) {
- for (let j = 0; j < views.length; j++) {
- let srcDoc = views[j];
- let dstDoc = views[i];
- let x1 = NumCast(srcDoc.x);
- let x1w = NumCast(srcDoc.width, -1);
- let x2 = NumCast(dstDoc.x);
- let x2w = NumCast(dstDoc.width, -1);
- if (x1w < 0 || x2w < 0 || i === j) {
- continue;
- }
+ let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
+ },
+ () => {
+ let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
+ views.forEach((dstDoc, i) => {
+ views.forEach((srcDoc, j) => {
let dstTarg = dstDoc;
let srcTarg = srcDoc;
- let findBrush = (field: List<Doc>) => field.findIndex(brush => {
- let bdocs = brush ? Cast(brush.brushingDocs, listSpec(Doc), []) : [];
- return (bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false);
- });
- let brushAction = (field: List<Doc>) => {
- let found = findBrush(field);
- if (found !== -1) {
- console.log("REMOVE BRUSH " + srcTarg.Title + " " + dstTarg.Title);
- field.splice(found, 1);
- }
- };
- if (Math.abs(x1 + x1w - x2) < 20) {
- let linkDoc: Doc = new Doc();
- linkDoc.title = "Histogram Brush";
- linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
- linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
-
- brushAction = (field: List<Doc>) => {
- if (findBrush(field) === -1) {
- console.log("ADD BRUSH " + srcTarg.Title + " " + dstTarg.Title);
- (findBrush(field) === -1) && field.push(linkDoc);
+ let x1 = NumCast(srcDoc.x);
+ let x2 = NumCast(dstDoc.x);
+ let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ if (x1w < 0 || x2w < 0 || i === j) { }
+ else {
+ let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
+ let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
+ return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
+ });
+ let brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ let found = findBrush(field);
+ if (found !== -1) {
+ console.log("REMOVE BRUSH " + srcTarg.title + " " + dstTarg.title);
+ field.splice(found, 1);
}
};
- }
- let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc));
- if (dstBrushDocs === undefined) {
- dstTarg.brushingDocs = dstBrushDocs = new List<Doc>();
- }
- let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc));
- if (srcBrushDocs === undefined) {
- srcTarg.brushingDocs = srcBrushDocs = new List<Doc>();
- }
- brushAction(dstBrushDocs);
- brushAction(srcBrushDocs);
+ if (Math.abs(x1 + x1w - x2) < 20) {
+ let linkDoc: Doc = new Doc();
+ linkDoc.title = "Histogram Brush";
+ linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
+ linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
- }
- }
+ brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ if (findBrush(field) === -1) {
+ console.log("ADD BRUSH " + srcTarg.title + " " + dstTarg.title);
+ field.push(linkDoc);
+ }
+ };
+ }
+ if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
+ if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
+ let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ brushAction(dstBrushDocs);
+ brushAction(srcBrushDocs);
+ }
+ });
+ });
});
}
componentWillUnmount() {
@@ -86,7 +84,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
}
if (view.props.ContainingCollectionView) {
let collid = view.props.ContainingCollectionView.props.Document[Id];
- Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).
+ DocListCast(this.props.Document[this.props.fieldKey]).
filter(child =>
child[Id] === collid).map(view =>
DocumentManager.Instance.getDocumentViews(view).map(view =>
@@ -102,21 +100,27 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
let targetViews = this.documentAnchors(connection.b);
let possiblePairs: { a: Doc, b: Doc, }[] = [];
srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
- possiblePairs.map(possiblePair =>
- drawnPairs.reduce((found, drawnPair) => {
- let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b);
+ possiblePairs.map(possiblePair => {
+ if (!drawnPairs.reduce((found, drawnPair) => {
+ let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
+ let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
+ let match = match1 || match2;
if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
drawnPair.l.push(connection.l);
}
return match || found;
- }, false)
- ||
- drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
- );
+ }, false)) {
+ console.log("A" + possiblePair.a[Id] + " B" + possiblePair.b[Id] + " L" + connection.l[Id]);
+ drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
+ }
+ });
return drawnPairs;
}, [] as { a: Doc, b: Doc, l: Doc[] }[]);
- return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l}
- removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
+ return connections.map(c => {
+ let x = c.l.reduce((p, l) => p + l[Id], "");
+ return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
+ removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
+ });
}
render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index 036745eca..2838b7905 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -1,26 +1,38 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { CollectionViewProps, CursorEntry } from "../CollectionSubView";
+import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
import v5 = require("uuid/v5");
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+import CursorField from "../../../../new_fields/CursorField";
+import { List } from "../../../../new_fields/List";
+import { Cast } from "../../../../new_fields/Types";
+import { listSpec } from "../../../../new_fields/Schema";
+import * as mobxUtils from 'mobx-utils';
@observer
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
- protected getCursors(): CursorEntry[] {
+
+ protected getCursors(): CursorField[] {
let doc = this.props.Document;
+
let id = CurrentUserUtils.id;
- let cursors = doc.GetList(KeyStore.Cursors, [] as CursorEntry[]);
- let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
- return id ? notMe : [];
+ if (!id) {
+ return [];
+ }
+
+ let cursors = Cast(doc.cursors, listSpec(CursorField));
+
+ const now = mobxUtils.now();
+ // const now = Date.now();
+ return (cursors || []).filter(cursor => cursor.data.metadata.id !== id && (now - cursor.data.metadata.timestamp) < 1000);
}
private crosshairs?: HTMLCanvasElement;
drawCrosshairs = (backgroundColor: string) => {
if (this.crosshairs) {
- let c = this.crosshairs;
- let ctx = c.getContext('2d');
+ let ctx = this.crosshairs.getContext('2d');
if (ctx) {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, 20, 20);
@@ -49,29 +61,26 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
}
}
}
- @computed
+
get sharedCursors() {
- return this.getCursors().map(entry => {
- if (entry.Data.length > 0) {
- let id = entry.Data[0][0];
- let email = entry.Data[0][1];
- let point = entry.Data[1];
- this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22");
- return (
- <div key={id} className="collectionFreeFormRemoteCursors-cont"
- style={{ transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)` }}
- >
- <canvas className="collectionFreeFormRemoteCursors-canvas"
- ref={(el) => { if (el) this.crosshairs = el; }}
- width={20}
- height={20}
- />
- <p className="collectionFreeFormRemoteCursors-symbol">
- {email[0].toUpperCase()}
- </p>
- </div>
- );
- }
+ return this.getCursors().map(c => {
+ let m = c.data.metadata;
+ let l = c.data.position;
+ this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22");
+ return (
+ <div key={m.id} className="collectionFreeFormRemoteCursors-cont"
+ style={{ transform: `translate(${l.x - 10}px, ${l.y - 10}px)` }}
+ >
+ <canvas className="collectionFreeFormRemoteCursors-canvas"
+ ref={(el) => { if (el) this.crosshairs = el; }}
+ width={20}
+ height={20}
+ />
+ <p className="collectionFreeFormRemoteCursors-symbol">
+ {m.identifier[0].toUpperCase()}
+ </p>
+ </div>
+ );
});
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index cb849b325..e10ba9d7e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -25,6 +25,9 @@
height: 100%;
width: 100%;
}
+ >.jsx-parser {
+ z-index:0;
+ }
//nested freeform views
// .collectionfreeformview-container {
@@ -37,7 +40,9 @@
border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
- overflow: hidden;
+ .marqueeView {
+ overflow: hidden;
+ }
top: 0;
left: 0;
width: 100%;
@@ -50,6 +55,10 @@
position: inherit;
height: 100%;
}
+
+ >.jsx-parser {
+ z-index:0;
+ }
.formattedTextBox-cont {
background: $light-color-secondary;
@@ -61,7 +70,10 @@
border-radius: $border-radius;
box-sizing: border-box;
position:absolute;
- overflow: hidden;
+ z-index: -1;
+ .marqueeView {
+ overflow: hidden;
+ }
top: 0;
left: 0;
width: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 69b880d20..e741953d6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -22,9 +22,11 @@ import v5 = require("uuid/v5");
import { Timeline } from "../../nodes/Timeline";
import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { FieldValue, Cast, NumCast } from "../../../../new_fields/Types";
+import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types";
import { pageSchema } from "../../nodes/ImageBox";
-import { Id } from "../../../../new_fields/RefField";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { HistoryUtil } from "../../../util/History";
+import { Id } from "../../../../new_fields/FieldSymbols";
export const panZoomSchema = createSchema({
panX: "number",
@@ -37,23 +39,22 @@ const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema)
@observer
export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
- public static RIGHT_BTN_DRAG = false;
private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
private _lastX: number = 0;
private _lastY: number = 0;
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
- @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
- @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
+ @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
- private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
- private panX = () => FieldValue(this.Document.panX, 0);
- private panY = () => FieldValue(this.Document.panY, 0);
- private zoomScaling = () => FieldValue(this.Document.scale, 1);
+ private panX = () => this.Document.panX || 0;
+ private panY = () => this.Document.panY || 0;
+ private zoomScaling = () => this.Document.scale || 1;
private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections
private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections
- private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
+ private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
private addLiveTextBox = (newBox: Doc) => {
@@ -66,13 +67,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return true;
}
private selectDocuments = (docs: Doc[]) => {
- SelectionManager.DeselectAll;
+ SelectionManager.DeselectAll();
docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv =>
SelectionManager.SelectDoc(dv!, true));
}
public getActiveDocuments = () => {
const curPage = FieldValue(this.Document.curPage, -1);
- return FieldValue(this.children, [] as Doc[]).filter((doc) => {
+ return this.childDocs.filter(doc => {
var page = NumCast(doc.page, -1);
return page === curPage || page === -1;
});
@@ -90,7 +91,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let y = yp - de.data.yOffset / zoom;
let dropX = NumCast(de.data.droppedDocuments[0].x);
let dropY = NumCast(de.data.droppedDocuments[0].y);
- de.data.droppedDocuments.map(d => {
+ de.data.droppedDocuments.forEach(d => {
d.x = x + NumCast(d.x) - dropX;
d.y = y + NumCast(d.y) - dropY;
if (!NumCast(d.width)) {
@@ -112,15 +113,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerDown = (e: React.PointerEvent): void => {
- let childSelected = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), [] as Doc[]).filter(doc => doc).reduce((childSelected, doc) => {
- var dv = DocumentManager.Instance.getDocumentView(doc);
- return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false);
- }, false);
- if ((CollectionFreeFormView.RIGHT_BTN_DRAG &&
- (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) ||
- (e.button === 0 && e.altKey)) && (childSelected || this.props.active()))) ||
- (!CollectionFreeFormView.RIGHT_BTN_DRAG &&
- ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && (childSelected || this.props.active())))) {
+ if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -138,29 +131,40 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble) {
- let x = Cast(this.props.Document.panX, "number", 0);
- let y = Cast(this.props.Document.panY, "number", 0);
- let docs = this.children || [];
+ let x = this.Document.panX || 0;
+ let y = this.Document.panY || 0;
+ let docs = this.childDocs || [];
let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
if (!this.isAnnotationOverlay) {
- let minx = docs.length ? Cast(docs[0].x, "number", 0) : 0;
- let maxx = docs.length ? Cast(docs[0].width, "number", 0) + minx : minx;
- let miny = docs.length ? Cast(docs[0].y, "number", 0) : 0;
- let maxy = docs.length ? Cast(docs[0].height, "number", 0) + miny : miny;
+ let minx = docs.length ? NumCast(docs[0].x) : 0;
+ let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis, 1) + minx : minx;
+ let miny = docs.length ? NumCast(docs[0].y) : 0;
+ let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis, 1) + miny : miny;
let ranges = docs.filter(doc => doc).reduce((range, doc) => {
- let x = Cast(doc.x, "number", 0);
- let xe = x + Cast(doc.width, "number", 0);
- let y = Cast(doc.y, "number", 0);
- let ye = y + Cast(doc.height, "number", 0);
+ let x = NumCast(doc.x);
+ let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis, 1);
+ let y = NumCast(doc.y);
+ let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis, 1);
return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
[range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
}, [[minx, maxx], [miny, maxy]]);
- let panelwidth = this._pwidth / this.zoomScaling() / 2;
- let panelheight = this._pheight / this.zoomScaling() / 2;
- if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx;
- if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx;
- if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy;
- if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy;
+ let ink = Cast(this.props.Document.ink, InkField);
+ if (ink && ink.inkData) {
+ ink.inkData.forEach((value: StrokeData, key: string) => {
+ let bounds = InkingCanvas.StrokeRect(value);
+ ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
+ ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
+ });
+ }
+
+ let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling(),
+ this._pheight / this.zoomScaling());
+ let panelwidth = panelDim[0];
+ let panelheight = panelDim[1];
+ if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
+ if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
+ if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
+ if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
}
this.setPan(x - dx, y - dy);
this._lastX = e.pageX;
@@ -175,7 +179,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// if (!this.props.active()) {
// return;
// }
- let childSelected = (this.children || []).filter(doc => doc).some(doc => {
+ let childSelected = this.childDocs.some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
return dv && SelectionManager.IsSelected(dv) ? true : false;
});
@@ -187,8 +191,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (e.ctrlKey) {
let deltaScale = (1 - (e.deltaY / coefficient));
- this.props.Document.nativeWidth = this.nativeWidth * deltaScale;
- this.props.Document.nativeHeight = this.nativeHeight * deltaScale;
+ let nw = this.nativeWidth * deltaScale;
+ let nh = this.nativeHeight * deltaScale;
+ if (nw && nh) {
+ this.props.Document.nativeWidth = nw;
+ this.props.Document.nativeHeight = nh;
+ }
e.stopPropagation();
e.preventDefault();
} else {
@@ -202,7 +210,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY);
let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
- let safeScale = Math.abs(localTransform.Scale);
+ let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
this.props.Document.scale = Math.abs(safeScale);
this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
e.stopPropagation();
@@ -211,6 +219,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
setPan(panX: number, panY: number) {
+ this.props.Document.panTransformType = "None";
var scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY));
@@ -228,33 +237,50 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
bringToFront = (doc: Doc) => {
- const docs = (this.children || []);
+ const docs = this.childDocs;
docs.slice().sort((doc1, doc2) => {
if (doc1 === doc) return 1;
if (doc2 === doc) return -1;
return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
}).forEach((doc, index) => doc.zIndex = index + 1);
doc.zIndex = docs.length + 1;
- return doc;
}
focusDocument = (doc: Doc) => {
+ const panX = this.Document.panX;
+ const panY = this.Document.panY;
+ const id = this.Document[Id];
+ const state = HistoryUtil.getState();
+ // TODO This technically isn't correct if type !== "doc", as
+ // currently nothing is done, but we should probably push a new state
+ if (state.type === "doc" && panX !== undefined && panY !== undefined) {
+ const init = state.initializers[id];
+ if (!init) {
+ state.initializers[id] = {
+ panX, panY
+ };
+ HistoryUtil.pushState(state);
+ } else if (init.panX !== panX || init.panY !== panY) {
+ init.panX = panX;
+ init.panY = panY;
+ HistoryUtil.pushState(state);
+ }
+ }
SelectionManager.DeselectAll();
+ const newPanX = NumCast(doc.x) + NumCast(doc.width) / NumCast(doc.zoomBasis, 1) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(doc.height) / NumCast(doc.zoomBasis, 1) / 2;
+ const newState = HistoryUtil.getState();
+ newState.initializers[id] = { panX: newPanX, panY: newPanY };
+ HistoryUtil.pushState(newState);
+ this.setPan(newPanX, newPanY);
this.props.Document.panTransformType = "Ease";
- this.setPan(
- NumCast(doc.x) + NumCast(doc.width) / 2,
- NumCast(doc.y) + NumCast(doc.height) / 2);
this.props.focus(this.props.Document);
- if (this.props.Document.panTransformType === "Ease") {
- setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false
- }
}
getDocumentViewProps(document: Doc): DocumentViewProps {
return {
Document: document,
- toggleMinimized: emptyFunction,
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
@@ -267,19 +293,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
parentActive: this.props.active,
- whenActiveChanged: this.props.active,
+ whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
+ addDocTab: this.props.addDocTab,
};
}
@computed.struct
get views() {
let curPage = FieldValue(this.Document.curPage, -1);
- let docviews = (this.children || []).filter(doc => doc).reduce((prev, doc) => {
- if (!FieldValue(doc)) return prev;
+ let docviews = this.childDocs.reduce((prev, doc) => {
+ if (!(doc instanceof Doc)) return prev;
var page = NumCast(doc.page, -1);
- if (page === curPage || page === -1) {
- let minim = Cast(doc.isMinimized, "boolean");
+ if (Math.round(page) === Math.round(curPage) || page === -1) {
+ let minim = BoolCast(doc.isMinimized, false);
if (minim === undefined || !minim) {
prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
}
@@ -297,7 +324,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
- private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />];
+ private childViews = () => [
+ <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
+ ...this.views
+ ];
render() {
const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
const easing = () => this.props.Document.panTransformType === "Ease";
@@ -316,23 +346,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
{this.childViews}
</InkingCanvas>
</CollectionFreeFormLinksView>
- {/* <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> */}
+ <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
- <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
</MarqueeView>
- <Timeline {...this.props} />
- </div>
+<<<<<<< HEAD
+ <Timeline {...this.props} />
+=======
+ <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
+>>>>>>> 6f49d067b58caf6297f7ae7687cf05b627c27a1d
+ </div >
);
}
}
@observer
-class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
+class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get overlayView() {
- let overlayLayout = Cast(this.props.Document.overlayLayout, "string", "");
- return !overlayLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />);
+ return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.overlayView;
@@ -342,13 +373,11 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
@observer
class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get backgroundView() {
- let backgroundLayout = Cast(this.props.Document.backgroundLayout, "string", "");
- return !backgroundLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
- return this.backgroundView;
+ return this.props.Document.backgroundLayout ? this.backgroundView : (null);
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index ae0a9fd48..6e8ec8662 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -21,6 +21,6 @@
white-space:nowrap;
}
.marquee-legend::after {
- content: "Press: C (collection), or Delete"
+ content: "Press: c (collection), s (summary), r (replace) or Delete"
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 8c81f6990..29734fa19 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,9 +1,10 @@
-import { action, computed, observable } from "mobx";
+import * as htmlToImage from "html-to-image";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
@@ -13,8 +14,14 @@ import { Utils } from "../../../../Utils";
import { Doc } from "../../../../new_fields/Doc";
import { NumCast, Cast } from "../../../../new_fields/Types";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { Templates } from "../../Templates";
import { List } from "../../../../new_fields/List";
+import { ImageField } from "../../../../new_fields/URLField";
+import { Template, Templates } from "../../Templates";
+import { SearchBox } from "../../SearchBox";
+import { DocServer } from "../../../DocServer";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { CollectionView } from "../CollectionView";
+import { CollectionViewType } from "../CollectionBaseView";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -31,6 +38,7 @@ interface MarqueeViewProps {
@observer
export class MarqueeView extends React.Component<MarqueeViewProps>
{
+ private _mainCont = React.createRef<HTMLDivElement>();
@observable _lastX: number = 0;
@observable _lastY: number = 0;
@observable _downX: number = 0;
@@ -48,28 +56,108 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._visible = false;
}
+ @undoBatch
@action
onKeyPress = (e: KeyboardEvent) => {
//make textbox and add it to this collection
let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY);
- let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
- this.props.addLiveTextDocument(newBox);
+ if (e.key === "q" && e.ctrlKey) {
+ e.preventDefault();
+ (async () => {
+ let text: string = await navigator.clipboard.readText();
+ let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+ for (let i = 0; i < ns.length - 1; i++) {
+ while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") ||
+ ns[i].endsWith(";\r") || ns[i].endsWith(";") ||
+ ns[i].endsWith(".\r") || ns[i].endsWith(".") ||
+ ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) {
+ let sub = ns[i].endsWith("\r") ? 1 : 0;
+ let br = ns[i + 1].trim() === "";
+ ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft());
+ if (br) break;
+ }
+ }
+ ns.map(line => {
+ let indent = line.search(/\S|$/);
+ let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+ this.props.addDocument(newBox, false);
+ y += 40 * this.props.getTransform().Scale;
+ });
+ })();
+ } else if (e.key === "b" && e.ctrlKey) {
+ e.preventDefault();
+ navigator.clipboard.readText().then(text => {
+ let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+ if (ns.length === 1 && text.startsWith("http")) {
+ this.props.addDocument(Docs.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
+ } else {
+ this.pasteTable(ns, x, y);
+ }
+ });
+ } else {
+ let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ this.props.addLiveTextDocument(newBox);
+ }
e.stopPropagation();
}
+ //heuristically converts pasted text into a table.
+ // assumes each entry is separated by a tab
+ // skips all rows until it gets to a row with more than one entry
+ // assumes that 1st row has header entry for each column
+ // assumes subsequent rows have entries for each column header OR
+ // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header
+ // assumes each cell is a string or a number
+ pasteTable(ns: string[], x: number, y: number) {
+ while (ns.length > 0 && ns[0].split("\t").length < 2) {
+ ns.splice(0, 1);
+ }
+ if (ns.length > 0) {
+ let columns = ns[0].split("\t");
+ let docList: Doc[] = [];
+ let groupAttr: string | number = "";
+ let rowProto = new Doc();
+ rowProto.title = rowProto.Id;
+ rowProto.width = 200;
+ rowProto.isPrototype = true;
+ for (let i = 1; i < ns.length - 1; i++) {
+ let values = ns[i].split("\t");
+ if (values.length === 1 && columns.length > 1) {
+ groupAttr = values[0];
+ continue;
+ }
+ let docDataProto = Doc.MakeDelegate(rowProto);
+ docDataProto.isPrototype = true;
+ columns.forEach((col, i) => docDataProto[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined));
+ if (groupAttr) {
+ docDataProto._group = groupAttr;
+ }
+ docDataProto.title = i.toString();
+ let doc = Doc.MakeDelegate(docDataProto);
+ doc.width = 200;
+ docList.push(doc);
+ }
+ let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+
+ this.props.addDocument(newCol, false);
+ }
+ }
@action
onPointerDown = (e: React.PointerEvent): void => {
this._downX = this._lastX = e.pageX;
this._downY = this._lastY = e.pageY;
this._commandExecuted = false;
PreviewCursor.Visible = false;
- if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) ||
- (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) {
+ if (e.button === 2 || (e.button === 0 && e.altKey)) {
+ if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
document.addEventListener("pointermove", this.onPointerMove, true);
document.addEventListener("pointerup", this.onPointerUp, true);
document.addEventListener("keydown", this.marqueeCommand, true);
- }
- if (e.altKey) {
- e.preventDefault();
+ if (e.altKey) {
+ //e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
+ e.preventDefault();
+ }
+ // bcz: do we need this? it kills the context menu on the main collection if !altKey
+ // e.stopPropagation();
}
}
@@ -135,32 +223,39 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@undoBatch
@action
- marqueeCommand = (e: KeyboardEvent) => {
- if (this._commandExecuted) {
+ marqueeCommand = async (e: KeyboardEvent) => {
+ if (this._commandExecuted || (e as any).propagationIsStopped) {
return;
}
- if (e.key === "Backspace" || e.key === "Delete" || e.key == "d") {
+ if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") {
this._commandExecuted = true;
+ e.stopPropagation();
+ (e as any).propagationIsStopped = true;
this.marqueeSelect().map(d => this.props.removeDocument(d));
let ink = Cast(this.props.container.props.Document.ink, InkField);
if (ink) {
this.marqueeInkDelete(ink.inkData);
}
+ SelectionManager.DeselectAll();
this.cleanupInteractions(false);
e.stopPropagation();
}
- if (e.key === "c" || e.key === "r" || e.key === "e") {
+ if (e.key === "c" || e.key === "s" || e.key === "S" || e.key === "e" || e.key === "p") {
this._commandExecuted = true;
e.stopPropagation();
+ e.preventDefault();
+ (e as any).propagationIsStopped = true;
let bounds = this.Bounds;
- let selected = this.marqueeSelect().map(d => {
- if (e.key !== "r")
+ let selected = this.marqueeSelect();
+ if (e.key === "c") {
+ selected.map(d => {
this.props.removeDocument(d);
- d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
- d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
- d.page = -1;
- return d;
- });
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
+ });
+ }
let ink = Cast(this.props.container.props.Document.ink, InkField);
let inkData = ink ? ink.inkData : undefined;
let zoomBasis = NumCast(this.props.container.props.Document.scale, 1);
@@ -170,51 +265,73 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
panX: 0,
panY: 0,
borderRounding: e.key === "e" ? -1 : undefined,
+ backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
scale: zoomBasis,
width: bounds.width * zoomBasis,
height: bounds.height * zoomBasis,
ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
- title: "a nested collection"
+ title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection",
});
-
+ newCollection.zoomBasis = zoomBasis;
this.marqueeInkDelete(inkData);
- // SelectionManager.DeselectAll();
- if (e.key === "r") {
- let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
- summary.maximizedDocs = new List<Doc>(selected);
- // summary.doc1 = selected[0];
- // if (selected.length > 1)
- // summary.doc2 = selected[1];
- // summary.templates = new List<string>([Templates.Summary.Layout]);
- this.props.addLiveTextDocument(summary);
- e.preventDefault();
- let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top);
- selected.map(maximizedDoc => {
- let maxx = NumCast(maximizedDoc.x, undefined);
- let maxy = NumCast(maximizedDoc.y, undefined);
- let maxw = NumCast(maximizedDoc.width, undefined);
- let maxh = NumCast(maximizedDoc.height, undefined);
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0])
+
+ if (e.key === "s") {
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
+ });
+ let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ newCollection.x = bounds.left + bounds.width;
+ summary.proto!.subBulletDocs = new List<Doc>(selected);
+ //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+ summary.templates = new List<string>([Templates.Bullet.Layout]);
+ let container = Docs.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" });
+ container.viewType = CollectionViewType.Stacking;
+ this.props.addLiveTextDocument(container);
+ // });
+ } else if (e.key === "S") {
+ await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 0.2 }).then((dataUrl) => {
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
+ });
+ let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ let imageSummary = Docs.ImageDocument(url, {
+ x: bounds.left, y: bounds.top + 100 / zoomBasis,
+ width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-"
+ });
+ summary.imageSummary = imageSummary;
+ this.props.addDocument(imageSummary, false);
+ }
+ })
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ newCollection.x = bounds.left + bounds.width;
+ //this.props.addDocument(newCollection, false);
+ summary.proto!.summarizedDocs = new List<Doc>(selected);
+ summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+
+ this.props.addLiveTextDocument(summary);
});
}
else {
this.props.addDocument(newCollection, false);
+ SelectionManager.DeselectAll();
+ this.props.selectDocuments([newCollection]);
}
this.cleanupInteractions(false);
}
- if (e.key === "s") {
- this._commandExecuted = true;
- e.stopPropagation();
- e.preventDefault();
- let bounds = this.Bounds;
- let selected = this.marqueeSelect();
- SelectionManager.DeselectAll();
- let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
- this.props.addLiveTextDocument(summary);
- selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!));
-
- this.cleanupInteractions(false);
- }
}
@action
marqueeInkSelect(ink: Map<any, any>) {
@@ -268,17 +385,21 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@computed
get marqueeDiv() {
- let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} >
+ return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
<span className="marquee-legend" />
</div>;
}
render() {
+ let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
- {this.props.children}
- {!this._visible ? (null) : this.marqueeDiv}
+ <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} >
+ {this._visible ? this.marqueeDiv : null}
+ <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
+ {this.props.children}
+ </div>
+ </div>
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
index 4f68b71b0..838d4d9ac 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -1,11 +1,13 @@
@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700");
// colors
$light-color: #fcfbf7;
-$light-color-secondary: rgb(241, 239, 235);
-$main-accent: #61aaa3;
+$light-color-secondary:#f1efeb;
+//$main-accent: #61aaa3;
+$main-accent: #aaaaa3;
// $alt-accent: #cdd5ec;
// $alt-accent: #cdeceb;
-$alt-accent: #59dff7;
+//$alt-accent: #59dff7;
+$alt-accent: #c2c2c5;
$lighter-alt-accent: rgb(207, 220, 240);
$intermediate-color: #9c9396;
$dark-color: #121721;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2ba0458f5..499b83c0f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,23 +1,24 @@
-import { computed, trace, action, reaction, IReactionDisposer } from "mobx";
+import { computed, IReactionDisposer, reaction, action } from "mobx";
import { observer } from "mobx-react";
+import { Doc } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { OmitKeys } from "../../../Utils";
import { Transform } from "../../util/Transform";
+import { DocComponent } from "../DocComponent";
import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
-import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types";
-import { OmitKeys, Utils } from "../../../Utils";
+import { UndoManager } from "../../util/UndoManager";
import { SelectionManager } from "../../util/SelectionManager";
-import { Doc } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
}
const schema = createSchema({
zoomBasis: "number",
- zIndex: "number"
+ zIndex: "number",
});
//TODO Types: The import order is wrong, so positionSchema is undefined
@@ -27,8 +28,6 @@ const FreeformDocument = makeInterface(schema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
private _mainCont = React.createRef<HTMLDivElement>();
- private _downX: number = 0;
- private _downY: number = 0;
_bringToFrontDisposer?: IReactionDisposer;
@computed get transform() {
@@ -40,9 +39,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
@computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
@computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
- @computed get width(): number { return FieldValue(this.Document.width, 0); }
- @computed get height(): number { return FieldValue(this.Document.height, 0); }
- @computed get zIndex(): number { return FieldValue(this.Document.zIndex, 0); }
+ @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); }
+ @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); }
set width(w: number) {
this.Document.width = w;
@@ -56,14 +54,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.Document.width = this.nativeWidth / this.nativeHeight * h;
}
}
- set zIndex(h: number) {
- this.Document.zIndex = h;
- }
-
contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
- toggleMinimized = () => this.toggleIcon();
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
.scale(1 / this.contentScaling()).scale(1 / this.zoom)
@@ -71,11 +64,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@computed
get docView() {
return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit}
- toggleMinimized={this.toggleMinimized}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
+ collapseToPoint={this.collapseToPoint}
/>;
}
@@ -84,99 +77,74 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.props.bringToFront(this.props.Document);
if (values instanceof List) {
let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]);
- this.animateBetweenIcon(true, scrpt, [values[2], values[3]], values[4], values[5], values[6], this.props.Document, values[7] ? true : false);
+ this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0],
+ this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false);
}
- });
+ }, { fireImmediately: true });
}
componentWillUnmount() {
if (this._bringToFrontDisposer) this._bringToFrontDisposer();
}
- animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) {
+ static _undoBatch?: UndoManager.Batch = undefined;
+ @action
+ public collapseToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ if (!CollectionFreeFormDocumentView._undoBatch) {
+ CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+ }
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
+ if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.willMaximize = isMinimized;
+ maximizedDoc.isMinimized = false;
+ maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
+ }
+ });
+ setTimeout(() => {
+ CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end();
+ CollectionFreeFormDocumentView._undoBatch = undefined;
+ }, 500);
+ }
+ }
+
+ animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) {
+
setTimeout(() => {
let now = Date.now();
let progress = Math.min(1, (now - stime) / 200);
let pval = maximizing ?
[icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] :
[targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress];
- target.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
- target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
- target.x = pval[0];
- target.y = pval[1];
+ this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
+ this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
+ this.props.Document.x = pval[0];
+ this.props.Document.y = pval[1];
+ if (first) {
+ this.props.Document.proto!.willMaximize = false;
+ }
if (now < stime + 200) {
- this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing);
+ this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing);
}
else {
if (!maximizing) {
- target.isMinimized = true;
- target.x = targ[0];
- target.y = targ[1];
- target.width = width;
- target.height = height;
+ this.props.Document.proto!.isMinimized = true;
+ this.props.Document.x = targ[0];
+ this.props.Document.y = targ[1];
+ this.props.Document.width = width;
+ this.props.Document.height = height;
}
- target.isIconAnimating = undefined;
+ this.props.Document.proto!.isIconAnimating = undefined;
}
},
2);
}
- @action
- public toggleIcon = async (): Promise<void> => {
- SelectionManager.DeselectAll();
- let isMinimized: boolean | undefined;
- let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc));
- let minimizedDoc: Doc | undefined = this.props.Document;
- if (!maximizedDocs) {
- minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
- if (minimizedDoc) maximizedDocs = await Cast(minimizedDoc.maximizedDocs, listSpec(Doc));
- }
- if (minimizedDoc && maximizedDocs && maximizedDocs instanceof List) {
- let minimizedTarget = minimizedDoc;
- maximizedDocs.map(maximizedDoc => {
- let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
- if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
- if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized, false);
- }
- let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) / 2;
- let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) / 2;
- let maxx = NumCast(maximizedDoc.x, undefined);
- let maxy = NumCast(maximizedDoc.y, undefined);
- let maxw = NumCast(maximizedDoc.width, undefined);
- let maxh = NumCast(maximizedDoc.height, undefined);
- if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined &&
- maxw !== undefined && maxh !== undefined) {
- let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
- maximizedDoc.isMinimized = false;
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
- }
- }
- });
- }
- }
- onPointerDown = (e: React.PointerEvent): void => {
- this._downX = e.clientX;
- this._downY = e.clientY;
- e.stopPropagation();
- }
- onClick = async (e: React.MouseEvent) => {
- e.stopPropagation();
- let ctrlKey = e.ctrlKey;
- if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
- Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- if (await BoolCast(this.props.Document.isButton, false)) {
- let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc));
- if (maximizedDocs) { // bcz: need a better way to associate behaviors with click events on widget-documents
- if (ctrlKey)
- this.props.addDocument && maximizedDocs.filter(d => d instanceof Doc).map(maxDoc => this.props.addDocument!(maxDoc, false));
- this.toggleIcon();
- }
- }
- }
- }
-
- onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }
- onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }
borderRounding = () => {
let br = NumCast(this.props.Document.borderRounding);
@@ -190,25 +158,19 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc)));
let zoomFade = 1;
//var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1);
- let transform = this.getTransform().scale(this.contentScaling()).inverse();
- var [sptX, sptY] = transform.transformPoint(0, 0);
- let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight());
- let w = bptX - sptX;
+ // let transform = this.getTransform().scale(this.contentScaling()).inverse();
+ // var [sptX, sptY] = transform.transformPoint(0, 0);
+ // let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight());
+ // let w = bptX - sptX;
//zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1;
const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800);
let fadeUp = .75 * screenWidth;
let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth;
- zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1;
+ // zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1;
return (
<div className="collectionFreeFormDocumentView-container" ref={this._mainCont}
- onPointerDown={this.onPointerDown}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
- onClick={this.onClick}
style={{
- outlineColor: "black",
- outlineStyle: "dashed",
- outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ? `${0.5 / this.contentScaling()}px` : "0px",
opacity: zoomFade,
borderRadius: `${this.borderRounding()}px`,
transformOrigin: "left top",
@@ -217,7 +179,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
width: this.width,
height: this.height,
position: "absolute",
- zIndex: this.zIndex,
+ zIndex: this.Document.zIndex || 0,
backgroundColor: "transparent"
}} >
{this.docView}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index bbc927b5a..02396c3af 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,4 @@
-import { computed } from "mobx";
+import { computed, trace } from "mobx";
import { observer } from "mobx-react";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
@@ -21,7 +21,7 @@ import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import React = require("react");
import { FieldViewProps } from "./FieldView";
import { Without, OmitKeys } from "../../../Utils";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -43,9 +43,21 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: () => boolean,
select: (ctrl: boolean) => void,
- layoutKey: string
+ layoutKey: string,
+ hideOnLeave?: boolean
}> {
- @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "<p>Error loading layout data</p>"); }
+ @computed get layout(): string {
+ const layout = Cast(this.props.Document[this.props.layoutKey], "string");
+ if (layout === undefined) {
+ return this.props.Document.data ?
+ "<FieldView {...props} fieldKey='data' />" :
+ KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : "");
+ } else if (typeof layout === "string") {
+ return layout;
+ } else {
+ return "<p>Loading layout</p>";
+ }
+ }
CreateBindings(): JsxBindings {
return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
@@ -58,20 +70,38 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
return new List<string>();
}
- set templates(templates: List<string>) { this.props.Document.templates = templates; }
- get finalLayout() {
- const baseLayout = this.layout;
+ @computed get finalLayout() {
+ const baseLayout = this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
let base = baseLayout;
let layout = baseLayout;
- this.templates.forEach(template => {
- layout = template.replace("{layout}", base);
- base = layout;
- });
+ // bcz: templates are intended only for a document's primary layout or overlay (not background). However,
+ // a DocumentContentsView is used to render annotation overlays, so we detect that here
+ // by checking the layoutKey. This should probably be moved into
+ // a prop so that the overlay can explicitly turn off templates.
+ if ((this.props.layoutKey === "overlayLayout" && StrCast(this.props.Document.layout).indexOf("CollectionView") !== -1) ||
+ (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) {
+ this.templates.forEach(template => {
+ let self = this;
+ // this scales constants in the markup by the scaling applied to the document, but caps the constants to be smaller
+ // than the width/height of the containing document
+ function convertConstantsToNative(match: string, offset: number, x: string) {
+ let px = Number(match.replace("px", ""));
+ return `${Math.min(NumCast(self.props.Document.height, 0),
+ Math.min(NumCast(self.props.Document.width, 0),
+ px * self.props.ScreenToLocalTransform().Scale))}px`;
+ }
+ // let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative);
+ // layout = nativizedTemplate.replace("{layout}", base);
+ layout = template.replace("{layout}", base);
+ base = layout;
+ });
+ }
return layout;
}
render() {
+ if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
bindings={this.CreateBindings()}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fd012e7ea..7750b9173 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,9 +1,19 @@
-import { action, computed, runInAction } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash, faConciergeBell, faFolder, faMapPin, faLink, faFingerprint, faCrosshairs, faDesktop } from '@fortawesome/free-solid-svg-icons';
+import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, StrCast, NumCast } from "../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { emptyFunction, Utils } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
+import { SearchUtil } from "../../util/SearchUtil";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
@@ -12,20 +22,30 @@ import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
-import { Template, Templates } from "./../Templates";
+import { DocComponent } from "../DocComponent";
+import { PresentationView } from "../PresentationView";
+import { Template } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import React = require("react");
-import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { FieldValue, StrCast, BoolCast } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { DocServer } from "../../DocServer";
-import { Id } from "../../../new_fields/RefField";
-import { PresentationView } from "../PresentationView";
+import { Id, Copy } from '../../../new_fields/FieldSymbols';
+import { ContextMenuProps } from '../ContextMenuItem';
+const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
+
+library.add(faTrash);
+library.add(faExpandArrowsAlt);
+library.add(faCompressArrowsAlt);
+library.add(faLayerGroup);
+library.add(faAlignCenter);
+library.add(faCaretSquareRight);
+library.add(faSquare);
+library.add(faConciergeBell);
+library.add(faFolder);
+library.add(faMapPin);
+library.add(faLink);
+library.add(faFingerprint);
+library.add(faCrosshairs);
+library.add(faDesktop);
const linkSchema = createSchema({
title: "string",
@@ -41,27 +61,28 @@ const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
- addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean;
- removeDocument?: (doc: Document) => boolean;
- moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument?: (doc: Doc) => boolean;
+ moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
isTopMost: boolean;
ContentScaling: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
- focus: (doc: Document) => void;
+ focus: (doc: Doc) => void;
selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- toggleMinimized: () => void;
bringToFront: (doc: Doc) => void;
+ addDocTab: (doc: Doc, where: string) => void;
+ collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
}
const schema = createSchema({
layout: "string",
nativeWidth: "number",
nativeHeight: "number",
- backgroundColor: "string"
+ backgroundColor: "string",
});
export const positionSchema = createSchema({
@@ -83,6 +104,9 @@ const Document = makeInterface(schema);
export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
private _downX: number = 0;
private _downY: number = 0;
+ private _lastTap: number = 0;
+ private _doubleTap = false;
+ private _hitExpander = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -99,6 +123,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
set templates(templates: List<string>) { this.props.Document.templates = templates; }
screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ _reactionDisposer?: IReactionDisposer;
@action
componentDidMount() {
if (this._mainCont.current) {
@@ -106,6 +131,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
handlers: { drop: this.drop.bind(this) }
});
}
+ // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes
+ this._reactionDisposer = reaction(() => [DocListCast(this.props.Document.maximizedDocs).map(md => md.title),
+ this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
+ () => {
+ let maxDoc = DocListCast(this.props.Document.maximizedDocs);
+ if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) {
+ this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon";
+ }
+ let sumDoc = Cast(this.props.Document.summaryDoc, Doc);
+ if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) {
+ this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded";
+ }
+ }, { fireImmediately: true });
DocumentManager.Instance.DocumentViews.push(this);
}
@action
@@ -121,9 +159,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@action
componentWillUnmount() {
- if (this._dropDisposer) {
- this._dropDisposer();
- }
+ if (this._reactionDisposer) this._reactionDisposer();
+ if (this._dropDisposer) this._dropDisposer();
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
@@ -131,10 +168,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
}
- startDragging(x: number, y: number, dropAction: dropActionType) {
+ startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
if (this._mainCont.current) {
+ let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
- let dragData = new DragManager.DocumentDragData([this.props.Document]);
+ let dragData = new DragManager.DocumentDragData(allConnected);
const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
dragData.xOffset = xoff;
@@ -148,28 +186,95 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
}
+ toggleMinimized = async () => {
+ let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
+ if (minimizedDoc) {
+ let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(
+ NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y));
+ this.props.collapseToPoint && this.props.collapseToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
+ }
+ }
- onClick = (e: React.MouseEvent): void => {
- if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
+ onClick = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ let altKey = e.altKey;
+ let ctrlKey = e.ctrlKey;
+ if (this._doubleTap && !this.props.isTopMost) {
+ this.props.addDocTab(this.props.Document, "inTab");
+ SelectionManager.DeselectAll();
+ this.props.Document.libraryBrush = false;
+ }
+ else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
SelectionManager.SelectDoc(this, e.ctrlKey);
+ let isExpander = (e.target as any).id === "isExpander";
+ if (BoolCast(this.props.Document.isButton, false) || isExpander) {
+ SelectionManager.DeselectAll();
+ let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
+ let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
+ let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
+ let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
+ let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
+ let expandedDocs: Doc[] = [];
+ expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
+ // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
+ if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
+ let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc));
+ let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
+ let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
+ if (altKey) {
+ maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace");
+ if (!maxLocation || maxLocation === "inPlace") {
+ let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
+ let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false);
+ expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false);
+ let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
+ if (!hasView) {
+ this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false));
+ }
+ expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
+ }
+ }
+ if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) {
+ let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
+ if (dataDocs) {
+ expandedDocs.forEach(maxDoc =>
+ (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
+ this.props.addDocTab(getDispDoc(maxDoc), maxLocation)));
+ }
+ } else {
+ let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
+ this.props.collapseToPoint && this.props.collapseToPoint(scrpt, expandedProtoDocs);
+ }
+ }
+ else if (linkedToDocs.length || linkedFromDocs.length) {
+ let linkedFwdDocs = [
+ linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
+ linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
+
+ let linkedFwdPage = [
+ linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined,
+ linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined];
+ if (!linkedFwdDocs.some(l => l instanceof Promise)) {
+ let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
+ DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0]);
+ }
+ }
+ }
}
}
onPointerDown = (e: React.PointerEvent): void => {
this._downX = e.clientX;
this._downY = e.clientY;
- if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) {
- return;
- }
- if (e.shiftKey && e.buttons === 1) {
- if (this.props.isTopMost) {
- this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined);
- } else {
- CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e);
- }
+ this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
+ if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
+ CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
e.stopPropagation();
- } else if (this.active) {
+ } else {
+ if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -177,12 +282,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble) {
+ if (!e.cancelBubble && this.active) {
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) {
- this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined);
+ if (!e.altKey && !this.topMost && e.buttons === 1) {
+ this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander);
}
}
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -192,30 +297,29 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
+ this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
+ this._lastTap = Date.now();
}
- deleteClicked = (): void => {
- this.props.removeDocument && this.props.removeDocument(this.props.Document);
- }
- fieldsClicked = (e: React.MouseEvent): void => {
- let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 });
- CollectionDockingView.Instance.AddRightSplit(kvp);
- }
- makeButton = (e: React.MouseEvent): void => {
- this.props.Document.isButton = !BoolCast(this.props.Document.isButton, false);
- if (this.props.Document.isButton && !this.props.Document.nativeWidth) {
- this.props.Document.nativeWidth = this.props.Document[WidthSym]();
- this.props.Document.nativeHeight = this.props.Document[HeightSym]();
+ deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }
+ fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
+ makeBtnClicked = (): void => {
+ let doc = Doc.GetProto(this.props.Document);
+ doc.isButton = !BoolCast(doc.isButton, false);
+ if (StrCast(doc.layout).indexOf("Formatted") !== -1) { // only need to freeze the dimensions of text boxes since they don't have a native width and height naturally
+ if (doc.isButton && !doc.nativeWidth) {
+ doc.nativeWidth = this.props.Document[WidthSym]();
+ doc.nativeHeight = this.props.Document[HeightSym]();
+ } else {
+ doc.nativeWidth = doc.nativeHeight = undefined;
+ }
}
}
- fullScreenClicked = (e: React.MouseEvent): void => {
- const doc = Doc.MakeDelegate(FieldValue(this.Document.proto));
- if (doc) {
- CollectionDockingView.Instance.OpenFullScreen(doc);
- }
- ContextMenu.Instance.clearItems();
+ fullScreenClicked = (): void => {
+ CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false));
SelectionManager.DeselectAll();
}
+
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
@@ -223,9 +327,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.Document;
- const protoDest = destDoc.proto;
- const protoSrc = sourceDoc.proto;
- Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
+ if (de.mods === "AltKey") {
+ const protoDest = destDoc.proto;
+ const protoSrc = sourceDoc.proto;
+ let src = protoSrc ? protoSrc : sourceDoc;
+ let dst = protoDest ? protoDest : destDoc;
+ dst.data = (src.data! as ObjectField)[Copy]();
+ dst.nativeWidth = src.nativeWidth;
+ dst.nativeHeight = src.nativeHeight;
+ }
+ else {
+ DocUtils.MakeLink(sourceDoc, destDoc);
+ de.data.droppedDocuments.push(destDoc);
+ }
e.stopPropagation();
}
}
@@ -242,7 +356,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
-
@action
addTemplate = (template: Template) => {
this.templates.push(template.Layout);
@@ -260,6 +373,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.templates = this.templates;
}
+ freezeNativeDimensions = (e: React.MouseEvent): void => {
+ if (NumCast(this.props.Document.nativeWidth)) {
+ let proto = Doc.GetProto(this.props.Document);
+ let nw = proto.nativeWidth;
+ let nh = proto.nativeHeight;
+ proto.nativeWidth = proto.nativeHeight = undefined;
+ this.props.Document.width = this.props.Document.frozenWidth;
+ this.props.Document.height = this.props.Document.frozenHeight;
+ }
+ else {
+ let scale = this.props.ScreenToLocalTransform().Scale * NumCast(this.props.Document.zoomBasis, 1);
+ let scr = this.screenRect();
+ let proto = Doc.GetProto(this.props.Document);
+ this.props.Document.frozenWidth = this.props.Document.width;
+ this.props.Document.frozenHeight = this.props.Document.height;
+ this.props.Document.height = proto.nativeHeight = scr.height * scale;
+ this.props.Document.width = proto.nativeWidth = scr.width * scale;
+ }
+ }
+
@action
onContextMenu = (e: React.MouseEvent): void => {
e.stopPropagation();
@@ -270,16 +403,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
e.preventDefault();
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked });
- ContextMenu.Instance.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton });
- ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked });
- ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) });
- ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])) });
- ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]) });
- //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) })
- ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document) });
- ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked });
+ const cm = ContextMenu.Instance;
+ let subitems: ContextMenuProps[] = [];
+ subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" });
+ subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
+ cm.addItem({ description: "Open...", subitems: subitems });
+ cm.addItem({ description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze", event: this.freezeNativeDimensions, icon: "edit" });
+ cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
+ cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });
+ cm.addItem({
+ description: "Find aliases", event: async () => {
+ const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
+ this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight");
+ }, icon: "search"
+ });
+ cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
+ cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+ cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
+ cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
if (!this.topMost) {
// DocumentViews should stop propagation of this event
e.stopPropagation();
@@ -290,6 +435,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
isSelected = () => SelectionManager.IsSelected(this);
select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed);
@@ -300,13 +447,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
render() {
var scaling = this.props.ContentScaling();
- var nativeHeight = this.nativeHeight > 0 ? this.nativeHeight.toString() + "px" : "100%";
- var nativeWidth = this.nativeWidth > 0 ? this.nativeWidth.toString() + "px" : "100%";
+ var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+ var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
return (
<div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ||
+ BoolCast(this.props.Document.protoBrush, false) ?
+ `${1 * this.props.ScreenToLocalTransform().Scale}px`
+ : "0px",
borderRadius: "inherit",
background: this.Document.backgroundColor || "",
width: nativeWidth,
@@ -314,6 +467,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
transform: `scale(${scaling}, ${scaling})`
}}
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
+
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
>
{this.contents}
</div>
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index a1e083b36..7b642b299 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,13 +1,13 @@
import React = require("react");
import { observer } from "mobx-react";
-import { computed } from "mobx";
+import { computed, observable } from "mobx";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
import { VideoBox } from "./VideoBox";
import { AudioBox } from "./AudioBox";
import { DocumentContentsView } from "./DocumentContentsView";
import { Transform } from "../../util/Transform";
-import { returnFalse, emptyFunction } from "../../../Utils";
+import { returnFalse, emptyFunction, returnOne } from "../../../Utils";
import { CollectionView } from "../collections/CollectionView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
@@ -17,6 +17,8 @@ import { List } from "../../../new_fields/List";
import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField";
import { IconField } from "../../../new_fields/IconField";
import { RichTextField } from "../../../new_fields/RichTextField";
+import { DateField } from "../../../new_fields/DateField";
+import { NumCast } from "../../../new_fields/Types";
//
@@ -33,6 +35,7 @@ export interface FieldViewProps {
isTopMost: boolean;
selectOnLoad: boolean;
addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocTab: (document: Doc, where: string) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -77,16 +80,20 @@ export class FieldView extends React.Component<FieldViewProps> {
}
else if (field instanceof AudioField) {
return <AudioBox {...this.props} />;
+ } else if (field instanceof DateField) {
+ return <p>{field.date.toLocaleString()}</p>;
}
else if (field instanceof Doc) {
+ let returnHundred = () => 100;
return (
<DocumentContentsView Document={field}
addDocument={undefined}
+ addDocTab={this.props.addDocTab}
removeDocument={undefined}
ScreenToLocalTransform={Transform.Identity}
- ContentScaling={() => 1}
- PanelWidth={() => 100}
- PanelHeight={() => 100}
+ ContentScaling={returnOne}
+ PanelWidth={returnHundred}
+ PanelHeight={returnHundred}
isTopMost={true} //TODO Why is this top most?
selectOnLoad={false}
focus={emptyFunction}
@@ -95,8 +102,8 @@ export class FieldView extends React.Component<FieldViewProps> {
layoutKey={"layout"}
ContainingCollectionView={this.props.ContainingCollectionView}
parentActive={this.props.active}
- toggleMinimized={emptyFunction}
- whenActiveChanged={this.props.whenActiveChanged} />
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction} />
);
}
else if (field instanceof List) {
@@ -109,7 +116,7 @@ export class FieldView extends React.Component<FieldViewProps> {
// return <WebBox {...this.props} />
// }
else if (!(field instanceof Promise)) {
- return <p>{JSON.stringify(field)}</p>;
+ return <p>{field.toString()}</p>;
}
else {
return <p> {"Waiting for server..."} </p>;
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index d43aa4e02..4a29c1949 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -11,14 +11,15 @@
}
.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {
- background: $light-color-secondary;
+ background: inherit;
padding: 0;
border-width: 0px;
border-radius: inherit;
border-color: $intermediate-color;
box-sizing: border-box;
+ background-color: inherit;
border-style: solid;
- overflow-y: scroll;
+ overflow-y: auto;
overflow-x: hidden;
color: initial;
height: 100%;
@@ -26,7 +27,6 @@
}
.formattedTextBox-cont-hidden {
- overflow: hidden;
pointer-events: none;
}
.formattedTextBox-inner-rounded {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index eeb60cb3d..5c635cc0c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,30 +1,37 @@
-import { action, IReactionDisposer, reaction, trace, computed, _allowStateChangesInsideComputed } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons';
+import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
+import { Doc, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { DocServer } from "../../DocServer";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorKeymap";
import { inpRules } from "../../util/RichTextRules";
-import { schema } from "../../util/RichTextSchema";
+import { ImageResizeView, schema } from "../../util/RichTextSchema";
+import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import { ContextMenu } from "../../views/ContextMenu";
-import { MainOverlayTextBox } from "../MainOverlayTextBox";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { DocComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
-import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { observer } from "mobx-react";
-import { InkingControl } from "../InkingControl";
-import { StrCast, Cast, NumCast, BoolCast } from "../../../new_fields/Types";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { Id } from "../../../new_fields/RefField";
-const { buildMenuItems } = require("prosemirror-example-setup");
-const { menuBar } = require("prosemirror-menu");
+import { DocUtils } from '../../documents/Documents';
+
+library.add(faEdit);
+library.add(faSmile);
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
@@ -43,8 +50,9 @@ const { menuBar } = require("prosemirror-menu");
// this will edit the document and assign the new value to that field.
//]
-export interface FormattedTextBoxOverlay {
+export interface FormattedTextBoxProps {
isOverlay?: boolean;
+ hideOnLeave?: boolean;
}
const richTextSchema = createSchema({
@@ -55,7 +63,7 @@ type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
@observer
-export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxOverlay), RichTextDocument>(RichTextDocument) {
+export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
@@ -63,15 +71,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
private _proseRef: React.RefObject<HTMLDivElement>;
private _editorView: Opt<EditorView>;
private _gotDown: boolean = false;
+ private _dropDisposer?: DragManager.DragDropDisposer;
private _reactionDisposer: Opt<IReactionDisposer>;
private _inputReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+
+ @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
+ public static InputBoxOverlayScroll: number = 0;
constructor(props: FieldViewProps) {
super(props);
this._ref = React.createRef();
this._proseRef = React.createRef();
+ if (this.props.isOverlay) {
+ DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
+ }
}
_applyingChange: boolean = false;
@@ -91,11 +107,29 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let titlestr = str.substr(0, Math.min(40, str.length));
let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
- };
+ }
+ }
+ }
+
+ @undoBatch
+ @action
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.props.Document;
+
+ DocUtils.MakeLink(sourceDoc, destDoc);
+ de.data.droppedDocuments.push(destDoc);
+ e.stopPropagation();
}
}
componentDidMount() {
+ if (this._ref.current) {
+ this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
+ }
const config = {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
@@ -118,7 +152,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
};
if (this.props.isOverlay) {
- this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc[Id],
+ this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay,
() => {
if (this._editorView) {
this._editorView.destroy();
@@ -128,7 +162,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
);
} else {
this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
- () => this.props.isSelected() && !BoolCast(this.props.Document.isButton, false) && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform));
+ () => {
+ if (this.props.isSelected()) {
+ FormattedTextBox.InputBoxOverlay = this;
+ FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
+ }
+ });
}
@@ -148,8 +187,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._proseRef.current) {
this._editorView = new EditorView(this._proseRef.current, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
- dispatchTransaction: this.dispatchTransaction
+ dispatchTransaction: this.dispatchTransaction,
+ nodeViews: {
+ image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }
+ }
});
+ let text = StrCast(this.props.Document.documentText);
+ if (text.startsWith("@@@")) {
+ this.props.Document.proto!.documentText = undefined;
+ this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", "")));
+ }
}
if (this.props.selectOnLoad) {
@@ -171,13 +218,35 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._proxyReactionDisposer) {
this._proxyReactionDisposer();
}
+ if (this._dropDisposer) {
+ this._dropDisposer();
+ }
}
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
- if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip)
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
this._toolTipTextMenu.tooltip.style.opacity = "0";
+ }
+ }
+ let ctrlKey = e.ctrlKey;
+ if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
+ let href = (e.target as any).href;
+ for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
+ href = parent.childNodes[0].href;
+ }
+ if (href) {
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ DocServer.GetRefField(docid).then(f => {
+ (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab"))
+ });
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
this._gotDown = true;
@@ -185,58 +254,24 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
onPointerUp = (e: React.PointerEvent): void => {
- if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip)
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
this._toolTipTextMenu.tooltip.style.opacity = "1";
+ }
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
+ @action
onFocused = (e: React.FocusEvent): void => {
if (!this.props.isOverlay) {
- MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform);
+ FormattedTextBox.InputBoxOverlay = this;
} else {
- if (this._proseRef.current) {
- this._proseRef.current.scrollTop = MainOverlayTextBox.Instance.TextScroll;
+ if (this._ref.current) {
+ this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
}
}
}
-
- //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- textCapability = (e: React.MouseEvent): void => {
- if (NumCast(this.props.Document.nativeWidth)) {
- this.props.Document.nativeWidth = undefined;
- this.props.Document.nativeHeight = undefined;
-
- } else {
- this.props.Document.nativeWidth = this.props.Document[WidthSym]();
- this.props.Document.nativeHeight = this.props.Document[HeightSym]();
- }
- }
- specificContextMenu = (e: React.MouseEvent): void => {
- if (!this._gotDown) {
- e.preventDefault();
- return;
- }
- ContextMenu.Instance.addItem({
- description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze",
- event: this.textCapability
- });
-
- // ContextMenu.Instance.addItem({
- // description: "Submenu",
- // items: [
- // {
- // description: "item 1", event:
- // },
- // {
- // description: "item 2", event:
- // }
- // ]
- // })
- // e.stopPropagation()
- }
-
onPointerWheel = (e: React.WheelEvent): void => {
if (this.props.isSelected()) {
e.stopPropagation();
@@ -271,7 +306,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
});
}
-
+ onBlur = (e: any) => {
+ if (this._undoTyping) {
+ this._undoTyping.end();
+ this._undoTyping = undefined;
+ }
+ }
+ public _undoTyping?: UndoManager.Batch;
onKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
SelectionManager.DeselectAll();
@@ -287,30 +328,48 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
+ if (!this._undoTyping) {
+ this._undoTyping = UndoManager.StartBatch("undoTyping");
+ }
+ }
+
+ @observable
+ _entered = false;
+ @action
+ onPointerEnter = (e: React.PointerEvent) => {
+ this._entered = true;
+ }
+ @action
+ onPointerLeave = (e: React.PointerEvent) => {
+ this._entered = false;
}
render() {
let style = this.props.isOverlay ? "scroll" : "hidden";
let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : "";
- let color = StrCast(this.props.Document.backgroundColor);
let interactive = InkingControl.Instance.selectedTool ? "" : "interactive";
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
+ background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined,
+ opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1,
+ color: this.props.hideOnLeave ? "white" : "initial",
pointerEvents: interactive ? "all" : "none",
- background: color,
}}
- onKeyDown={this.onKeyPress}
+ // onKeyDown={this.onKeyPress}
onKeyPress={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
+ onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
onMouseDown={this.onMouseDown}
onContextMenu={this.specificContextMenu}
// tfs: do we need this event handler
onWheel={this.onPointerWheel}
+ onPointerEnter={this.onPointerEnter}
+ onPointerLeave={this.onPointerLeave}
>
- <div className={`formattedTextBox-inner${rounded}`} style={{ pointerEvents: this.props.Document.isButton ? "none" : "all" }} ref={this._proseRef} />
+ <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} ref={this._proseRef} />
</div>
);
}
diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss
index 85bbdeb59..488681027 100644
--- a/src/client/views/nodes/IconBox.scss
+++ b/src/client/views/nodes/IconBox.scss
@@ -1,20 +1,21 @@
@import "../globalCssVariables";
.iconBox-container {
- position: absolute;
+ position: inherit;
left:0;
top:0;
- height: 100%;
+ height: auto;
width: max-content;
// overflow: hidden;
pointer-events: all;
svg {
width: $MINIMIZED_ICON_SIZE !important;
- height: 100%;
+ height: $MINIMIZED_ICON_SIZE !important;
+ height: auto;
background: white;
}
.iconBox-label {
- position: inherit;
+ position: absolute;
width:max-content;
font-size: 14px;
margin-top: 3px;
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 3fab10df4..00021bc78 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -2,18 +2,16 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
+import { computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { SelectionManager } from "../../util/SelectionManager";
import { FieldView, FieldViewProps } from './FieldView';
import "./IconBox.scss";
import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
import { IconField } from "../../../new_fields/IconField";
import { ContextMenu } from "../ContextMenu";
import Measure from "react-measure";
import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss";
-import { listSpec } from "../../../new_fields/Schema";
library.add(faCaretUp);
@@ -42,24 +40,40 @@ export class IconBox extends React.Component<FieldViewProps> {
setLabelField = (e: React.MouseEvent): void => {
this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
}
+ setUseOwnTitleField = (e: React.MouseEvent): void => {
+ this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle);
+ }
specificContextMenu = (e: React.MouseEvent): void => {
ContextMenu.Instance.addItem({
- description: BoolCast(this.props.Document.hideLabel) ? "show label" : "hide label",
+ description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon",
event: this.setLabelField
});
+ let maxDocs = DocListCast(this.props.Document.maximizedDocs);
+ if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) {
+ ContextMenu.Instance.addItem({
+ description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label",
+ event: this.setUseOwnTitleField
+ });
+ }
}
@observable _panelWidth: number = 0;
@observable _panelHeight: number = 0;
render() {
let labelField = StrCast(this.props.Document.labelField);
let hideLabel = BoolCast(this.props.Document.hideLabel);
- let maxDoc = Cast(this.props.Document.maximizedDocs, listSpec(Doc), []);
- let label = !hideLabel && maxDoc && labelField ? (maxDoc.length === 1 ? maxDoc[0][labelField] : this.props.Document[labelField]) : "";
+ let maxDocs = DocListCast(this.props.Document.maximizedDocs);
+ let firstDoc = maxDocs.length ? maxDocs[0] : undefined;
+ let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle, false) ? firstDoc[labelField] : this.props.Document.title);
return (
<div className="iconBox-container" onContextMenu={this.specificContextMenu}>
{this.minimizedIcon}
- <Measure onResize={(r) => runInAction(() => { if (r.entry.width || BoolCast(this.props.Document.hideLabel)) this.props.Document.nativeWidth = this.props.Document.width = (r.entry.width + Number(MINIMIZED_ICON_SIZE)); })}>
+ <Measure offset onResize={(r) => runInAction(() => {
+ if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) {
+ this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE));
+ if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth;
+ }
+ })}>
{({ measureRef }) =>
<span ref={measureRef} className="iconBox-label">{label}</span>
}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 2316a050e..f1b73a676 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -25,7 +25,11 @@
}
.imageBox-cont img {
- height: 100%;
+ height: auto;
+ width:100%;
+}
+.imageBox-cont-interactive img {
+ height: auto;
width:100%;
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0e9e904a8..4c2b73b70 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,5 +1,4 @@
-
-import { action, observable } from 'mobx';
+import { action, observable, trace } from 'mobx';
import { observer } from "mobx-react";
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
@@ -13,11 +12,19 @@ import React = require("react");
import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
import { DocComponent } from '../DocComponent';
import { positionSchema } from './DocumentView';
-import { FieldValue, Cast, StrCast } from '../../../new_fields/Types';
+import { FieldValue, Cast, StrCast, PromiseValue, NumCast } from '../../../new_fields/Types';
import { ImageField } from '../../../new_fields/URLField';
import { List } from '../../../new_fields/List';
import { InkingControl } from '../InkingControl';
-import { Doc } from '../../../new_fields/Doc';
+import { Doc, WidthSym, HeightSym } from '../../../new_fields/Doc';
+import { faImage } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { ContextMenuItemProps, ContextMenuProps } from '../ContextMenuItem';
+var path = require('path');
+
+
+library.add(faImage);
+
export const pageSchema = createSchema({
curPage: "number"
@@ -38,15 +45,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@observable private _isOpen: boolean = false;
private dropDisposer?: DragManager.DragDropDisposer;
- @action
- onLoad = (target: any) => {
- var h = this._imgRef.current!.naturalHeight;
- var w = this._imgRef.current!.naturalWidth;
- if (this._photoIndex === 0) {
- this.Document.nativeHeight = FieldValue(this.Document.nativeWidth, 0) * h / w;
- this.Document.height = FieldValue(this.Document.width, 0) * h / w;
- }
- }
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -88,16 +86,19 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
onPointerDown = (e: React.PointerEvent): void => {
- if (Date.now() - this._lastTap < 300) {
- if (e.buttons === 1) {
- this._downX = e.clientX;
- this._downY = e.clientY;
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- }
- } else {
- this._lastTap = Date.now();
- }
+ if (e.shiftKey && e.ctrlKey)
+
+ e.stopPropagation();
+ // if (Date.now() - this._lastTap < 300) {
+ // if (e.buttons === 1) {
+ // this._downX = e.clientX;
+ // this._downY = e.clientY;
+ // document.removeEventListener("pointerup", this.onPointerUp);
+ // document.addEventListener("pointerup", this.onPointerUp);
+ // }
+ // } else {
+ // this._lastTap = Date.now();
+ // }
}
@action
onPointerUp = (e: PointerEvent): void => {
@@ -131,11 +132,20 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let field = Cast(this.Document[this.props.fieldKey], ImageField);
if (field) {
let url = field.url.href;
- ContextMenu.Instance.addItem({
- description: "Copy path", event: () => {
- Utils.CopyText(url);
- }
+ let subitems: ContextMenuProps[] = [];
+ subitems.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
+ subitems.push({
+ description: "Rotate", event: action(() => {
+ this.props.Document.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
+ let nw = this.props.Document.nativeWidth;
+ this.props.Document.nativeWidth = this.props.Document.nativeHeight;
+ this.props.Document.nativeHeight = nw;
+ let w = this.props.Document.width;
+ this.props.Document.width = this.props.Document.height;
+ this.props.Document.height = w;
+ }), icon: "expand-arrows-alt"
});
+ ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: subitems });
}
}
@@ -156,18 +166,66 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
);
}
+ choosePath(url: URL) {
+ const lower = url.href.toLowerCase();
+ if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1 || !(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
+ return url.href;
+ }
+ let ext = path.extname(url.href);
+ return url.href.replace(ext, this._curSuffix + ext);
+ }
+
+ @observable _smallRetryCount = 1;
+ @observable _mediumRetryCount = 1;
+ @observable _largeRetryCount = 1;
+ @action retryPath = () => {
+ if (this._curSuffix === "_s") this._smallRetryCount++;
+ if (this._curSuffix === "_m") this._mediumRetryCount++;
+ if (this._curSuffix === "_l") this._largeRetryCount++;
+ }
+ @action onError = () => {
+ let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
+ if (timeout < 10)
+ setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ }
+ _curSuffix = "_m";
render() {
+ // let transform = this.props.ScreenToLocalTransform().inverse();
+ let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
+ // var [sptX, sptY] = transform.transformPoint(0, 0);
+ // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
+ // let w = bptX - sptX;
+
+ let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
+ let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
+ let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"];
+ // this._curSuffix = "";
+ // if (w > 20) {
let field = this.Document[this.props.fieldKey];
- let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"];
- if (field instanceof ImageField) paths = [field.url.href];
- else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href);
- let nativeWidth = FieldValue(this.Document.nativeWidth, 1);
+ // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
+ // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
+ // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
+ if (field instanceof ImageField) paths = [this.choosePath(field.url)];
+ else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => this.choosePath((p as ImageField).url));
+ // }
let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
+ let rotation = NumCast(this.props.Document.rotation, 0);
+ let aspect = (rotation % 180) ? this.props.Document[HeightSym]() / this.props.Document[WidthSym]() : 1;
+ let shift = (rotation % 180) ? (this.props.Document[HeightSym]() - this.props.Document[WidthSym]() / aspect) / 2 : 0;
return (
- <div className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
- <img src={paths[Math.min(paths.length, this._photoIndex)]} style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} />
+ <div id={id} className={`imageBox-cont${interactive}`}
+ onPointerDown={this.onPointerDown}
+ onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ <img id={id}
+ key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
+ src={paths[Math.min(paths.length, this._photoIndex)]}
+ style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
+ // style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }}
+ width={nativeWidth}
+ ref={this._imgRef}
+ onError={this.onError} />
{paths.length > 1 ? this.dots(paths) : (null)}
- {this.lightbox(paths)}
+ {/* {this.lightbox(paths)} */}
</div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 876a3c173..849f17aa4 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -8,7 +8,7 @@ import "./KeyValueBox.scss";
import { KeyValuePair } from "./KeyValuePair";
import React = require("react");
import { NumCast, Cast, FieldValue } from "../../../new_fields/Types";
-import { Doc, IsField } from "../../../new_fields/Doc";
+import { Doc, Field } from "../../../new_fields/Doc";
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
@@ -18,7 +18,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@observable private _keyInput: string = "";
@observable private _valueInput: string = "";
@computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
-
+ get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; }
constructor(props: FieldViewProps) {
super(props);
@@ -28,7 +28,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
onEnterKey = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
if (this._keyInput && this._valueInput) {
- let doc = FieldValue(Cast(this.props.Document.data, Doc));
+ let doc = this.fieldDocToLayout;
if (!doc) {
return;
}
@@ -41,7 +41,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let res = script.run();
if (!res.success) return;
const field = res.result;
- if (IsField(field)) {
+ if (Field.IsField(field)) {
realDoc[this._keyInput] = field;
}
this._keyInput = "";
@@ -60,7 +60,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
createTable = () => {
- let doc = FieldValue(Cast(this.props.Document.data, Doc));
+ let doc = this.fieldDocToLayout;
if (!doc) {
return <tr><td>Loading...</td></tr>;
}
@@ -78,7 +78,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let rows: JSX.Element[] = [];
let i = 0;
- for (let key in ids) {
+ for (let key of Object.keys(ids).sort()) {
rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index ff6885965..a1c5d5537 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -26,4 +26,12 @@
.keyValuePair-td-value {
display:inline-block;
overflow: scroll;
+ img {
+ max-height: 36px;
+ width: auto;
+ }
+ .videoBox-cont{
+ width: auto;
+ max-height: 36px;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 235792cbe..228d07018 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,7 +1,7 @@
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { emptyFunction, returnFalse, returnZero } from '../../../Utils';
+import { emptyFunction, returnFalse, returnZero, returnTrue } from '../../../Utils';
import { CompileScript } from "../../util/Scripting";
import { Transform } from '../../util/Transform';
import { EditableView } from "../EditableView";
@@ -9,7 +9,7 @@ import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import "./KeyValuePair.scss";
import React = require("react");
-import { Doc, Opt, IsField } from '../../../new_fields/Doc';
+import { Doc, Opt, Field } from '../../../new_fields/Doc';
import { FieldValue } from '../../../new_fields/Types';
// Represents one row in a key value plane
@@ -38,28 +38,31 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
focus: emptyFunction,
PanelWidth: returnZero,
PanelHeight: returnZero,
+ addDocTab: emptyFunction
};
let contents = <FieldView {...props} />;
+ let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
return (
<tr className={this.props.rowStyle}>
<td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
<div className="keyValuePair-td-key-container">
<button className="keyValuePair-td-key-delete" onClick={() => {
- let field = FieldValue(props.Document[props.fieldKey]);
- field && (props.Document[props.fieldKey] = undefined);
+ if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
+ props.Document[props.fieldKey] = undefined;
+ }
+ else props.Document.proto![props.fieldKey] = undefined;
}}>
X
</button>
- <div className="keyValuePair-keyField">{this.props.keyName}</div>
+ <div className="keyValuePair-keyField">{fieldKey}</div>
</div>
</td>
<td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}>
<EditableView contents={contents} height={36} GetValue={() => {
+
let field = FieldValue(props.Document[props.fieldKey]);
- if (field) {
- //TODO Types
- return String(field);
- // return field.ToScriptString();
+ if (Field.IsField(field)) {
+ return Field.toScriptString(field);
}
return "";
}}
@@ -71,7 +74,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
let res = script.run();
if (!res.success) return false;
const field = res.result;
- if (IsField(field)) {
+ if (Field.IsField(field, true)) {
props.Document[props.fieldKey] = field;
return true;
}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 08cfa590b..68b692aad 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -31,28 +31,7 @@ export class LinkBox extends React.Component<Props> {
@undoBatch
onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc);
- if (docView) {
- docView.props.focus(docView.props.Document);
- } else {
- const contextDoc = await Cast(this.props.pairedDoc.annotationOn, Doc);
- if (!contextDoc) {
- CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(this.props.pairedDoc));
- } else {
- const page = NumCast(this.props.pairedDoc.page, undefined);
- const curPage = NumCast(contextDoc.curPage, undefined);
- if (page !== curPage) {
- contextDoc.curPage = page;
- }
- let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
- if (contextView) {
- contextDoc.panTransformType = "Ease";
- contextView.props.focus(contextDoc);
- } else {
- CollectionDockingView.Instance.AddRightSplit(contextDoc);
- }
- }
- }
+ DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
}
onEditButtonPressed = (e: React.PointerEvent): void => {
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index f82c6e9cb..71a423338 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -24,8 +24,9 @@ export class LinkEditor extends React.Component<Props> {
onSaveButtonPressed = (e: React.PointerEvent): void => {
e.stopPropagation();
- this.props.linkDoc.title = this._nameInput;
- this.props.linkDoc.linkDescription = this._descriptionInput;
+ let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+ linkDoc.title = this._nameInput;
+ linkDoc.linkDescription = this._descriptionInput;
this.props.showLinks();
}
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index e21adebbc..3f09d6214 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -5,10 +5,9 @@ import { LinkBox } from "./LinkBox";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
-import { Doc } from "../../../new_fields/Doc";
-import { Cast, FieldValue } from "../../../new_fields/Types";
-import { listSpec } from "../../../new_fields/Schema";
-import { Id } from "../../../new_fields/RefField";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
interface Props {
docView: DocumentView;
@@ -24,19 +23,19 @@ export class LinkMenu extends React.Component<Props> {
return links.map(link => {
let doc = FieldValue(Cast(link[key], Doc));
if (doc) {
- return <LinkBox key={doc[Id]} linkDoc={link} linkName={Cast(link.title, "string", "")} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
+ return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
}
});
}
render() {
//get list of links from document
- let linkFrom: Doc[] = Cast(this.props.docView.props.Document.linkedFromDocs, listSpec(Doc), []);
- let linkTo: Doc[] = Cast(this.props.docView.props.Document.linkedToDocs, listSpec(Doc), []);
+ let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs);
+ let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs);
if (this._editingLink === undefined) {
return (
<div id="linkMenu-container">
- <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input>
+ {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
<div id="linkMenu-list">
{this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
{this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 3760e378a..449408a61 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -17,6 +17,10 @@
z-index: 25;
pointer-events: all;
}
+.pdfBox-thumbnail {
+ position: absolute;
+ width: 100%;
+}
.pdfButton {
pointer-events: all;
width: 100px;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 14cbed04c..aa29a7170 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,26 +1,29 @@
import * as htmlToImage from "html-to-image";
-import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css';
import Measure from "react-measure";
//@ts-ignore
import { Document, Page } from "react-pdf";
import 'react-pdf/dist/Page/AnnotationLayer.css';
+import { Id } from "../../../new_fields/FieldSymbols";
+import { makeInterface } from "../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { ImageField, PdfField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from '../../../Utils';
+import { DocServer } from "../../DocServer";
+import { DocComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
+import { SearchBox } from "../SearchBox";
import { Annotation } from './Annotation';
+import { positionSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
+import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
+var path = require('path');
import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { Opt } from "../../../new_fields/Doc";
-import { DocComponent } from "../DocComponent";
-import { makeInterface } from "../../../new_fields/Schema";
-import { positionSchema } from "./DocumentView";
-import { pageSchema } from "./ImageBox";
-import { ImageField, PdfField } from "../../../new_fields/URLField";
-import { InkingControl } from "../InkingControl";
+import { ContextMenu } from "../ContextMenu";
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
* This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
@@ -53,10 +56,12 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
public static LayoutString() { return FieldView.LayoutString(PDFBox); }
private _mainDiv = React.createRef<HTMLDivElement>();
+ private renderHeight = 2400;
@observable private _renderAsSvg = true;
+ @observable private _alt = false;
- private _reactionDisposer: Opt<IReactionDisposer>;
+ private _reactionDisposer?: IReactionDisposer;
@observable private _perPageInfo: Object[] = []; //stores pageInfo
@observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno
@@ -65,27 +70,25 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@observable private _interactive: boolean = false;
@observable private _loaded: boolean = false;
- @computed private get curPage() { return FieldValue(this.Document.curPage, 1); }
- @computed private get thumbnailPage() { return Cast(this.props.Document.thumbnailPage, "number", -1); }
+ @computed private get curPage() { return NumCast(this.Document.curPage, 1); }
+ @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); }
componentDidMount() {
+ let wasSelected = this.props.isSelected();
this._reactionDisposer = reaction(
- () => [SelectionManager.SelectedDocuments().slice()],
+ () => [this.props.isSelected(), this.curPage],
() => {
- if (this.curPage > 0 && this.thumbnailPage > 0 && this.curPage !== this.thumbnailPage && !this.props.isSelected()) {
+ if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) {
this.saveThumbnail();
- this._interactive = true;
}
+ wasSelected = this._interactive = this.props.isSelected();
},
{ fireImmediately: true });
}
componentWillUnmount() {
- if (this._reactionDisposer) {
- this._reactionDisposer();
- this._reactionDisposer = undefined;
- }
+ if (this._reactionDisposer) this._reactionDisposer();
}
/**
@@ -163,10 +166,8 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
let index: any;
this._pageInfo.divs.forEach((obj: any) => {
obj.spans.forEach((element: any) => {
- if (element === span) {
- if (!index) {
- index = this._pageInfo.divs.indexOf(obj);
- }
+ if (element === span && !index) {
+ index = this._pageInfo.divs.indexOf(obj);
}
});
});
@@ -216,14 +217,15 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
if (e.altKey) {
this._alt = true;
} else {
- if (e.metaKey)
+ if (e.metaKey) {
e.stopPropagation();
+ }
}
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
}
if (this.props.isSelected() && e.buttons === 2) {
- this._alt = true;
+ runInAction(() => this._alt = true);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
}
@@ -243,23 +245,28 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
}
-
@action
saveThumbnail = () => {
+ this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
this._renderAsSvg = false;
setTimeout(() => {
+ runInAction(() => this._smallRetryCount = this._mediumRetryCount = this._largeRetryCount = 0);
let nwidth = FieldValue(this.Document.nativeWidth, 0);
let nheight = FieldValue(this.Document.nativeHeight, 0);
- htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 })
+ htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 })
.then(action((dataUrl: string) => {
- this.props.Document.thumbnail = new ImageField(new URL(dataUrl));
- this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
- this._renderAsSvg = true;
+ SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ this.props.Document.thumbnail = new ImageField(new URL(url));
+ }
+ runInAction(() => this._renderAsSvg = true);
+ })
}))
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
});
- }, 250);
+ }, 1250);
}
@action
@@ -279,33 +286,34 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
// so this design is flawed.
var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
if (!FieldValue(this.Document.nativeHeight, 0)) {
- var nativeHeight = nativeWidth * r.entry.height / r.entry.width;
+ var nativeHeight = nativeWidth * r.offset.height / r.offset.width;
this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
this.props.Document.nativeHeight = nativeHeight;
}
}
- renderHeight = 2400;
@computed
get pdfPage() {
- return <Page height={this.renderHeight} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />
+ return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />;
}
@computed
get pdfContent() {
- let page = this.curPage;
- const renderHeight = 2400;
let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- let xf = FieldValue(this.Document.nativeHeight, 0) / renderHeight;
- let body = NumCast(this.props.Document.nativeHeight) ?
- this.pdfPage :
- <Measure onResize={this.setScaling}>
+ if (!pdfUrl) {
+ return <p>No pdf url to render</p>;
+ }
+ let pdfpage = this.pdfPage;
+ let body = this.Document.nativeHeight ?
+ pdfpage :
+ <Measure offset onResize={this.setScaling}>
{({ measureRef }) =>
<div className="pdfBox-page" ref={measureRef}>
- {this.pdfPage}
+ {pdfpage}
</div>
}
</Measure>;
+ let xf = (this.Document.nativeHeight || 0) / this.renderHeight;
return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}>
- <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}>
+ <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg || this.props.isTopMost ? "svg" : "canvas"}>
{body}
</Document>
</div >;
@@ -313,47 +321,81 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@computed
get pdfRenderer() {
- let proxy = this._loaded ? (null) : this.imageProxyRenderer;
let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- if ((!this._interactive && proxy) || !pdfUrl) {
+ let proxy = this.imageProxyRenderer;
+ if ((!this._interactive && proxy && (!this.props.ContainingCollectionView || !this.props.ContainingCollectionView.props.isTopMost)) || !pdfUrl) {
return proxy;
}
return [
+ proxy,
this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
this._currAnno.map((element: any) => element),
- this.pdfContent,
- proxy
+ this.pdfContent
];
}
+ choosePath(url: URL) {
+ if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1)
+ return url.href;
+ let ext = path.extname(url.href);
+ return url.href.replace(ext, this._curSuffix + ext);
+ }
+ @observable _smallRetryCount = 1;
+ @observable _mediumRetryCount = 1;
+ @observable _largeRetryCount = 1;
+ @action retryPath = () => {
+ if (this._curSuffix === "_s") this._smallRetryCount++;
+ if (this._curSuffix === "_m") this._mediumRetryCount++;
+ if (this._curSuffix === "_l") this._largeRetryCount++;
+ }
+ @action onError = () => {
+ let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
+ if (timeout < 10)
+ setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ }
+ _curSuffix = "_m";
+
@computed
get imageProxyRenderer() {
let thumbField = this.props.Document.thumbnail;
- if (thumbField) {
- let path = this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
- return <img src={path} width="100%" />;
+ if (thumbField && this._renderAsSvg && NumCast(this.props.Document.thumbnailPage, 0) === this.Document.curPage) {
+
+ // let transform = this.props.ScreenToLocalTransform().inverse();
+ let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
+ // var [sptX, sptY] = transform.transformPoint(0, 0);
+ // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
+ // let w = bptX - sptX;
+
+ let path = thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
+ // this._curSuffix = "";
+ // if (w > 20) {
+ let field = thumbField;
+ // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
+ // else if (w < 400 && this._mediumRetryCount < 10) this._curSuffix = "_m";
+ // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
+ if (field instanceof ImageField) path = this.choosePath(field.url);
+ // }
+ return <img className="pdfBox-thumbnail" key={path + (this._mediumRetryCount).toString()} src={path} onError={this.onError} />;
}
return (null);
}
- @observable _alt = false;
- @action
- onKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Alt") {
- this._alt = true;
- }
- }
- @action
- onKeyUp = (e: React.KeyboardEvent) => {
- if (e.key === "Alt") {
- this._alt = false;
+ @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true);
+ @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false);
+ onContextMenu = (e: React.MouseEvent): void => {
+ let field = Cast(this.Document[this.props.fieldKey], PdfField);
+ if (field) {
+ let url = field.url.href;
+ ContextMenu.Instance.addItem({
+ description: "Copy path", event: () => {
+ Utils.CopyText(url);
+ }, icon: "expand-arrows-alt"
+ });
}
}
render() {
- trace();
let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
return (
- <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} >
+ <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onContextMenu={this.onContextMenu} >
{this.pdfRenderer}
</div >
);
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 76bbeb37c..35db64cf4 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,8 @@
-.videobox-cont{
+.videoBox-cont, .videoBox-cont-fullScreen{
width: 100%;
height: Auto;
+}
+
+.videoBox-cont-fullScreen {
+ pointer-events: all;
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 422508f90..35ecf12f6 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,82 +1,145 @@
import React = require("react");
import { observer } from "mobx-react";
import { FieldView, FieldViewProps } from './FieldView';
+import * as rp from "request-promise";
import "./VideoBox.scss";
-import { action, computed, trace } from "mobx";
+import { action, IReactionDisposer, reaction, observable } from "mobx";
import { DocComponent } from "../DocComponent";
import { positionSchema } from "./DocumentView";
import { makeInterface } from "../../../new_fields/Schema";
import { pageSchema } from "./ImageBox";
-import { Cast, FieldValue, NumCast, ToConstructor, ListSpec } from "../../../new_fields/Types";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
-import Measure from "react-measure";
import "./VideoBox.scss";
-import { Field, FieldResult, Opt } from "../../../new_fields/Doc";
+import { RouteStore } from "../../../server/RouteStore";
+import { DocServer } from "../../DocServer";
+import { actionFieldDecorator } from "mobx/lib/internal";
type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const VideoDocument = makeInterface(positionSchema, pageSchema);
@observer
export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
-
+ private _reactionDisposer?: IReactionDisposer;
private _videoRef: HTMLVideoElement | null = null;
- private _loaded: boolean = false;
- private get initialTimecode() { return FieldValue(this.Document.curPage, -1); }
+ @observable _playTimer?: NodeJS.Timeout = undefined;
+ @observable _fullScreen = false;
+ @observable public Playing: boolean = false;
public static LayoutString() { return FieldView.LayoutString(VideoBox); }
- public get player(): HTMLVideoElement | undefined {
- if (this._videoRef) {
- return this._videoRef;
+ public get player(): HTMLVideoElement | null {
+ return this._videoRef;
+ }
+
+ videoLoad = () => {
+ let aspect = this.player!.videoWidth / this.player!.videoHeight;
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ if (!nativeWidth || !nativeHeight) {
+ if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
+ this.Document.nativeHeight = this.Document.nativeWidth / aspect;
+ this.Document.height = FieldValue(this.Document.width, 0) / aspect;
}
}
- @action
- setScaling = (r: any) => {
- if (this._loaded) {
- // bcz: the nativeHeight should really be set when the document is imported.
- var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
- var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
- var newNativeHeight = nativeWidth * r.entry.height / r.entry.width;
- if (!nativeHeight && newNativeHeight !== nativeHeight && !isNaN(newNativeHeight)) {
- this.Document.height = newNativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
- this.Document.nativeHeight = newNativeHeight;
- }
- } else {
- this._loaded = true;
+
+ @action public Play() {
+ this.Playing = true;
+ if (this.player) this.player.play();
+ if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 500);
+ }
+
+ @action public Pause() {
+ this.Playing = false;
+ if (this.player) this.player.pause();
+ if (this._playTimer) {
+ clearInterval(this._playTimer);
+ this._playTimer = undefined;
}
}
+ @action public FullScreen() {
+ this._fullScreen = true;
+ this.player && this.player.requestFullscreen();
+ }
+
+ @action
+ updateTimecode = () => {
+ this.player && (this.props.Document.curPage = this.player.currentTime);
+ }
+
componentDidMount() {
if (this.props.setVideoBox) this.props.setVideoBox(this);
}
+ componentWillUnmount() {
+ this.Pause();
+ if (this._reactionDisposer) this._reactionDisposer();
+ }
@action
setVideoRef = (vref: HTMLVideoElement | null) => {
this._videoRef = vref;
- if (this.initialTimecode >= 0 && vref) {
- vref.currentTime = this.initialTimecode;
+ if (vref) {
+ vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
+ if (this._reactionDisposer) this._reactionDisposer();
+ this._reactionDisposer = reaction(() => this.props.Document.curPage, () =>
+ !this.Playing && (vref.currentTime = this.Document.curPage || 0)
+ , { fireImmediately: true });
}
}
- videoContent(path: string) {
- return <video className="videobox-cont" ref={this.setVideoRef}>
- <source src={path} type="video/mp4" />
- Not supported.
- </video>;
+
+ getMp4ForVideo(videoId: string = "JN5beCVArMs") {
+ return new Promise(async (resolve, reject) => {
+ const videoInfoRequestConfig = {
+ headers: {
+ connection: 'keep-alive',
+ "user-agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/46.0',
+ },
+ };
+ try {
+ let responseSchema: any = {};
+ const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
+ const dataHtml = videoInfoResponse;
+ const start = dataHtml.indexOf('ytplayer.config = ') + 18;
+ const end = dataHtml.indexOf(';ytplayer.load');
+ const subString = dataHtml.substring(start, end);
+ const subJson = JSON.parse(subString);
+ const stringSub = subJson.args.player_response;
+ const stringSubJson = JSON.parse(stringSub);
+ const adaptiveFormats = stringSubJson.streamingData.adaptiveFormats;
+ const videoDetails = stringSubJson.videoDetails;
+ responseSchema.adaptiveFormats = adaptiveFormats;
+ responseSchema.videoDetails = videoDetails;
+ resolve(responseSchema);
+ }
+ catch (err) {
+ console.log(`
+ --- Youtube ---
+ Function: getMp4ForVideo
+ Error: `, err);
+ reject(err);
+ }
+ });
+ }
+ onPointerDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
}
render() {
let field = Cast(this.Document[this.props.fieldKey], VideoField);
- if (!field) {
- return <div>Loading</div>;
- }
- let content = this.videoContent(field.url.href);
- return NumCast(this.props.Document.nativeHeight) ?
- content :
- <Measure onResize={this.setScaling}>
- {({ measureRef }) =>
- <div style={{ width: "100%", height: "auto" }} ref={measureRef}>
- {content}
- </div>
- }
- </Measure>;
+
+ // this.getMp4ForVideo().then((mp4) => {
+ // console.log(mp4);
+ // }).catch(e => {
+ // console.log("")
+ // });
+ // //
+
+ let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : "");
+ return !field ? <div>Loading</div> :
+ <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} onPointerDown={this.onPointerDown}>
+ <source src={field.url.href} type="video/mp4" />
+ Not supported.
+ </video>;
}
} \ No newline at end of file
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
new file mode 100644
index 000000000..c2db3bdcb
--- /dev/null
+++ b/src/debug/Repl.tsx
@@ -0,0 +1,63 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import { observer } from 'mobx-react';
+import { observable, computed } from 'mobx';
+import { CompileScript } from '../client/util/Scripting';
+import { makeInterface } from '../new_fields/Schema';
+
+@observer
+class Repl extends React.Component {
+ @observable text: string = "";
+
+ @observable executedCommands: { command: string, result: any }[] = [];
+
+ onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+ this.text = e.target.value;
+ }
+
+ onKeyDown = (e: React.KeyboardEvent) => {
+ if (!e.ctrlKey && e.key === "Enter") {
+ e.preventDefault();
+ const script = CompileScript(this.text, {
+ addReturn: true, typecheck: false,
+ params: { makeInterface: "any" }
+ });
+ if (!script.compiled) {
+ this.executedCommands.push({ command: this.text, result: "Compile Error" });
+ } else {
+ const result = script.run({ makeInterface });
+ if (result.success) {
+ this.executedCommands.push({ command: this.text, result: result.result });
+ } else {
+ this.executedCommands.push({ command: this.text, result: result.error.message || result.error });
+ }
+ }
+ this.text = "";
+ }
+ }
+
+ @computed
+ get commands() {
+ return this.executedCommands.map(command => {
+ return (
+ <div style={{ marginTop: "5px" }}>
+ <p>{command.command}</p>
+ <pre>{JSON.stringify(command.result, null, 2)}</pre>
+ </div>
+ );
+ });
+ }
+
+ render() {
+ return (
+ <div>
+ <div style={{ verticalAlign: "bottom" }}>
+ {this.commands}
+ </div>
+ <textarea style={{ width: "100%", position: "absolute", bottom: "0px" }} value={this.text} onChange={this.onChange} onKeyDown={this.onKeyDown} />
+ </div>
+ );
+ }
+}
+
+ReactDOM.render(<Repl />, document.getElementById("root")); \ No newline at end of file
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 04ef00722..57221aa39 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -6,6 +6,7 @@ import { ImageField } from '../new_fields/URLField';
import { Doc } from '../new_fields/Doc';
import { List } from '../new_fields/List';
+
const schema1 = createSchema({
hello: "number",
test: "string",
@@ -41,7 +42,7 @@ class Test extends React.Component {
doc.fields = "test";
doc.test = "hello doc";
doc.url = url;
- doc.testDoc = doc2;
+ //doc.testDoc = doc2;
const test1: TestDoc = TestDoc(doc);
@@ -70,7 +71,9 @@ class Test extends React.Component {
}
render() {
- return <button onClick={this.onClick}>Click me</button>;
+ return <div><button onClick={this.onClick}>Click me</button>
+ {/* <input onKeyPress={this.onEnter}></input> */}
+ </div>;
}
}
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 4cac09dee..b22300d0b 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -3,184 +3,175 @@ import "normalize.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { observer } from 'mobx-react';
-
-// configure({
-// enforceActions: "observed"
-// });
-
-// @observer
-// class FieldViewer extends React.Component<{ field: BasicField<any> }> {
-// render() {
-// return <span>{JSON.stringify(this.props.field.Data)} ({this.props.field.Id})</span>;
-// }
-// }
-
-// @observer
-// class KeyViewer extends React.Component<{ field: Key }> {
-// render() {
-// return this.props.field.Name;
-// }
-// }
-
-// @observer
-// class ListViewer extends React.Component<{ field: ListField<Field> }>{
-// @observable
-// expanded = false;
-
-// render() {
-// let content;
-// if (this.expanded) {
-// content = (
-// <div>
-// {this.props.field.Data.map(field => <DebugViewer fieldId={field.Id} key={field.Id} />)}
-// </div>
-// );
-// } else {
-// content = <>[...] ({this.props.field.Id})</>;
-// }
-// return (
-// <div>
-// <button onClick={action(() => this.expanded = !this.expanded)}>Toggle</button>
-// {content}
-// </div >
-// );
-// }
-// }
-
-// @observer
-// class DocumentViewer extends React.Component<{ field: Document }> {
-// private keyMap: ObservableMap<string, Key> = new ObservableMap;
-
-// private disposer?: Lambda;
-
-// componentDidMount() {
-// let f = () => {
-// Array.from(this.props.field._proxies.keys()).forEach(id => {
-// if (!this.keyMap.has(id)) {
-// Server.GetField(id, (field) => {
-// if (field && field instanceof Key) {
-// this.keyMap.set(id, field);
-// }
-// });
-// }
-// });
-// };
-// this.disposer = this.props.field._proxies.observe(f);
-// f();
-// }
-
-// componentWillUnmount() {
-// if (this.disposer) {
-// this.disposer();
-// }
-// }
-
-// render() {
-// let fields = Array.from(this.props.field._proxies.entries()).map(kv => {
-// let key = this.keyMap.get(kv[0]);
-// return (
-// <div key={kv[0]}>
-// <b>({key ? key.Name : kv[0]}): </b>
-// <DebugViewer fieldId={kv[1]}></DebugViewer>
-// </div>
-// );
-// });
-// return (
-// <div>
-// Document ({this.props.field.Id})
-// <div style={{ paddingLeft: "25px" }}>
-// {fields}
-// </div>
-// </div>
-// );
-// }
-// }
-
-// @observer
-// class DebugViewer extends React.Component<{ fieldId: string }> {
-// @observable
-// private field?: Field;
-
-// @observable
-// private error?: string;
-
-// constructor(props: { fieldId: string }) {
-// super(props);
-// this.update();
-// }
-
-// update() {
-// Server.GetField(this.props.fieldId, action((field: Opt<Field>) => {
-// this.field = field;
-// if (!field) {
-// this.error = `Field with id ${this.props.fieldId} not found`;
-// }
-// }));
-
-// }
-
-// render() {
-// let content;
-// if (this.field) {
-// // content = this.field.ToJson();
-// if (this.field instanceof ListField) {
-// content = (<ListViewer field={this.field} />);
-// } else if (this.field instanceof Document) {
-// content = (<DocumentViewer field={this.field} />);
-// } else if (this.field instanceof BasicField) {
-// content = (<FieldViewer field={this.field} />);
-// } else if (this.field instanceof Key) {
-// content = (<KeyViewer field={this.field} />);
-// } else {
-// content = (<span>Unrecognized field type</span>);
-// }
-// } else if (this.error) {
-// content = <span>Field <b>{this.props.fieldId}</b> not found <button onClick={() => this.update()}>Refresh</button></span>;
-// } else {
-// content = <span>Field loading: {this.props.fieldId}</span>;
-// }
-// return content;
-// }
-// }
-
-// @observer
-// class Viewer extends React.Component {
-// @observable
-// private idToAdd: string = '';
-
-// @observable
-// private ids: string[] = [];
-
-// @action
-// inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-// this.idToAdd = e.target.value;
-// }
-
-// @action
-// onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
-// if (e.key === "Enter") {
-// this.ids.push(this.idToAdd);
-// this.idToAdd = "";
-// }
-// }
-
-// render() {
-// return (
-// <>
-// <input value={this.idToAdd}
-// onChange={this.inputOnChange}
-// onKeyDown={this.onKeyPress} />
-// <div>
-// {this.ids.map(id => <DebugViewer fieldId={id} key={id}></DebugViewer>)}
-// </div>
-// </>
-// );
-// }
-// }
-
-// ReactDOM.render((
-// <div style={{ position: "absolute", width: "100%", height: "100%" }}>
-// <Viewer />
-// </div>),
-// document.getElementById('root')
-// ); \ No newline at end of file
+import { Doc, Field, FieldResult, Opt } from '../new_fields/Doc';
+import { DocServer } from '../client/DocServer';
+import { Id } from '../new_fields/FieldSymbols';
+import { List } from '../new_fields/List';
+import { URLField } from '../new_fields/URLField';
+import { EditableView } from '../client/views/EditableView';
+import { CompileScript } from '../client/util/Scripting';
+
+function applyToDoc(doc: { [index: string]: FieldResult }, key: string, scriptString: string): boolean;
+function applyToDoc(doc: { [index: number]: FieldResult }, key: number, scriptString: string): boolean;
+function applyToDoc(doc: any, key: string | number, scriptString: string): boolean {
+ let script = CompileScript(scriptString, { addReturn: true, params: { this: doc instanceof Doc ? Doc.name : List.name } });
+ if (!script.compiled) {
+ return false;
+ }
+ const res = script.run({ this: doc });
+ if (!res.success) return false;
+ if (!Field.IsField(res.result, true)) return false;
+ doc[key] = res.result;
+ return true;
+}
+
+configure({
+ enforceActions: "observed"
+});
+
+@observer
+class ListViewer extends React.Component<{ field: List<Field> }>{
+ @observable
+ expanded = false;
+
+ @action
+ onClick = (e: React.MouseEvent) => {
+ this.expanded = !this.expanded;
+ e.stopPropagation();
+ }
+
+ render() {
+ let content;
+ if (this.expanded) {
+ content = (
+ <div>
+ {this.props.field.map((field, index) => <DebugViewer field={field} key={index} setValue={value => applyToDoc(this.props.field, index, value)} />)}
+ </div>
+ );
+ } else {
+ content = <>[...]</>;
+ }
+ return (
+ <div>
+ <button onClick={this.onClick}>Toggle</button>
+ {content}
+ </div >
+ );
+ }
+}
+
+@observer
+class DocumentViewer extends React.Component<{ field: Doc }> {
+ @observable
+ expanded = false;
+
+ @action
+ onClick = (e: React.MouseEvent) => {
+ this.expanded = !this.expanded;
+ e.stopPropagation();
+ }
+
+ render() {
+ let content;
+ if (this.expanded) {
+ const keys = Object.keys(this.props.field);
+ let fields = keys.map(key => {
+ return (
+ <div key={key}>
+ <b>({key}): </b>
+ <DebugViewer field={this.props.field[key]} setValue={value => applyToDoc(this.props.field, key, value)}></DebugViewer>
+ </div>
+ );
+ });
+ content = (
+ <div>
+ Document ({this.props.field[Id]})
+ <div style={{ paddingLeft: "25px" }}>
+ {fields}
+ </div>
+ </div>
+ );
+ } else {
+ content = <>[...] ({this.props.field[Id]})</>;
+ }
+ return (
+ <div>
+ <button onClick={this.onClick}>Toggle</button>
+ {content}
+ </div >
+ );
+ }
+}
+
+@observer
+class DebugViewer extends React.Component<{ field: FieldResult, setValue(value: string): boolean }> {
+
+ render() {
+ let content;
+ const field = this.props.field;
+ if (field instanceof List) {
+ content = (<ListViewer field={field} />);
+ } else if (field instanceof Doc) {
+ content = (<DocumentViewer field={field} />);
+ } else if (typeof field === "string") {
+ content = <p>"{field}"</p>;
+ } else if (typeof field === "number" || typeof field === "boolean") {
+ content = <p>{field}</p>;
+ } else if (field instanceof URLField) {
+ content = <p>{field.url.href}</p>;
+ } else if (field instanceof Promise) {
+ return <p>Field loading</p>;
+ } else {
+ return <p>Unrecognized field type</p>;
+ }
+
+ return <EditableView GetValue={() => Field.toScriptString(field)} SetValue={this.props.setValue}
+ contents={content}></EditableView>;
+ }
+}
+
+@observer
+class Viewer extends React.Component {
+ @observable
+ private idToAdd: string = '';
+
+ @observable
+ private fields: Field[] = [];
+
+ @action
+ inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.idToAdd = e.target.value;
+ }
+
+ @action
+ onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+ if (e.key === "Enter") {
+ DocServer.GetRefField(this.idToAdd).then(action((field: any) => {
+ if (field !== undefined) {
+ this.fields.push(field);
+ }
+ }));
+ this.idToAdd = "";
+ }
+ }
+
+ render() {
+ return (
+ <>
+ <input value={this.idToAdd}
+ onChange={this.inputOnChange}
+ onKeyDown={this.onKeyPress} />
+ <div>
+ {this.fields.map((field, index) => <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer>)}
+ </div>
+ </>
+ );
+ }
+}
+
+ReactDOM.render((
+ <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+ <Viewer />
+ </div>),
+ document.getElementById('root')
+); \ No newline at end of file
diff --git a/src/fields/TemplateField.ts b/src/fields/TemplateField.ts
deleted file mode 100644
index 72ae13c2e..000000000
--- a/src/fields/TemplateField.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-import { Template, TemplatePosition } from "../client/views/Templates";
-
-
-export class TemplateField extends BasicField<Array<Template>> {
- constructor(data: Array<Template> = [], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new TemplateField("${this.Data}")`;
- }
-
- Copy() {
- return new TemplateField(this.Data);
- }
-
- ToJson() {
- let templates: Array<{ name: string, position: TemplatePosition, layout: string }> = [];
- this.Data.forEach(template => {
- templates.push({ name: template.Name, layout: template.Layout, position: template.Position });
- });
- return {
- type: Types.Templates,
- data: templates,
- id: this.Id,
- };
- }
-
- UpdateFromServer(data: any) {
- this.data = new Array(data);
- }
-
- static FromJson(id: string, data: any): TemplateField {
- let templates: Array<Template> = [];
- data.forEach((template: { name: string, position: number, layout: string }) => {
- templates.push(new Template(template.name, template.position, template.layout));
- });
- return new TemplateField(templates, id, false);
- }
-} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.scss b/src/mobile/ImageUpload.scss
index d0b7d4e41..eea69b81c 100644
--- a/src/mobile/ImageUpload.scss
+++ b/src/mobile/ImageUpload.scss
@@ -1,7 +1,13 @@
+@import "../client/views/globalCssVariables.scss";
+
.imgupload_cont {
- height: 100vh;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
width: 100vw;
- align-content: center;
+ height: 100vh;
+
.button_file {
text-align: center;
height: 50%;
@@ -10,4 +16,19 @@
color: grey;
font-size: 3em;
}
+
+ .input_file {
+ display: none;
+ }
+
+ .upload_label,
+ .upload_button {
+ background: $dark-color;
+ font-size: 500%;
+ font-family: $sans-serif;
+ text-align: center;
+ padding: 5vh;
+ margin-bottom: 20px;
+ color: white;
+ }
} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 1f9e160ce..bfc1738fc 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -9,6 +9,8 @@ import { Opt, Doc } from '../new_fields/Doc';
import { Cast } from '../new_fields/Types';
import { listSpec } from '../new_fields/Schema';
import { List } from '../new_fields/List';
+import { observer } from 'mobx-react';
+import { observable } from 'mobx';
@@ -19,58 +21,91 @@ import { List } from '../new_fields/List';
// imgInput.click();
// }
// }
+const inputRef = React.createRef<HTMLInputElement>();
-const onFileLoad = async (file: any) => {
- let imgPrev = document.getElementById("img_preview");
- if (imgPrev) {
- let files: File[] = file.target.files;
- if (files.length !== 0) {
- console.log(files[0]);
- let formData = new FormData();
- formData.append("file", files[0]);
+@observer
+class Uploader extends React.Component {
+ @observable
+ error: string = "";
+ @observable
+ status: string = "";
- const upload = window.location.origin + "/upload";
- const res = await fetch(upload, {
- method: 'POST',
- body: formData
- });
- const json = await res.json();
- json.map(async (file: any) => {
- let path = window.location.origin + file;
- var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200 });
+ onClick = async () => {
+ try {
+ this.status = "initializing protos";
+ await Docs.initProtos();
+ let imgPrev = document.getElementById("img_preview");
+ if (imgPrev) {
+ let files: FileList | null = inputRef.current!.files;
+ if (files && files.length !== 0) {
+ console.log(files[0]);
+ const name = files[0].name;
+ let formData = new FormData();
+ formData.append("file", files[0]);
- const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
- if (!res) {
- throw new Error("No user id returned");
- }
- const field = await DocServer.GetRefField(res);
- let pending: Opt<Doc>;
- if (field instanceof Doc) {
- pending = await Cast(field.optionalRightCollection, Doc);
- }
- if (pending) {
- const data = await Cast(pending.data, listSpec(Doc));
- if (data) {
- data.push(doc);
- } else {
- pending.data = new List([doc]);
- }
- }
- });
+ const upload = window.location.origin + "/upload";
+ this.status = "uploading image";
+ const res = await fetch(upload, {
+ method: 'POST',
+ body: formData
+ });
+ this.status = "upload image, getting json";
+ const json = await res.json();
+ json.map(async (file: any) => {
+ let path = window.location.origin + file;
+ var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
+
+ this.status = "getting user document";
+
+ const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
+ if (!res) {
+ throw new Error("No user id returned");
+ }
+ const field = await DocServer.GetRefField(res);
+ let pending: Opt<Doc>;
+ if (field instanceof Doc) {
+ pending = await Cast(field.optionalRightCollection, Doc);
+ }
+ if (pending) {
+ this.status = "has pending docs";
+ const data = await Cast(pending.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ pending.data = new List([doc]);
+ }
+ this.status = "finished";
+ }
+ });
- // console.log(window.location.origin + file[0])
+ // console.log(window.location.origin + file[0])
- //imgPrev.setAttribute("src", window.location.origin + files[0].name)
+ //imgPrev.setAttribute("src", window.location.origin + files[0].name)
+ }
+ }
+ } catch (error) {
+ this.error = JSON.stringify(error);
}
}
-};
+
+ render() {
+ return (
+ <div className="imgupload_cont">
+ <label htmlFor="input_image_file" className="upload_label">Choose an Image</label>
+ <input type="file" accept="image/*" className="input_file" id="input_image_file" ref={inputRef}></input>
+ <button onClick={this.onClick} className="upload_button">Upload</button>
+ <img id="img_preview" src=""></img>
+ <p>{this.status}</p>
+ <p>{this.error}</p>
+ </div>
+ );
+ }
+
+}
+
ReactDOM.render((
- <div className="imgupload_cont">
- {/* <button className = "button_file" = {onPointerDown}> Open Image </button> */}
- <input type="file" accept="image/*" onChange={onFileLoad} className="input_file" id="input_image_file"></input>
- <img id="img_preview" src=""></img>
- <div id="message" />
- </div>),
+ <Uploader />
+),
document.getElementById('root')
); \ No newline at end of file
diff --git a/src/new_fields/CursorField.ts b/src/new_fields/CursorField.ts
new file mode 100644
index 000000000..fd86031a8
--- /dev/null
+++ b/src/new_fields/CursorField.ts
@@ -0,0 +1,63 @@
+import { ObjectField } from "./ObjectField";
+import { observable } from "mobx";
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, createSimpleSchema, object, date } from "serializr";
+import { OnUpdate, ToScriptString, Copy } from "./FieldSymbols";
+
+export type CursorPosition = {
+ x: number,
+ y: number
+};
+
+export type CursorMetadata = {
+ id: string,
+ identifier: string,
+ timestamp: number
+};
+
+export type CursorData = {
+ metadata: CursorMetadata,
+ position: CursorPosition
+};
+
+const PositionSchema = createSimpleSchema({
+ x: true,
+ y: true
+});
+
+const MetadataSchema = createSimpleSchema({
+ id: true,
+ identifier: true,
+ timestamp: true
+});
+
+const CursorSchema = createSimpleSchema({
+ metadata: object(MetadataSchema),
+ position: object(PositionSchema)
+});
+
+@Deserializable("cursor")
+export default class CursorField extends ObjectField {
+
+ @serializable(object(CursorSchema))
+ readonly data: CursorData;
+
+ constructor(data: CursorData) {
+ super();
+ this.data = data;
+ }
+
+ setPosition(position: CursorPosition) {
+ this.data.position = position;
+ this.data.metadata.timestamp = Date.now();
+ this[OnUpdate]();
+ }
+
+ [Copy]() {
+ return new CursorField(this.data);
+ }
+
+ [ToScriptString]() {
+ return "invalid";
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts
new file mode 100644
index 000000000..fc8abb9d9
--- /dev/null
+++ b/src/new_fields/DateField.ts
@@ -0,0 +1,23 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, date } from "serializr";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
+
+@Deserializable("date")
+export class DateField extends ObjectField {
+ @serializable(date())
+ readonly date: Date;
+
+ constructor(date: Date = new Date()) {
+ super();
+ this.date = date;
+ }
+
+ [Copy]() {
+ return new DateField(this.date);
+ }
+
+ [ToScriptString]() {
+ return `new DateField(new Date(${this.date.toISOString()}))`;
+ }
+}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 3055af1bf..7f7263cf1 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -4,28 +4,54 @@ import { autoObject, SerializationHelper, Deserializable } from "../client/util/
import { DocServer } from "../client/DocServer";
import { setter, getter, getField, updateFunction, deleteProperty } from "./util";
import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
-import { UndoManager, undoBatch } from "../client/util/UndoManager";
import { listSpec } from "./Schema";
-import { List } from "./List";
-import { ObjectField, Parent, OnUpdate } from "./ObjectField";
-import { RefField, FieldId, Id, HandleUpdate } from "./RefField";
-import { Docs } from "../client/documents/Documents";
-
-export function IsField(field: any): field is Field {
- return (typeof field === "string")
- || (typeof field === "number")
- || (typeof field === "boolean")
- || (field instanceof ObjectField)
- || (field instanceof RefField);
+import { ObjectField } from "./ObjectField";
+import { RefField, FieldId } from "./RefField";
+import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
+
+export namespace Field {
+ export function toScriptString(field: Field): string {
+ if (typeof field === "string") {
+ return `"${field}"`;
+ } else if (typeof field === "number" || typeof field === "boolean") {
+ return String(field);
+ } else {
+ return field[ToScriptString]();
+ }
+ }
+ export function IsField(field: any): field is Field;
+ export function IsField(field: any, includeUndefined: true): field is Field | undefined;
+ export function IsField(field: any, includeUndefined: boolean = false): field is Field | undefined {
+ return (typeof field === "string")
+ || (typeof field === "number")
+ || (typeof field === "boolean")
+ || (field instanceof ObjectField)
+ || (field instanceof RefField)
+ || (includeUndefined && field === undefined);
+ }
}
export type Field = number | string | boolean | ObjectField | RefField;
export type Opt<T> = T | undefined;
export type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>;
export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
-export const Update = Symbol("Update");
-export const Self = Symbol("Self");
-const SelfProxy = Symbol("SelfProxy");
+/**
+ * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs.
+ * If a default value is given, that will be returned instead of undefined.
+ * If a default value is given, the returned value should not be modified as it might be a temporary value.
+ * If no default value is given, and the returned value is not undefined, it can be safely modified.
+ */
+export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>;
+export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>;
+export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) {
+ const list = Cast(field, listSpec(Doc));
+ return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue);
+}
+
+export function DocListCast(field: FieldResult): Doc[] {
+ return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[];
+}
+
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
@@ -36,6 +62,7 @@ export class Doc extends RefField {
const doc = new Proxy<this>(this, {
set: setter,
get: getter,
+ // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
has: (target, key) => key in target.__fields,
ownKeys: target => Object.keys(target.__fields),
getOwnPropertyDescriptor: (target, prop) => {
@@ -43,6 +70,7 @@ export class Doc extends RefField {
return {
configurable: true,//TODO Should configurable be true?
enumerable: true,
+ value: target.__fields[prop]
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
@@ -85,11 +113,14 @@ export class Doc extends RefField {
private [Self] = this;
private [SelfProxy]: any;
- public [WidthSym] = () => NumCast(this.__fields.width); // bcz: is this the right way to access width/height? it didn't work with : this.width
- public [HeightSym] = () => NumCast(this.__fields.height);
+ public [WidthSym] = () => NumCast(this[SelfProxy].width); // bcz: is this the right way to access width/height? it didn't work with : this.width
+ public [HeightSym] = () => NumCast(this[SelfProxy].height);
+
+ [ToScriptString]() {
+ return "invalid";
+ }
public [HandleUpdate](diff: any) {
- console.log(diff);
const set = diff.$set;
if (set) {
for (const key in set) {
@@ -119,11 +150,15 @@ export namespace Doc {
const self = doc[Self];
return getField(self, key, ignoreProto);
}
- export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): T | null | undefined {
- return Cast(Get(doc, key, ignoreProto), ctor) as T | null | undefined;
+ export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
+ return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
+ }
+ export function IsPrototype(doc: Doc) {
+ return GetT(doc, "isPrototype", "boolean", true);
}
export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
- const proto = doc.proto;
+ const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc;
+
if (proto) {
proto[key] = value;
}
@@ -141,23 +176,47 @@ export namespace Doc {
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
const value = fields[key];
- if (value !== undefined) {
- doc[key] = value;
- }
+ // Do we want to filter out undefineds?
+ // if (value !== undefined) {
+ doc[key] = value;
+ // }
}
}
return doc;
}
- export function MakeAlias(doc: Doc) {
- const alias = new Doc;
+ // compare whether documents or their protos match
+ export function AreProtosEqual(doc: Doc, other: Doc) {
+ let r = (doc === other);
+ let r2 = (doc.proto === other);
+ let r3 = (other.proto === doc);
+ let r4 = (doc.proto === other.proto);
+ return r || r2 || r3 || r4;
+ }
- PromiseValue(Cast(doc.proto, Doc)).then(proto => {
- if (proto) {
- alias.proto = proto;
- }
- });
+ // gets the document's prototype or returns the document if it is a prototype
+ export function GetProto(doc: Doc) {
+ return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : doc.proto!;
+ }
+
+ export function allKeys(doc: Doc): string[] {
+ const results: Set<string> = new Set;
+ let proto: Doc | undefined = doc;
+ while (proto) {
+ Object.keys(proto).forEach(key => results.add(key));
+ proto = proto.proto;
+ }
+
+ return Array.from(results);
+ }
+
+
+ export function MakeAlias(doc: Doc) {
+ const alias = new Doc;
+ if (!GetT(doc, "isPrototype", "boolean", true)) {
+ alias.proto = doc.proto;
+ }
return alias;
}
@@ -182,42 +241,14 @@ export namespace Doc {
return copy;
}
- export function MakeLink(source: Doc, target: Doc) {
- UndoManager.RunInBatch(() => {
- let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
- //let linkDoc = new Doc;
- linkDoc.title = "-link name-";
- linkDoc.linkDescription = "";
- linkDoc.linkTags = "Default";
-
- linkDoc.linkedTo = target;
- linkDoc.linkedFrom = source;
-
- let linkedFrom = Cast(target.linkedFromDocs, listSpec(Doc));
- if (!linkedFrom) {
- target.linkedFromDocs = linkedFrom = new List<Doc>();
- }
- linkedFrom.push(linkDoc);
-
- let linkedTo = Cast(source.linkedToDocs, listSpec(Doc));
- if (!linkedTo) {
- source.linkedToDocs = linkedTo = new List<Doc>();
- }
- linkedTo.push(linkDoc);
- return linkDoc;
- }, "make link");
- }
-
- export function MakeDelegate(doc: Doc): Doc;
- export function MakeDelegate(doc: Opt<Doc>): Opt<Doc>;
- export function MakeDelegate(doc: Opt<Doc>): Opt<Doc> {
+ export function MakeDelegate(doc: Doc, id?: string): Doc;
+ export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc>;
+ export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc> {
if (!doc) {
return undefined;
}
- const delegate = new Doc();
- //TODO Does this need to be doc[Self]?
+ const delegate = new Doc(id, true);
delegate.proto = doc;
return delegate;
}
- export const Prototype = Symbol("Prototype");
} \ No newline at end of file
diff --git a/src/new_fields/FieldSymbols.ts b/src/new_fields/FieldSymbols.ts
new file mode 100644
index 000000000..a436dcf2b
--- /dev/null
+++ b/src/new_fields/FieldSymbols.ts
@@ -0,0 +1,10 @@
+
+export const Update = Symbol("Update");
+export const Self = Symbol("Self");
+export const SelfProxy = Symbol("SelfProxy");
+export const HandleUpdate = Symbol("HandleUpdate");
+export const Id = Symbol("Id");
+export const OnUpdate = Symbol("OnUpdate");
+export const Parent = Symbol("Parent");
+export const Copy = Symbol("Copy");
+export const ToScriptString = Symbol("Copy"); \ No newline at end of file
diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts
index d998746bb..f952acff9 100644
--- a/src/new_fields/HtmlField.ts
+++ b/src/new_fields/HtmlField.ts
@@ -1,6 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, primitive } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("html")
export class HtmlField extends ObjectField {
@@ -15,4 +16,8 @@ export class HtmlField extends ObjectField {
[Copy]() {
return new HtmlField(this.html);
}
+
+ [ToScriptString]() {
+ return "invalid";
+ }
}
diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts
index 1a928389d..62b2cd254 100644
--- a/src/new_fields/IconField.ts
+++ b/src/new_fields/IconField.ts
@@ -1,6 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, primitive } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("icon")
export class IconField extends ObjectField {
@@ -15,4 +16,8 @@ export class IconField extends ObjectField {
[Copy]() {
return new IconField(this.icon);
}
+
+ [ToScriptString]() {
+ return "invalid";
+ }
}
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 86a8bd18a..4e3b7abe0 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -1,8 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
-import { number } from "prop-types";
-import { any } from "bluebird";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
import { deepCopy } from "../Utils";
export enum InkTool {
@@ -11,6 +10,7 @@ export enum InkTool {
Highlighter,
Eraser
}
+
export interface StrokeData {
pathData: Array<{ x: number, y: number }>;
color: string;
@@ -39,6 +39,10 @@ export class InkField extends ObjectField {
}
[Copy]() {
- return new InkField(deepCopy(this.inkData))
+ return new InkField(deepCopy(this.inkData));
+ }
+
+ [ToScriptString]() {
+ return "invalid";
}
}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index 96018dafa..f1e4c4721 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -1,11 +1,12 @@
import { Deserializable, autoObject } from "../client/util/SerializationHelper";
-import { Field, Update, Self, FieldResult } from "./Doc";
-import { setter, getter, deleteProperty } from "./util";
+import { Field } from "./Doc";
+import { setter, getter, deleteProperty, updateFunction } from "./util";
import { serializable, alias, list } from "serializr";
import { observable, action } from "mobx";
-import { ObjectField, OnUpdate, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
import { RefField } from "./RefField";
import { ProxyField } from "./Proxy";
+import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, Copy } from "./FieldSymbols";
const listHandlers: any = {
/// Mutator methods
@@ -27,7 +28,17 @@ const listHandlers: any = {
},
push: action(function (this: any, ...items: any[]) {
items = items.map(toObjectField);
- const res = this[Self].__fields.push(...items);
+ const list = this[Self];
+ const length = list.__fields.length;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i + length, item, this);
+ }
+ }
+ const res = list.__fields.push(...items);
this[Update]();
return res;
}),
@@ -48,12 +59,33 @@ const listHandlers: any = {
},
splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
items = items.map(toObjectField);
- const res = this[Self].__fields.splice(start, deleteCount, ...items);
+ const list = this[Self];
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i + start, item, this);
+ }
+ }
+ const res = list.__fields.splice(start, deleteCount, ...items);
this[Update]();
return res.map(toRealField);
}),
unshift(...items: any[]) {
items = items.map(toObjectField);
+ const list = this[Self];
+ const length = list.__fields.length;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i, item, this);
+ }
+ }
const res = this[Self].__fields.unshift(...items);
this[Update]();
return res;
@@ -194,19 +226,32 @@ type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
@Deserializable("list")
class ListImpl<T extends Field> extends ObjectField {
- constructor(fields: T[] = []) {
+ constructor(fields?: T[]) {
super();
const list = new Proxy<this>(this, {
set: setter,
get: listGetter,
+ ownKeys: target => Object.keys(target.__fields),
+ getOwnPropertyDescriptor: (target, prop) => {
+ if (prop in target.__fields) {
+ return {
+ configurable: true,//TODO Should configurable be true?
+ enumerable: true,
+ };
+ }
+ return Reflect.getOwnPropertyDescriptor(target, prop);
+ },
deleteProperty: deleteProperty,
defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
});
- (list as any).push(...fields);
+ this[SelfProxy] = list;
+ if (fields) {
+ (list as any).push(...fields);
+ }
return list;
}
- [key: number]: FieldResult<T>;
+ [key: number]: T | (T extends RefField ? Promise<T> : never);
@serializable(alias("fields", list(autoObject())))
private get __fields() {
@@ -215,6 +260,12 @@ class ListImpl<T extends Field> extends ObjectField {
private set __fields(value) {
this.___fields = value;
+ for (const key in value) {
+ const field = value[key];
+ if (!(field instanceof ObjectField)) continue;
+ (field as ObjectField)[Parent] = this[Self];
+ (field as ObjectField)[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ }
}
[Copy]() {
@@ -235,6 +286,12 @@ class ListImpl<T extends Field> extends ObjectField {
}
private [Self] = this;
+ private [SelfProxy]: any;
+
+ [ToScriptString]() {
+ return "invalid";
+ // return `new List([${(this as any).map((field => Field.toScriptString(field))}])`;
+ }
}
-export type List<T extends Field> = ListImpl<T> & T[];
+export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; \ No newline at end of file
diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts
index 0f3777af6..5f4a6f8fb 100644
--- a/src/new_fields/ObjectField.ts
+++ b/src/new_fields/ObjectField.ts
@@ -1,13 +1,13 @@
import { Doc } from "./Doc";
-
-export const OnUpdate = Symbol("OnUpdate");
-export const Parent = Symbol("Parent");
-export const Copy = Symbol("Copy");
+import { RefField } from "./RefField";
+import { OnUpdate, Parent, Copy, ToScriptString } from "./FieldSymbols";
export abstract class ObjectField {
- protected [OnUpdate]?: (diff?: any) => void;
- private [Parent]?: Doc;
+ protected [OnUpdate](diff?: any) { }
+ private [Parent]?: RefField | ObjectField;
abstract [Copy](): ObjectField;
+
+ abstract [ToScriptString](): string;
}
export namespace ObjectField {
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index fd99ae1c0..130ec066e 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -3,8 +3,9 @@ import { FieldWaiting } from "./Doc";
import { primitive, serializable } from "serializr";
import { observable, action } from "mobx";
import { DocServer } from "../client/DocServer";
-import { RefField, Id } from "./RefField";
-import { ObjectField, Copy } from "./ObjectField";
+import { RefField } from "./RefField";
+import { ObjectField } from "./ObjectField";
+import { Id, Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("proxy")
export class ProxyField<T extends RefField> extends ObjectField {
@@ -26,6 +27,10 @@ export class ProxyField<T extends RefField> extends ObjectField {
return new ProxyField<T>(this.fieldId);
}
+ [ToScriptString]() {
+ return "invalid";
+ }
+
@serializable(primitive())
readonly fieldId: string = "";
diff --git a/src/new_fields/RefField.ts b/src/new_fields/RefField.ts
index 202c65f21..75ce4287f 100644
--- a/src/new_fields/RefField.ts
+++ b/src/new_fields/RefField.ts
@@ -1,9 +1,8 @@
import { serializable, primitive, alias } from "serializr";
import { Utils } from "../Utils";
+import { Id, HandleUpdate, ToScriptString } from "./FieldSymbols";
export type FieldId = string;
-export const HandleUpdate = Symbol("HandleUpdate");
-export const Id = Symbol("Id");
export abstract class RefField {
@serializable(alias("id", primitive()))
private __id: FieldId;
@@ -15,4 +14,6 @@ export abstract class RefField {
}
protected [HandleUpdate]?(diff: any): void;
+
+ abstract [ToScriptString](): string;
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index eb30e76de..89d077a47 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -1,6 +1,7 @@
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
import { serializable } from "serializr";
import { Deserializable } from "../client/util/SerializationHelper";
+import { Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("RichTextField")
export class RichTextField extends ObjectField {
@@ -15,4 +16,8 @@ export class RichTextField extends ObjectField {
[Copy]() {
return new RichTextField(this.Data);
}
+
+ [ToScriptString]() {
+ return "invalid";
+ }
} \ No newline at end of file
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
index b821baec9..250f3c975 100644
--- a/src/new_fields/Schema.ts
+++ b/src/new_fields/Schema.ts
@@ -1,4 +1,4 @@
-import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType } from "./Types";
+import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from "./Types";
import { Doc, Field } from "./Doc";
type AllToInterface<T extends Interface[]> = {
@@ -10,7 +10,7 @@ export const emptySchema = createSchema({});
export const Document = makeInterface(emptySchema);
export type Document = makeInterface<[typeof emptySchema]>;
-export type makeInterface<T extends Interface[]> = Partial<AllToInterface<T>> & Doc & { proto: Doc | undefined };
+export type makeInterface<T extends Interface[]> = AllToInterface<T> & Doc & { proto: Doc | undefined };
// export function makeInterface<T extends Interface[], U extends Doc>(schemas: T): (doc: U) => All<T, U>;
// export function makeInterface<T extends Interface, U extends Doc>(schema: T): (doc: U) => makeInterface<T, U>;
export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc) => makeInterface<T> {
@@ -24,7 +24,12 @@ export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc)
get(target: any, prop, receiver) {
const field = receiver.doc[prop];
if (prop in schema) {
- return Cast(field, (schema as any)[prop]);
+ const desc = (schema as any)[prop];
+ if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {
+ return Cast(field, desc.type, desc.defaultVal);
+ } else {
+ return Cast(field, (schema as any)[prop]);
+ }
}
return field;
},
@@ -79,4 +84,11 @@ export function createSchema<T extends Interface>(schema: T): T & { proto: ToCon
export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> {
return { List: type as any };//TODO Types
+}
+
+export function defaultSpec<T extends ToConstructor<Field>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> {
+ return {
+ type: type as any,
+ defaultVal
+ };
} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index 60f08dc90..c04dd5e6d 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -1,12 +1,14 @@
-import { Field, Opt, FieldResult } from "./Doc";
+import { Field, Opt, FieldResult, Doc } from "./Doc";
import { List } from "./List";
+import { RefField } from "./RefField";
-export type ToType<T extends ToConstructor<Field> | ListSpec<Field>> =
+export type ToType<T extends ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field>> =
T extends "string" ? string :
T extends "number" ? number :
T extends "boolean" ? boolean :
T extends ListSpec<infer U> ? List<U> :
// T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
+ T extends DefaultFieldConstructor<infer _U> ? never :
T extends { new(...args: any[]): List<Field> } ? never :
T extends { new(...args: any[]): infer R } ? R : never;
@@ -18,12 +20,17 @@ export type ToConstructor<T extends Field> =
new (...args: any[]) => T;
export type ToInterface<T extends Interface> = {
- [P in Exclude<keyof T, "proto">]: FieldResult<ToType<T[P]>>;
+ [P in Exclude<keyof T, "proto">]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>;
};
// type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> };
export type ListSpec<T extends Field> = { List: ToConstructor<T> };
+export type DefaultFieldConstructor<T extends Field> = {
+ type: ToConstructor<T>,
+ defaultVal: T
+};
+
// type ListType<U extends Field[]> = { 0: List<ListType<Tail<U>>>, 1: ToType<Head<U>> }[HasTail<U> extends true ? 0 : 1];
export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
@@ -33,7 +40,7 @@ export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
export interface Interface {
- [key: string]: ToConstructor<Field> | ListSpec<Field>;
+ [key: string]: ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field>;
// [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
}
@@ -71,7 +78,7 @@ export function BoolCast(field: FieldResult, defaultVal: boolean | null = null)
return Cast(field, "boolean", defaultVal);
}
-type WithoutList<T extends Field> = T extends List<infer R> ? R[] : T;
+type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
export function FieldValue<T extends Field, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
export function FieldValue<T extends Field>(field: FieldResult<T>): Opt<T>;
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index d00a95a16..4a2841fb6 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -1,6 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
+import { ToScriptString, Copy } from "./FieldSymbols";
function url() {
return custom(
@@ -13,15 +14,24 @@ function url() {
);
}
-export class URLField extends ObjectField {
+export abstract class URLField extends ObjectField {
@serializable(url())
readonly url: URL;
- constructor(url: URL) {
+ constructor(url: string);
+ constructor(url: URL);
+ constructor(url: URL | string) {
super();
+ if (typeof url === "string") {
+ url = new URL(url);
+ }
this.url = url;
}
+ [ToScriptString]() {
+ return `new ${this.constructor.name}("${this.url.href}")`;
+ }
+
[Copy](): this {
return new (this.constructor as any)(this.url);
}
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index bbd8157f6..2b304c373 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -1,11 +1,12 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Update, Doc, Field } from "./Doc";
+import { Doc, Field } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField } from "./Proxy";
import { FieldValue } from "./Types";
-import { RefField, Id } from "./RefField";
-import { ObjectField, Parent, OnUpdate } from "./ObjectField";
+import { RefField } from "./RefField";
+import { ObjectField } from "./ObjectField";
import { action } from "mobx";
+import { Parent, OnUpdate, Update, Id } from "./FieldSymbols";
export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
if (SerializationHelper.IsSerializing()) {
@@ -37,7 +38,11 @@ export const setter = action(function (target: any, prop: string | symbol | numb
delete curValue[Parent];
delete curValue[OnUpdate];
}
- target.__fields[prop] = value;
+ if (value === undefined) {
+ delete target.__fields[prop];
+ } else {
+ target.__fields[prop] = value;
+ }
target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
UndoManager.AddEvent({
redo: () => receiver[prop] = value,
@@ -55,23 +60,30 @@ export function getter(target: any, prop: string | symbol | number, receiver: an
}
return getField(target, prop);
}
+function getProtoField(protoField: Doc | undefined, prop: string | number, cb?: (field: Field | undefined) => void) {
+ if (!protoField) return undefined;
+ let field = protoField[prop];
+ if (field instanceof Promise) {
+ cb && field.then(cb);
+ return field;
+ } else {
+ cb && cb(field);
+ return field;
+ }
+}
+//TODO The callback parameter is never being passed in currently, so we should be able to get rid of it.
export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
const field = target.__fields[prop];
if (field instanceof ProxyField) {
return field.value(callback);
}
- if (field === undefined && !ignoreProto) {
+ if (field === undefined && !ignoreProto && prop !== "proto") {
const proto = getField(target, "proto", true);
if (proto instanceof Doc) {
- let field = proto[prop];
- if (field instanceof Promise) {
- callback && field.then(callback);
- return undefined;
- } else {
- callback && callback(field);
- return field;
- }
+ return getProtoField(proto, prop, callback);
+ } else if (proto instanceof Promise) {
+ return proto.then(async proto => getProtoField(proto, prop, callback));
}
}
callback && callback(field);
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index fdf5b6a5c..c4af5cdaa 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -11,6 +11,7 @@ export enum RouteStore {
// UPLOAD AND STATIC FILE SERVING
public = "/public",
upload = "/upload",
+ dataUriToImage = "/uploadURI",
images = "/images",
// USER AND WORKSPACES
diff --git a/src/server/Search.ts b/src/server/Search.ts
new file mode 100644
index 000000000..5ca5578a7
--- /dev/null
+++ b/src/server/Search.ts
@@ -0,0 +1,49 @@
+import * as rp from 'request-promise';
+import { Database } from './database';
+import { thisExpression } from 'babel-types';
+
+export class Search {
+ public static Instance = new Search();
+ private url = 'http://localhost:8983/solr/';
+
+ public async updateDocument(document: any) {
+ try {
+ const res = await rp.post(this.url + "dash/update", {
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify([document])
+ });
+ return res;
+ } catch (e) {
+ console.warn("Search error: " + e + document);
+ }
+ }
+
+ public async search(query: string) {
+ try {
+ const searchResults = JSON.parse(await rp.get(this.url + "dash/select", {
+ qs: {
+ q: query,
+ fl: "id"
+ }
+ }));
+ const fields = searchResults.response.docs;
+ const ids = fields.map((field: any) => field.id);
+ return ids;
+ } catch {
+ return [];
+ }
+ }
+
+ public async clear() {
+ try {
+ return await rp.post(this.url + "dash/update", {
+ body: {
+ delete: {
+ query: "*:*"
+ }
+ },
+ json: true
+ });
+ } catch { }
+ }
+} \ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 5f45d7bcc..e5b7a025b 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,15 +1,17 @@
-import { computed, observable, action, runInAction } from "mobx";
+import { action, computed, observable, runInAction } from "mobx";
import * as rp from 'request-promise';
+import { DocServer } from "../../../client/DocServer";
import { Docs } from "../../../client/documents/Documents";
+import { Gateway, NorthstarSettings } from "../../../client/northstar/manager/Gateway";
import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea";
import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil";
-import { RouteStore } from "../../RouteStore";
-import { DocServer } from "../../../client/DocServer";
-import { Doc } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView";
-import { CollectionTreeView } from "../../../client/views/collections/CollectionTreeView";
import { CollectionView } from "../../../client/views/collections/CollectionView";
+import { Doc } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast } from "../../../new_fields/Types";
+import { RouteStore } from "../../RouteStore";
export class CurrentUserUtils {
private static curr_email: string;
@@ -31,13 +33,13 @@ export class CurrentUserUtils {
doc.title = this.email;
doc.data = new List<Doc>();
doc.excludeFromLibrary = true;
- doc.optionalRightCollection = Docs.SchemaDocument([], { title: "Pending documents" });
+ doc.optionalRightCollection = Docs.StackingDocument([], { title: "New mobile uploads" });
// doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
// (doc.library as Doc).excludeFromLibrary = true;
return doc;
}
- public static loadCurrentUser(): Promise<any> {
+ public static async loadCurrentUser(): Promise<any> {
let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
if (response) {
let obj = JSON.parse(response);
@@ -47,7 +49,7 @@ export class CurrentUserUtils {
throw new Error("There should be a user! Why does Dash think there isn't one?");
}
});
- let userDocPromise = rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+ let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
return DocServer.GetRefField(id).then(field =>
runInAction(() => this.user_document = field instanceof Doc ? field : this.createUserDocument(id)));
@@ -55,14 +57,63 @@ export class CurrentUserUtils {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
});
+ try {
+ const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" });
+ NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json());
+ await Gateway.Instance.ClearCatalog();
+ const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
+ let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc)));
+ let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras);
+ // if (catprom) await Promise.all(catprom);
+ } catch (e) {
+
+ }
return Promise.all([userPromise, userDocPromise]);
}
/* Northstar catalog ... really just for testing so this should eventually go away */
+ // --------------- Northstar hooks ------------- /
+ static _northstarSchemas: Doc[] = [];
@observable private static _northstarCatalog?: Catalog;
@computed public static get NorthstarDBCatalog() { return this._northstarCatalog; }
+
+ @action static SetNorthstarCatalog(ctlog: Catalog, extras: Catalog[]) {
+ CurrentUserUtils.NorthstarDBCatalog = ctlog;
+ // if (ctlog && ctlog.schemas) {
+ // extras.map(ex => ctlog.schemas!.push(ex));
+ // return ctlog.schemas.map(async schema => {
+ // let schemaDocuments: Doc[] = [];
+ // let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema);
+ // await Promise.all(attributesToBecomeDocs.reduce((promises, attr) => {
+ // promises.push(DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+ // if (field instanceof Doc) {
+ // schemaDocuments.push(field);
+ // } else {
+ // var atmod = new ColumnAttributeModel(attr);
+ // let histoOp = new HistogramOperation(schema.displayName!,
+ // new AttributeTransformationModel(atmod, AggregateFunction.None),
+ // new AttributeTransformationModel(atmod, AggregateFunction.Count),
+ // new AttributeTransformationModel(atmod, AggregateFunction.Count));
+ // schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
+ // }
+ // })));
+ // return promises;
+ // }, [] as Promise<void>[]));
+ // return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
+ // });
+ // }
+ }
public static set NorthstarDBCatalog(ctlog: Catalog | undefined) { this._northstarCatalog = ctlog; }
+ public static AddNorthstarSchema(schema: Schema, schemaDoc: Doc) {
+ if (this._northstarCatalog && CurrentUserUtils._northstarSchemas) {
+ this._northstarCatalog.schemas!.push(schema);
+ CurrentUserUtils._northstarSchemas.push(schemaDoc);
+ let schemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
+ schemas.push(schema.displayName!);
+ CurrentUserUtils.UserDocument.DBSchemas = new List<string>(schemas);
+ }
+ }
public static GetNorthstarSchema(name: string): Schema | undefined {
return !this._northstarCatalog || !this._northstarCatalog.schemas ? undefined :
ArrayUtil.FirstOrDefault<Schema>(this._northstarCatalog.schemas, (s: Schema) => s.displayName === name);
diff --git a/src/server/database.ts b/src/server/database.ts
index 37cfcf3a3..70b3efced 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -8,9 +8,13 @@ export class Database {
private url = 'mongodb://localhost:27017/Dash';
private currentWrites: { [id: string]: Promise<void> } = {};
private db?: mongodb.Db;
+ private onConnect: (() => void)[] = [];
constructor() {
- this.MongoClient.connect(this.url, (err, client) => this.db = client.db());
+ this.MongoClient.connect(this.url, (err, client) => {
+ this.db = client.db();
+ this.onConnect.forEach(fn => fn());
+ });
}
public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) {
@@ -22,13 +26,6 @@ export class Database {
return new Promise<void>(resolve => {
collection.updateOne({ _id: id }, value, { upsert }
, (err, res) => {
- if (err) {
- console.log(err.message);
- console.log(err.errmsg);
- }
- // if (res) {
- // console.log(JSON.stringify(res.result));
- // }
if (this.currentWrites[id] === newProm) {
delete this.currentWrites[id];
}
@@ -39,50 +36,98 @@ export class Database {
};
newProm = prom ? prom.then(run) : run();
this.currentWrites[id] = newProm;
+ } else {
+ this.onConnect.push(() => this.update(id, value, callback, upsert, collectionName));
}
}
public delete(id: string, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).remove({ id: id });
+ if (this.db) {
+ this.db.collection(collectionName).remove({ id: id });
+ } else {
+ this.onConnect.push(() => this.delete(id, collectionName));
+ }
}
public deleteAll(collectionName = Database.DocumentsCollection): Promise<any> {
- return new Promise(res =>
- this.db && this.db.collection(collectionName).deleteMany({}, res));
+ return new Promise(res => {
+ if (this.db) {
+ this.db.collection(collectionName).deleteMany({}, res);
+ } else {
+ this.onConnect.push(() => this.db && this.db.collection(collectionName).deleteMany({}, res));
+ }
+ });
}
public insert(value: any, collectionName = Database.DocumentsCollection) {
- if ("id" in value) {
- value._id = value.id;
- delete value.id;
+ if (this.db) {
+ if ("id" in value) {
+ value._id = value.id;
+ delete value.id;
+ }
+ const id = value._id;
+ const collection = this.db.collection(collectionName);
+ const prom = this.currentWrites[id];
+ let newProm: Promise<void>;
+ const run = (): Promise<void> => {
+ return new Promise<void>(resolve => {
+ collection.insertOne(value, (err, res) => {
+ if (this.currentWrites[id] === newProm) {
+ delete this.currentWrites[id];
+ }
+ resolve();
+ });
+ });
+ };
+ newProm = prom ? prom.then(run) : run();
+ this.currentWrites[id] = newProm;
+ } else {
+ this.onConnect.push(() => this.insert(value, collectionName));
}
- this.db && this.db.collection(collectionName).insertOne(value);
}
public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).findOne({ _id: id }, (err, result) => {
- if (result) {
- result.id = result._id;
- delete result._id;
- fn(result);
- } else {
- fn(undefined);
- }
- });
+ if (this.db) {
+ this.db.collection(collectionName).findOne({ _id: id }, (err, result) => {
+ if (result) {
+ result.id = result._id;
+ delete result._id;
+ fn(result);
+ } else {
+ fn(undefined);
+ }
+ });
+ } else {
+ this.onConnect.push(() => this.getDocument(id, fn, collectionName));
+ }
}
public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => {
- if (err) {
- console.log(err.message);
- console.log(err.errmsg);
- }
- fn(docs.map(doc => {
- doc.id = doc._id;
- delete doc._id;
- return doc;
- }));
- });
+ if (this.db) {
+ this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => {
+ if (err) {
+ console.log(err.message);
+ console.log(err.errmsg);
+ }
+ fn(docs.map(doc => {
+ doc.id = doc._id;
+ delete doc._id;
+ return doc;
+ }));
+ });
+ } else {
+ this.onConnect.push(() => this.getDocuments(ids, fn, collectionName));
+ }
+ }
+
+ public query(query: any): Promise<mongodb.Cursor> {
+ if (this.db) {
+ return Promise.resolve<mongodb.Cursor>(this.db.collection('newDocuments').find(query));
+ } else {
+ return new Promise<mongodb.Cursor>(res => {
+ this.onConnect.push(() => res(this.query(query)));
+ });
+ }
}
public print() {
diff --git a/src/server/downsize.ts b/src/server/downsize.ts
new file mode 100644
index 000000000..ed68fbecc
--- /dev/null
+++ b/src/server/downsize.ts
@@ -0,0 +1,40 @@
+import * as sharp from 'sharp';
+import * as fs from 'fs';
+
+const folder = "./src/server/public/files/";
+const pngTypes = ["png", "PNG"];
+const jpgTypes = ["jpg", "JPG", "jpeg", "JPEG"];
+const smallResizer = sharp().resize(100);
+fs.readdir(folder, async (err, files) => {
+ if (err) {
+ console.log(err);
+ return;
+ }
+ // files.forEach(file => {
+ // if (file.includes("_s") || file.includes("_m") || file.includes("_l")) {
+ // fs.unlink(folder + file, () => { });
+ // }
+ // });
+ for (const file of files) {
+ const filesplit = file.split(".");
+ let resizers = [
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ ];
+ if (pngTypes.some(type => file.endsWith(type))) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.png();
+ });
+ } else if (jpgTypes.some(type => file.endsWith(type))) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.jpeg();
+ });
+ } else {
+ continue;
+ }
+ resizers.forEach(resizer => {
+ fs.createReadStream(folder + file).pipe(resizer.resizer).pipe(fs.createWriteStream(folder + filesplit[0] + resizer.suffix + "." + filesplit[1]));
+ });
+ }
+}); \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 6801b3132..fd66c90b4 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -6,11 +6,14 @@ import * as session from 'express-session';
import * as expressValidator from 'express-validator';
import * as formidable from 'formidable';
import * as fs from 'fs';
+import * as sharp from 'sharp';
+const imageDataUri = require('image-data-uri');
import * as mobileDetect from 'mobile-detect';
import { ObservableMap } from 'mobx';
import * as passport from 'passport';
import * as path from 'path';
import * as request from 'request';
+import * as rp from 'request-promise';
import * as io from 'socket.io';
import { Socket } from 'socket.io';
import * as webpack from 'webpack';
@@ -21,7 +24,7 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo
import { DashUserModel } from './authentication/models/user_model';
import { Client } from './Client';
import { Database } from './database';
-import { MessageStore, Transferable, Diff } from "./Message";
+import { MessageStore, Transferable, Types, Diff } from "./Message";
import { RouteStore } from './RouteStore';
const app = express();
const config = require('../../webpack.config');
@@ -31,6 +34,9 @@ const serverPort = 4321;
import expressFlash = require('express-flash');
import flash = require('connect-flash');
import c = require("crypto");
+import { Search } from './Search';
+import { debug } from 'util';
+import _ = require('lodash');
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
@@ -54,7 +60,7 @@ app.use(session({
app.use(flash());
app.use(expressFlash());
-app.use(bodyParser.json());
+app.use(bodyParser.json({ limit: "10mb" }));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(expressValidator());
app.use(passport.initialize());
@@ -117,8 +123,16 @@ app.get("/pull", (req, res) =>
res.redirect("/");
}));
+// SEARCH
+
// GETTERS
+app.get("/search", async (req, res) => {
+ let query = req.query.query || "hello";
+ let results = await Search.Instance.search(query);
+ res.send(results);
+});
+
// anyone attempting to navigate to localhost at this port will
// first have to login
addSecureRoute(
@@ -154,13 +168,15 @@ addSecureRoute(
RouteStore.getCurrUser
);
+const pngTypes = [".png", ".PNG"];
+const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
+const uploadDir = __dirname + "/public/files/";
// SETTERS
-
-addSecureRoute(
- Method.POST,
- (user, res, req) => {
+app.post(
+ RouteStore.upload,
+ (req, res) => {
let form = new formidable.IncomingForm();
- form.uploadDir = __dirname + "/public/files/";
+ form.uploadDir = uploadDir;
form.keepExtensions = true;
// let path = req.body.path;
console.log("upload");
@@ -168,15 +184,76 @@ addSecureRoute(
console.log("parsing");
let names: string[] = [];
for (const name in files) {
- names.push(`/files/` + path.basename(files[name].path));
+ const file = path.basename(files[name].path);
+ const ext = path.extname(file);
+ let resizers = [
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ ];
+ let isImage = false;
+ if (pngTypes.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.png();
+ });
+ isImage = true;
+ } else if (jpgTypes.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.jpeg();
+ });
+ isImage = true;
+ }
+ if (isImage) {
+ resizers.forEach(resizer => {
+ fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
+ });
+ }
+ names.push(`/files/` + file);
}
res.send(names);
});
+ }
+);
+
+addSecureRoute(
+ Method.POST,
+ (user, res, req) => {
+ const uri = req.body.uri;
+ const filename = req.body.name;
+ if (!uri || !filename) {
+ res.status(401).send("incorrect parameters specified");
+ return;
+ }
+ imageDataUri.outputFile(uri, uploadDir + filename).then((savedName: string) => {
+ const ext = path.extname(savedName);
+ let resizers = [
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ ];
+ let isImage = false;
+ if (pngTypes.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.png();
+ });
+ isImage = true;
+ } else if (jpgTypes.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.jpeg();
+ });
+ isImage = true;
+ }
+ if (isImage) {
+ resizers.forEach(resizer => {
+ fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + filename + resizer.suffix + ext));
+ });
+ }
+ res.send("/files/" + filename + ext);
+ });
},
undefined,
- RouteStore.upload
+ RouteStore.dataUriToImage
);
-
// AUTHENTICATION
// Sign Up
@@ -240,6 +317,7 @@ server.on("connection", function (socket: Socket) {
async function deleteFields() {
await Database.Instance.deleteAll();
+ await Search.Instance.clear();
await Database.Instance.deleteAll('newDocuments');
}
@@ -248,6 +326,7 @@ async function deleteAll() {
await Database.Instance.deleteAll('newDocuments');
await Database.Instance.deleteAll('sessions');
await Database.Instance.deleteAll('users');
+ await Search.Instance.clear();
}
function barReceived(guid: String) {
@@ -266,6 +345,11 @@ function getFields([ids, callback]: [string[], (result: Transferable[]) => void]
function setField(socket: Socket, newValue: Transferable) {
Database.Instance.update(newValue.id, newValue, () =>
socket.broadcast.emit(MessageStore.SetField.Message, newValue));
+ if (newValue.type === Types.Text) {
+ Search.Instance.updateDocument({ id: newValue.id, data: (newValue as any).data });
+ console.log("set field");
+ console.log("checking in");
+ }
}
function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
@@ -276,9 +360,81 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v
Database.Instance.getDocuments(ids, callback, "newDocuments");
}
+
+const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
+ "number": "_n",
+ "string": "_t",
+ // "boolean": "_b",
+ // "image": ["_t", "url"],
+ "video": ["_t", "url"],
+ "pdf": ["_t", "url"],
+ "audio": ["_t", "url"],
+ "web": ["_t", "url"],
+ "date": ["_d", value => new Date(value.date).toISOString()],
+ "proxy": ["_i", "fieldId"],
+ "list": ["_l", list => {
+ const results = [];
+ for (const value of list.fields) {
+ const term = ToSearchTerm(value);
+ if (term) {
+ results.push(term.value);
+ }
+ }
+ return results.length ? results : null;
+ }]
+};
+
+function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
+ if (val === null || val === undefined) {
+ return;
+ }
+ const type = val.__type || typeof val;
+ let suffix = suffixMap[type];
+ if (!suffix) {
+ return;
+ }
+
+ if (Array.isArray(suffix)) {
+ const accessor = suffix[1];
+ if (typeof accessor === "function") {
+ val = accessor(val);
+ } else {
+ val = val[accessor];
+ }
+ suffix = suffix[0];
+ }
+
+ return { suffix, value: val };
+}
+
+function getSuffix(value: string | [string, any]): string {
+ return typeof value === "string" ? value : value[0];
+}
+
function UpdateField(socket: Socket, diff: Diff) {
Database.Instance.update(diff.id, diff.diff,
() => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments");
+ const docfield = diff.diff.$set;
+ if (!docfield) {
+ return;
+ }
+ const update: any = { id: diff.id };
+ let dynfield = false;
+ for (let key in docfield) {
+ if (!key.startsWith("fields.")) continue;
+ dynfield = true;
+ let val = docfield[key];
+ key = key.substring(7);
+ Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null });
+ let term = ToSearchTerm(val);
+ if (term !== undefined) {
+ let { suffix, value } = term;
+ update[key + suffix] = { set: value };
+ }
+ }
+ if (dynfield) {
+ Search.Instance.updateDocument(update);
+ }
}
function CreateField(newValue: any) {
diff --git a/src/server/public/files/.gitignore b/src/server/public/files/.gitignore
index f59ec20aa..c96a04f00 100644
--- a/src/server/public/files/.gitignore
+++ b/src/server/public/files/.gitignore
@@ -1 +1,2 @@
-* \ No newline at end of file
+*
+!.gitignore \ No newline at end of file
diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts
new file mode 100644
index 000000000..6f4d6642f
--- /dev/null
+++ b/src/server/remapUrl.ts
@@ -0,0 +1,59 @@
+import { Database } from "./database";
+import { Search } from "./Search";
+import * as path from 'path';
+
+const suffixMap: { [type: string]: true } = {
+ "video": true,
+ "pdf": true,
+ "audio": true,
+ "web": true
+};
+
+async function update() {
+ await new Promise(res => setTimeout(res, 10));
+ console.log("update");
+ const cursor = await Database.Instance.query({});
+ console.log("Cleared");
+ const updates: [string, any][] = [];
+ function updateDoc(doc: any) {
+ if (doc.__type !== "Doc") {
+ return;
+ }
+ const fields = doc.fields;
+ if (!fields) {
+ return;
+ }
+ const update: any = {
+ };
+ let dynfield = false;
+ for (const key in fields) {
+ const value = fields[key];
+ if (value && value.__type && suffixMap[value.__type]) {
+ const url = new URL(value.url);
+ if (url.href.includes("azure")) {
+ dynfield = true;
+
+ update.$set = { ["fields." + key + ".url"]: `${url.protocol}//localhost:1050${url.pathname}` };
+ }
+ }
+ }
+ if (dynfield) {
+ updates.push([doc._id, update]);
+ }
+ }
+ await cursor.forEach(updateDoc);
+ await Promise.all(updates.map(doc => {
+ console.log(doc[0], doc[1]);
+ return new Promise(res => Database.Instance.update(doc[0], doc[1], () => {
+ console.log("wrote " + JSON.stringify(doc[1]));
+ res();
+ }, false, "newDocuments"));
+ }));
+ console.log("Done");
+ // await Promise.all(updates.map(update => {
+ // return limit(() => Search.Instance.updateDocument(update));
+ // }));
+ cursor.close();
+}
+
+update();
diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts
new file mode 100644
index 000000000..de1fd25e1
--- /dev/null
+++ b/src/server/updateSearch.ts
@@ -0,0 +1,101 @@
+import { Database } from "./database";
+import { Cursor } from "mongodb";
+import { Search } from "./Search";
+import pLimit from 'p-limit';
+
+const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
+ "number": "_n",
+ "string": "_t",
+ // "boolean": "_b",
+ // "image": ["_t", "url"],
+ "video": ["_t", "url"],
+ "pdf": ["_t", "url"],
+ "audio": ["_t", "url"],
+ "web": ["_t", "url"],
+ "date": ["_d", value => new Date(value.date).toISOString()],
+ "proxy": ["_i", "fieldId"],
+ "list": ["_l", list => {
+ const results = [];
+ for (const value of list.fields) {
+ const term = ToSearchTerm(value);
+ if (term) {
+ results.push(term.value);
+ }
+ }
+ return results.length ? results : null;
+ }]
+};
+
+function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
+ if (val === null || val === undefined) {
+ return;
+ }
+ const type = val.__type || typeof val;
+ let suffix = suffixMap[type];
+ if (!suffix) {
+ return;
+ }
+
+ if (Array.isArray(suffix)) {
+ const accessor = suffix[1];
+ if (typeof accessor === "function") {
+ val = accessor(val);
+ } else {
+ val = val[accessor];
+ }
+ suffix = suffix[0];
+ }
+
+ return { suffix, value: val };
+}
+
+function getSuffix(value: string | [string, any]): string {
+ return typeof value === "string" ? value : value[0];
+}
+
+const limit = pLimit(5);
+async function update() {
+ // await new Promise(res => setTimeout(res, 5));
+ console.log("update");
+ await Search.Instance.clear();
+ const cursor = await Database.Instance.query({});
+ console.log("Cleared");
+ const updates: any[] = [];
+ let numDocs = 0;
+ function updateDoc(doc: any) {
+ numDocs++;
+ if ((numDocs % 50) === 0) {
+ console.log("updateDoc " + numDocs);
+ }
+ console.log("doc " + numDocs);
+ if (doc.__type !== "Doc") {
+ return;
+ }
+ const fields = doc.fields;
+ if (!fields) {
+ return;
+ }
+ const update: any = { id: doc._id };
+ let dynfield = false;
+ for (const key in fields) {
+ const value = fields[key];
+ const term = ToSearchTerm(value);
+ if (term !== undefined) {
+ let { suffix, value } = term;
+ update[key + suffix] = value;
+ dynfield = true;
+ }
+ }
+ if (dynfield) {
+ updates.push(update);
+ console.log(updates.length);
+ }
+ }
+ await cursor.forEach(updateDoc);
+ await Promise.all(updates.map(update => {
+ return limit(() => Search.Instance.updateDocument(update));
+ }));
+ cursor.close();
+}
+
+update(); \ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index c08742272..5e0a6a883 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -8,6 +8,7 @@ module.exports = {
entry: {
bundle: ["./src/client/views/Main.tsx", 'webpack-hot-middleware/client?reload=true'],
viewer: ["./src/debug/Viewer.tsx", 'webpack-hot-middleware/client?reload=true'],
+ repl: ["./src/debug/Repl.tsx", 'webpack-hot-middleware/client?reload=true'],
test: ["./src/debug/Test.tsx", 'webpack-hot-middleware/client?reload=true'],
inkControls: ["./src/mobile/InkControls.tsx", 'webpack-hot-middleware/client?reload=true'],
imageUpload: ["./src/mobile/ImageUpload.tsx", 'webpack-hot-middleware/client?reload=true'],