aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.vscode/launch.json9
-rw-r--r--package.json7
-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.xml49
-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.ts36
-rw-r--r--src/client/DocServer.ts130
-rw-r--r--src/client/Server.ts173
-rw-r--r--src/client/SocketStub.ts69
-rw-r--r--src/client/documents/Documents.ts290
-rw-r--r--src/client/goldenLayout.js5359
-rw-r--r--src/client/northstar/core/brusher/IBaseBrushable.ts4
-rw-r--r--src/client/northstar/core/filter/FilterModel.ts17
-rw-r--r--src/client/northstar/core/filter/IBaseFilterConsumer.ts4
-rw-r--r--src/client/northstar/dash-fields/HistogramField.ts95
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx50
-rw-r--r--src/client/northstar/operations/HistogramOperation.ts17
-rw-r--r--src/client/util/DocumentManager.ts44
-rw-r--r--src/client/util/DragManager.ts76
-rw-r--r--src/client/util/Scripting.ts34
-rw-r--r--src/client/util/SelectionManager.ts6
-rw-r--r--src/client/util/SerializationHelper.ts130
-rw-r--r--src/client/util/TooltipTextMenu.tsx8
-rw-r--r--src/client/util/UndoManager.ts7
-rw-r--r--src/client/views/DocComponent.tsx14
-rw-r--r--src/client/views/DocumentDecorations.scss116
-rw-r--r--src/client/views/DocumentDecorations.tsx256
-rw-r--r--src/client/views/EditableView.tsx2
-rw-r--r--src/client/views/InkingCanvas.tsx56
-rw-r--r--src/client/views/InkingControl.tsx7
-rw-r--r--src/client/views/InkingStroke.scss3
-rw-r--r--src/client/views/InkingStroke.tsx10
-rw-r--r--src/client/views/Main.scss6
-rw-r--r--src/client/views/Main.tsx189
-rw-r--r--src/client/views/MainOverlayTextBox.tsx17
-rw-r--r--src/client/views/PresentationView.scss68
-rw-r--r--src/client/views/PresentationView.tsx191
-rw-r--r--src/client/views/PreviewCursor.tsx2
-rw-r--r--src/client/views/TemplateMenu.tsx36
-rw-r--r--src/client/views/Templates.tsx16
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx111
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx211
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx21
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx170
-rw-r--r--src/client/views/collections/CollectionSubView.tsx343
-rw-r--r--src/client/views/collections/CollectionTreeView.scss56
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx170
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx94
-rw-r--r--src/client/views/collections/CollectionView.tsx15
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx36
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx110
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss148
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx208
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx142
-rw-r--r--src/client/views/globalCssVariables.scss2
-rw-r--r--src/client/views/nodes/AudioBox.tsx27
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx205
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx64
-rw-r--r--src/client/views/nodes/DocumentView.tsx227
-rw-r--r--src/client/views/nodes/FieldView.tsx71
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss8
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx123
-rw-r--r--src/client/views/nodes/IconBox.scss12
-rw-r--r--src/client/views/nodes/IconBox.tsx53
-rw-r--r--src/client/views/nodes/ImageBox.scss2
-rw-r--r--src/client/views/nodes/ImageBox.tsx75
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx37
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx61
-rw-r--r--src/client/views/nodes/LinkBox.scss23
-rw-r--r--src/client/views/nodes/LinkBox.tsx84
-rw-r--r--src/client/views/nodes/LinkEditor.scss3
-rw-r--r--src/client/views/nodes/LinkEditor.tsx17
-rw-r--r--src/client/views/nodes/LinkMenu.tsx27
-rw-r--r--src/client/views/nodes/PDFBox.tsx60
-rw-r--r--src/client/views/nodes/VideoBox.tsx69
-rw-r--r--src/client/views/nodes/WebBox.tsx23
-rw-r--r--src/debug/Test.tsx90
-rw-r--r--src/debug/Viewer.tsx368
-rw-r--r--src/fields/AudioField.ts31
-rw-r--r--src/fields/BasicField.ts59
-rw-r--r--src/fields/BooleanField.ts25
-rw-r--r--src/fields/Document.ts460
-rw-r--r--src/fields/DocumentReference.ts57
-rw-r--r--src/fields/Field.ts69
-rw-r--r--src/fields/FieldUpdatedArgs.ts27
-rw-r--r--src/fields/HtmlField.ts25
-rw-r--r--src/fields/IconFIeld.ts25
-rw-r--r--src/fields/ImageField.ts29
-rw-r--r--src/fields/InkField.ts53
-rw-r--r--src/fields/Key.ts50
-rw-r--r--src/fields/KeyStore.ts70
-rw-r--r--src/fields/ListField.ts196
-rw-r--r--src/fields/NumberField.ts25
-rw-r--r--src/fields/PDFField.ts36
-rw-r--r--src/fields/RichTextField.ts26
-rw-r--r--src/fields/ScriptField.ts174
-rw-r--r--src/fields/TextField.ts25
-rw-r--r--src/fields/TupleField.ts59
-rw-r--r--src/fields/VideoField.ts30
-rw-r--r--src/fields/WebField.ts30
-rw-r--r--src/mobile/ImageUpload.tsx34
-rw-r--r--src/new_fields/DateField.ts18
-rw-r--r--src/new_fields/Doc.ts229
-rw-r--r--src/new_fields/HtmlField.ts18
-rw-r--r--src/new_fields/IconField.ts18
-rw-r--r--src/new_fields/InkField.ts44
-rw-r--r--src/new_fields/List.ts240
-rw-r--r--src/new_fields/ObjectField.ts17
-rw-r--r--src/new_fields/Proxy.ts65
-rw-r--r--src/new_fields/RefField.ts18
-rw-r--r--src/new_fields/RichTextField.ts18
-rw-r--r--src/new_fields/Schema.ts82
-rw-r--r--src/new_fields/Types.ts88
-rw-r--r--src/new_fields/URLField.ts34
-rw-r--r--src/new_fields/util.ts104
-rw-r--r--src/server/Message.ts13
-rw-r--r--src/server/Search.ts37
-rw-r--r--src/server/ServerUtil.ts64
-rw-r--r--src/server/authentication/controllers/WorkspacesMenu.css3
-rw-r--r--src/server/authentication/controllers/WorkspacesMenu.tsx89
-rw-r--r--src/server/authentication/models/current_user_utils.ts37
-rw-r--r--src/server/database.ts70
-rw-r--r--src/server/index.ts58
-rw-r--r--test/test.ts166
168 files changed, 18711 insertions, 4435 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index fb91a1080..e92a4949a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -15,6 +15,15 @@
},
{
"type": "node",
+ "request": "attach",
+ "name": "Typescript Server",
+ "protocol": "inspector",
+ "port": 9229,
+ "localRoot": "${workspaceFolder}",
+ "remoteRoot": "."
+ },
+ {
+ "type": "node",
"request": "launch",
"name": "Mocha Tests",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
diff --git a/package.json b/package.json
index 2463afa74..147f59c25 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": {
@@ -125,7 +126,7 @@
"mobx-react-devtools": "^6.1.1",
"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",
@@ -163,8 +164,10 @@
"react-table": "^6.9.2",
"request": "^2.88.0",
"request-promise": "^4.2.4",
+ "serializr": "^1.5.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..5e80b17d9
--- /dev/null
+++ b/solr/conf/schema.xml
@@ -0,0 +1,49 @@
+<?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" docValues="true"/>
+ <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/>
+ <fieldType name="pdate" class="solr.DatePointField" docValues="true"/>
+ <fieldType name="pdouble" class="solr.DoublePointField" docValues="true"/>
+
+ <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="plong" indexed="false" stored="false"/>
+
+ <dynamicField name="*_t" type="string" indexed="true" stored="true"/>
+ <dynamicField name="*_n" type="pdouble" indexed="true" stored="true"/>
+</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..981500022
--- /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">data</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 8252ba011..d4b6f5377 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,7 +2,6 @@ import v4 = require('uuid/v4');
import v5 = require("uuid/v5");
import { Socket } from 'socket.io';
import { Message } from './server/Message';
-import { Document } from './fields/Document';
export class Utils {
@@ -89,13 +88,20 @@ export class Utils {
}
}
-export function OmitKeys(obj: any, keys: any, addKeyFunc?: (dup: any) => void) {
+export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void): { omit: any, extract: any } {
+ const omit: any = { ...obj };
+ const extract: any = {};
+ keys.forEach(key => {
+ extract[key] = omit[key];
+ delete omit[key];
+ });
+ addKeyFunc && addKeyFunc(omit);
+ return { omit, extract };
+}
+
+export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void) {
var dup: any = {};
- for (var key in obj) {
- if (keys.indexOf(key) === -1) {
- dup[key] = obj[key];
- }
- }
+ keys.forEach(key => dup[key] = obj[key]);
addKeyFunc && addKeyFunc(dup);
return dup;
}
@@ -110,6 +116,18 @@ export function returnZero() { return 0; }
export function emptyFunction() { }
-export function emptyDocFunction(doc: Document) { }
+export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
-export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; \ No newline at end of file
+export type Predicate<K, V> = (entry: [K, V]) => boolean;
+
+export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
+ let deepCopy = new Map<K, V>();
+ let entries = source.entries(), next = entries.next();
+ while (!next.done) {
+ let entry = next.value;
+ if (!predicate || predicate(entry)) {
+ deepCopy.set(entry[0], entry[1]);
+ }
+ }
+ return deepCopy;
+} \ No newline at end of file
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
new file mode 100644
index 000000000..a288d394a
--- /dev/null
+++ b/src/client/DocServer.ts
@@ -0,0 +1,130 @@
+import * as OpenSocket from 'socket.io-client';
+import { MessageStore } from "./../server/Message";
+import { Opt } from '../new_fields/Doc';
+import { Utils } from '../Utils';
+import { SerializationHelper } from './util/SerializationHelper';
+import { RefField, HandleUpdate, Id } from '../new_fields/RefField';
+
+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 prepend(extension: string): string {
+ return window.location.origin + extension;
+ }
+
+ export function DeleteDatabase() {
+ Utils.Emit(_socket, MessageStore.DeleteAll, {});
+ }
+
+ 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 field = SerializationHelper.Deserialize(fieldJson);
+ if (_cache[id] !== undefined && !(_cache[id] instanceof Promise)) {
+ id;
+ }
+ if (field !== undefined) {
+ _cache[id] = field;
+ } else {
+ delete _cache[id];
+ }
+ return field;
+ });
+ _cache[id] = prom;
+ return prom;
+ } else if (cached instanceof Promise) {
+ return cached;
+ } else {
+ return cached;
+ }
+ }
+
+ export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
+ const requestedIds: string[] = [];
+ const waitingIds: string[] = [];
+ const promises: Promise<Opt<RefField>>[] = [];
+ const map: { [id: string]: Opt<RefField> } = {};
+ for (const id of ids) {
+ const cached = _cache[id];
+ if (cached === undefined) {
+ requestedIds.push(id);
+ } else if (cached instanceof Promise) {
+ promises.push(cached);
+ waitingIds.push(id);
+ } else {
+ map[id] = cached;
+ }
+ }
+ const prom = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds).then(fields => {
+ const fieldMap: { [id: string]: RefField } = {};
+ for (const field of fields) {
+ if (field !== undefined) {
+ fieldMap[field.id] = SerializationHelper.Deserialize(field);
+ }
+ }
+ return fieldMap;
+ });
+ requestedIds.forEach(id => _cache[id] = prom.then(fields => fields[id]));
+ const fields = await prom;
+ requestedIds.forEach(id => {
+ const field = fields[id];
+ if (field !== undefined) {
+ _cache[id] = field;
+ } else {
+ delete _cache[id];
+ }
+ map[id] = field;
+ });
+ const otherFields = await Promise.all(promises);
+ waitingIds.forEach((id, index) => map[id] = otherFields[index]);
+ return map;
+ }
+
+ export function UpdateField(id: string, diff: any) {
+ if (id === updatingId) {
+ return;
+ }
+ Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ }
+
+ export function CreateField(field: RefField) {
+ _cache[field[Id]] = field;
+ const initialState = SerializationHelper.Serialize(field);
+ Utils.Emit(_socket, MessageStore.CreateField, initialState);
+ }
+
+ let updatingId: string | undefined;
+ function respondToUpdate(diff: any) {
+ const id = diff.id;
+ if (id === undefined) {
+ return;
+ }
+ const field = _cache[id];
+ const update = (f: Opt<RefField>) => {
+ if (f === undefined) {
+ return;
+ }
+ const handler = f[HandleUpdate];
+ if (handler) {
+ updatingId = id;
+ handler.call(f, diff.diff);
+ updatingId = undefined;
+ }
+ };
+ if (field instanceof Promise) {
+ field.then(update);
+ } else {
+ update(field);
+ }
+ }
+
+ function connected() {
+ _socket.emit(MessageStore.Bar.Message, GUID);
+ }
+
+ Utils.AddServerHandler(_socket, MessageStore.Foo, connected);
+ Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+} \ No newline at end of file
diff --git a/src/client/Server.ts b/src/client/Server.ts
deleted file mode 100644
index 66e9878d9..000000000
--- a/src/client/Server.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { Key } from "../fields/Key";
-import { ObservableMap, action, reaction, runInAction } from "mobx";
-import { Field, FieldWaiting, FIELD_WAITING, Opt, FieldId } from "../fields/Field";
-import { Document } from "../fields/Document";
-import { SocketStub, FieldMap } from "./SocketStub";
-import * as OpenSocket from 'socket.io-client';
-import { Utils, emptyFunction } from "./../Utils";
-import { MessageStore, Types } from "./../server/Message";
-
-export class Server {
- public static ClientFieldsCached: ObservableMap<FieldId, Field | FIELD_WAITING> = new ObservableMap();
- static Socket: SocketIOClient.Socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
- static GUID: string = Utils.GenerateGuid();
-
- // Retrieves the cached value of the field and sends a request to the server for the real value (if it's not cached).
- // Call this is from within a reaction and test whether the return value is FieldWaiting.
- public static GetField(fieldid: FieldId): Promise<Opt<Field>>;
- public static GetField(fieldid: FieldId, callback: (field: Opt<Field>) => void): void;
- public static GetField(fieldid: FieldId, callback?: (field: Opt<Field>) => void): Promise<Opt<Field>> | void {
- let fn = (cb: (field: Opt<Field>) => void) => {
-
- let cached = this.ClientFieldsCached.get(fieldid);
- if (cached === undefined) {
- this.ClientFieldsCached.set(fieldid, FieldWaiting);
- SocketStub.SEND_FIELD_REQUEST(fieldid, action((field: Field | undefined) => {
- let cached = this.ClientFieldsCached.get(fieldid);
- if (cached !== FieldWaiting) {
- cb(cached);
- }
- else {
- if (field) {
- this.ClientFieldsCached.set(fieldid, field);
- } else {
- this.ClientFieldsCached.delete(fieldid);
- }
- cb(field);
- }
- }));
- } else if (cached !== FieldWaiting) {
- setTimeout(() => cb(cached as Field), 0);
- } else {
- reaction(() => this.ClientFieldsCached.get(fieldid),
- (field, reaction) => {
- if (field !== FieldWaiting) {
- reaction.dispose();
- cb(field);
- }
- });
- }
- };
- if (callback) {
- fn(callback);
- } else {
- return new Promise(fn);
- }
- }
-
- public static GetFields(fieldIds: FieldId[]): Promise<{ [id: string]: Field }>;
- public static GetFields(fieldIds: FieldId[], callback: (fields: FieldMap) => any): void;
- public static GetFields(fieldIds: FieldId[], callback?: (fields: FieldMap) => any): Promise<FieldMap> | void {
- let fn = action((cb: (fields: FieldMap) => void) => {
-
- let neededFieldIds: FieldId[] = [];
- let waitingFieldIds: FieldId[] = [];
- let existingFields: FieldMap = {};
- for (let id of fieldIds) {
- let field = this.ClientFieldsCached.get(id);
- if (field === undefined) {
- neededFieldIds.push(id);
- this.ClientFieldsCached.set(id, FieldWaiting);
- } else if (field === FieldWaiting) {
- waitingFieldIds.push(id);
- } else {
- existingFields[id] = field;
- }
- }
- SocketStub.SEND_FIELDS_REQUEST(neededFieldIds, action((fields: FieldMap) => {
- for (let id of neededFieldIds) {
- let field = fields[id];
- if (field) {
- if (this.ClientFieldsCached.get(field.Id) === FieldWaiting) {
- this.ClientFieldsCached.set(field.Id, field);
- } else {
- throw new Error("we shouldn't be trying to replace things that are already in the cache");
- }
- } else {
- if (this.ClientFieldsCached.get(id) === FieldWaiting) {
- this.ClientFieldsCached.delete(id);
- } else {
- throw new Error("we shouldn't be trying to replace things that are already in the cache");
- }
- }
- }
- reaction(() => waitingFieldIds.map(id => this.ClientFieldsCached.get(id)),
- (cachedFields, reaction) => {
- if (!cachedFields.some(field => field === FieldWaiting)) {
- const realFields = cachedFields as Opt<Field>[];
- reaction.dispose();
- waitingFieldIds.forEach((id, index) => {
- existingFields[id] = realFields[index];
- });
- cb({ ...fields, ...existingFields });
- }
- }, { fireImmediately: true });
- }));
- });
- if (callback) {
- fn(callback);
- } else {
- return new Promise(fn);
- }
- }
-
- public static GetDocumentField(doc: Document, key: Key, callback?: (field: Field) => void) {
- let field = doc._proxies.get(key.Id);
- if (field) {
- this.GetField(field,
- action((fieldfromserver: Opt<Field>) => {
- if (fieldfromserver) {
- doc.fields.set(key.Id, { key, field: fieldfromserver });
- if (callback) {
- callback(fieldfromserver);
- }
- }
- }));
- }
- }
-
- public static DeleteDocumentField(doc: Document, key: Key) {
- SocketStub.SEND_DELETE_DOCUMENT_FIELD(doc, key);
- }
-
- public static UpdateField(field: Field) {
- if (!this.ClientFieldsCached.has(field.Id)) {
- this.ClientFieldsCached.set(field.Id, field);
- }
- SocketStub.SEND_SET_FIELD(field);
- }
-
- static connected(message: string) {
- Server.Socket.emit(MessageStore.Bar.Message, Server.GUID);
- }
-
- @action
- private static cacheField(clientField: Field) {
- var cached = this.ClientFieldsCached.get(clientField.Id);
- if (!cached) {
- this.ClientFieldsCached.set(clientField.Id, clientField);
- } else {
- // probably should overwrite the values within any field that was already here...
- }
- return this.ClientFieldsCached.get(clientField.Id);
- }
-
- @action
- static updateField(field: { id: string, data: any, type: Types }) {
- if (Server.ClientFieldsCached.has(field.id)) {
- var f = Server.ClientFieldsCached.get(field.id);
- if (f) {
- // console.log("Applying : " + field.id);
- f.UpdateFromServer(field.data);
- f.init(emptyFunction);
- } else {
- // console.log("Not applying wa : " + field.id);
- }
- } else {
- // console.log("Not applying mi : " + field.id);
- }
- }
-}
-
-Utils.AddServerHandler(Server.Socket, MessageStore.Foo, Server.connected);
-Utils.AddServerHandler(Server.Socket, MessageStore.SetField, Server.updateField);
diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts
deleted file mode 100644
index 382a81f66..000000000
--- a/src/client/SocketStub.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Key } from "../fields/Key";
-import { Field, FieldId, Opt } from "../fields/Field";
-import { ObservableMap } from "mobx";
-import { Document } from "../fields/Document";
-import { MessageStore, Transferable } from "../server/Message";
-import { Utils } from "../Utils";
-import { Server } from "./Server";
-import { ServerUtils } from "../server/ServerUtil";
-
-
-export interface FieldMap {
- [id: string]: Opt<Field>;
-}
-
-//TODO tfs: I think it might be cleaner to not have SocketStub deal with turning what the server gives it into Fields (in other words not call ServerUtils.FromJson), and leave that for the Server class.
-export class SocketStub {
-
- static FieldStore: ObservableMap<FieldId, Field> = new ObservableMap();
-
- public static SEND_FIELD_REQUEST(fieldid: FieldId): Promise<Opt<Field>>;
- public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Opt<Field>) => void): void;
- public static SEND_FIELD_REQUEST(fieldid: FieldId, callback?: (field: Opt<Field>) => void): Promise<Opt<Field>> | void {
- let fn = function (cb: (field: Opt<Field>) => void) {
- Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: Transferable) => {
- if (field) {
- ServerUtils.FromJson(field).init(cb);
- } else {
- cb(undefined);
- }
- });
- };
- if (callback) {
- fn(callback);
- } else {
- return new Promise(fn);
- }
- }
-
- public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: FieldMap) => any) {
- Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: Transferable[]) => {
- let fieldMap: FieldMap = {};
- fields.map(field => fieldMap[field.id] = ServerUtils.FromJson(field));
- let proms = Object.values(fieldMap).map(val =>
- new Promise(resolve => val!.init(resolve)));
- Promise.all(proms).then(() => callback(fieldMap));
- });
- }
-
- public static SEND_DELETE_DOCUMENT_FIELD(doc: Document, key: Key) {
- // Send a request to delete the field stored under the specified key from the document
-
- // ...SOCKET(DELETE_DOCUMENT_FIELD, document id, key id)
-
- // Server removes the field id from the document's list of field proxies
- var document = this.FieldStore.get(doc.Id) as Document;
- if (document) {
- document._proxies.delete(key.Id);
- }
- }
-
- public static SEND_SET_FIELD(field: Field) {
- // Send a request to set the value of a field
-
- // ...SOCKET(SET_FIELD, field id, serialized field value)
-
- // Server updates the value of the field in its fieldstore
- Utils.Emit(Server.Socket, MessageStore.SetField, field.ToJson());
- }
-}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 847f604d0..a770ccc93 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,20 +1,6 @@
-import { AudioField } from "../../fields/AudioField";
-import { Document } from "../../fields/Document";
-import { Field, Opt } from "../../fields/Field";
-import { HtmlField } from "../../fields/HtmlField";
-import { ImageField } from "../../fields/ImageField";
-import { InkField, StrokeData } from "../../fields/InkField";
-import { Key } from "../../fields/Key";
-import { KeyStore } from "../../fields/KeyStore";
-import { ListField } from "../../fields/ListField";
-import { PDFField } from "../../fields/PDFField";
-import { TextField } from "../../fields/TextField";
-import { VideoField } from "../../fields/VideoField";
-import { WebField } from "../../fields/WebField";
import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { HistogramBox } from "../northstar/dash-nodes/HistogramBox";
import { HistogramOperation } from "../northstar/operations/HistogramOperation";
-import { Server } from "../Server";
import { CollectionPDFView } from "../views/collections/CollectionPDFView";
import { CollectionVideoView } from "../views/collections/CollectionVideoView";
import { CollectionView } from "../views/collections/CollectionView";
@@ -36,44 +22,59 @@ 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 { IconField } from "../../fields/IconFIeld";
+import { Field, Doc, Opt } from "../../new_fields/Doc";
+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 { 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";
export interface DocumentOptions {
x?: number;
y?: number;
- ink?: Map<string, StrokeData>;
+ ink?: InkField;
width?: number;
height?: number;
nativeWidth?: number;
nativeHeight?: number;
title?: string;
- panx?: number;
- pany?: number;
+ panX?: number;
+ panY?: number;
page?: number;
scale?: number;
baseLayout?: string;
layout?: string;
- templates?: Array<Template>;
- layoutKeys?: Key[];
+ templates?: List<string>;
viewType?: number;
backgroundColor?: string;
- copyDraggedItems?: boolean;
+ dropAction?: dropActionType;
+ backgroundLayout?: string;
+ curPage?: number;
documentText?: string;
borderRounding?: number;
- columnsKey?: Array<Key>;
+ schemaColumns?: List<string>;
+ dockingConfig?: string;
+ // [key: string]: Opt<Field>;
}
+const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
-export namespace Documents {
- let textProto: Document;
- let histoProto: Document;
- let imageProto: Document;
- let webProto: Document;
- let collProto: Document;
- let kvpProto: Document;
- let videoProto: Document;
- let audioProto: Document;
- let pdfProto: Document;
- let iconProto: Document;
+export namespace Docs {
+ let textProto: Doc;
+ let histoProto: Doc;
+ let imageProto: Doc;
+ let webProto: Doc;
+ let collProto: Doc;
+ let kvpProto: Doc;
+ let videoProto: Doc;
+ let audioProto: Doc;
+ let pdfProto: Doc;
+ let iconProto: Doc;
const textProtoId = "textProto";
const histoProtoId = "histoProto";
const pdfProtoId = "pdfProto";
@@ -86,129 +87,100 @@ export namespace Documents {
const iconProtoId = "iconProto";
export function initProtos(): Promise<void> {
- return Server.GetFields([textProtoId, histoProtoId, collProtoId, pdfProtoId, imageProtoId, videoProtoId, audioProtoId, webProtoId, kvpProtoId]).then(fields => {
- textProto = fields[textProtoId] as Document || CreateTextPrototype();
- histoProto = fields[histoProtoId] as Document || CreateHistogramPrototype();
- collProto = fields[collProtoId] as Document || CreateCollectionPrototype();
- imageProto = fields[imageProtoId] as Document || CreateImagePrototype();
- webProto = fields[webProtoId] as Document || CreateWebPrototype();
- kvpProto = fields[kvpProtoId] as Document || CreateKVPPrototype();
- videoProto = fields[videoProtoId] as Document || CreateVideoPrototype();
- audioProto = fields[audioProtoId] as Document || CreateAudioPrototype();
- pdfProto = fields[pdfProtoId] as Document || CreatePdfPrototype();
- iconProto = fields[iconProtoId] as Document || CreateIconPrototype();
+ return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => {
+ textProto = fields[textProtoId] as Doc || CreateTextPrototype();
+ histoProto = fields[histoProtoId] as Doc || CreateHistogramPrototype();
+ collProto = fields[collProtoId] as Doc || CreateCollectionPrototype();
+ imageProto = fields[imageProtoId] as Doc || CreateImagePrototype();
+ webProto = fields[webProtoId] as Doc || CreateWebPrototype();
+ kvpProto = fields[kvpProtoId] as Doc || CreateKVPPrototype();
+ videoProto = fields[videoProtoId] as Doc || CreateVideoPrototype();
+ audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
+ pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
+ iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
});
}
- function assignOptions(doc: Document, options: DocumentOptions): Document {
- if (options.nativeWidth !== undefined) { doc.SetNumber(KeyStore.NativeWidth, options.nativeWidth); }
- if (options.nativeHeight !== undefined) { doc.SetNumber(KeyStore.NativeHeight, options.nativeHeight); }
- if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); }
- if (options.page !== undefined) { doc.SetNumber(KeyStore.Page, options.page); }
- if (options.documentText !== undefined) { doc.SetText(KeyStore.DocumentText, options.documentText); }
- if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); }
- if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); }
- if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); }
- if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); }
- if (options.backgroundColor !== undefined) { doc.SetText(KeyStore.BackgroundColor, options.backgroundColor); }
- if (options.ink !== undefined) { doc.Set(KeyStore.Ink, new InkField(options.ink)); }
- if (options.baseLayout !== undefined) { doc.SetText(KeyStore.BaseLayout, options.baseLayout); }
- if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); }
- if (options.templates !== undefined) { doc.Set(KeyStore.Templates, new TemplateField(options.templates)); }
- if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); }
- if (options.copyDraggedItems !== undefined) { doc.SetBoolean(KeyStore.CopyDraggedItems, options.copyDraggedItems); }
- if (options.borderRounding !== undefined) { doc.SetNumber(KeyStore.BorderRounding, options.borderRounding); }
- if (options.columnsKey !== undefined) { doc.SetData(KeyStore.ColumnsKey, options.columnsKey, ListField); }
- return doc;
- }
- function assignToDelegate(doc: Document, options: DocumentOptions): Document {
- if (options.x !== undefined) { doc.SetNumber(KeyStore.X, options.x); }
- if (options.y !== undefined) { doc.SetNumber(KeyStore.Y, options.y); }
- if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); }
- if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); }
- if (options.panx !== undefined) { doc.SetNumber(KeyStore.PanX, options.panx); }
- if (options.pany !== undefined) { doc.SetNumber(KeyStore.PanY, options.pany); }
- return doc;
+ function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Doc {
+ return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout, baseLayout: layout });
}
-
- function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document {
- return assignOptions(new Document(protoId), { ...options, title: title, layout: layout, baseLayout: layout });
+ function SetInstanceOptions<U extends Field>(doc: Doc, options: DocumentOptions, value: U) {
+ const deleg = Doc.MakeDelegate(doc);
+ deleg.data = value;
+ return Doc.assign(deleg, options);
}
- function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: [T, { new(): U }] | Document, id?: string) {
- var deleg = doc.MakeDelegate(id);
- if (value instanceof Document) {
- deleg.Set(KeyStore.Data, value);
- }
- else {
- deleg.SetData(KeyStore.Data, value[0], value[1]);
- }
- return assignOptions(deleg, options);
+ function SetDelegateOptions<U extends Field>(doc: Doc, options: DocumentOptions) {
+ const deleg = Doc.MakeDelegate(doc);
+ return Doc.assign(deleg, options);
}
- function CreateImagePrototype(): Document {
- let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
- imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString());
- imageProto.SetNumber(KeyStore.CurPage, 0);
- imageProto.SetData(KeyStore.LayoutFields, [KeyStore.Title], ListField);
+ function CreateImagePrototype(): Doc {
+ let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"),
+ { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0 });
return imageProto;
}
- function CreateHistogramPrototype(): Document {
- let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
- histoProto.SetText(KeyStore.BackgroundLayout, HistogramBox.LayoutString());
+ function CreateHistogramPrototype(): Doc {
+ let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"),
+ { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString() });
return histoProto;
}
- function CreateIconPrototype(): Document {
+ function CreateIconPrototype(): Doc {
let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(),
- { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
return iconProto;
}
- function CreateTextPrototype(): Document {
+ function CreateTextPrototype(): Doc {
let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: 300, height: 150, backgroundColor: "#f1efeb" });
return textProto;
}
- function CreatePdfPrototype(): Document {
- let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, nativeWidth: 1200, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
- pdfProto.SetNumber(KeyStore.CurPage, 1);
- pdfProto.SetText(KeyStore.BackgroundLayout, PDFBox.LayoutString());
+ function CreatePdfPrototype(): Doc {
+ let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"),
+ { x: 0, y: 0, nativeWidth: 1200, width: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1 });
return pdfProto;
}
- function CreateWebPrototype(): Document {
+ function CreateWebPrototype(): Doc {
let webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: 300, height: 300 });
return webProto;
}
- function CreateCollectionPrototype(): Document {
- let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("DataKey"),
- { panx: 0, pany: 0, scale: 1, width: 500, height: 500, layoutKeys: [KeyStore.Data] });
+ function CreateCollectionPrototype(): Doc {
+ let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"),
+ { panX: 0, panY: 0, scale: 1, width: 500, height: 500 });
return collProto;
}
- function CreateKVPPrototype(): Document {
+ function CreateKVPPrototype(): Doc {
let kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: 300, height: 150 });
return kvpProto;
}
- function CreateVideoPrototype(): Document {
- let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
- videoProto.SetNumber(KeyStore.CurPage, 0);
- videoProto.SetText(KeyStore.BackgroundLayout, VideoBox.LayoutString());
+ function CreateVideoPrototype(): Doc {
+ let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"),
+ { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0 });
return videoProto;
}
- function CreateAudioPrototype(): Document {
+ function CreateAudioPrototype(): Doc {
let audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: 300, height: 150 });
return audioProto;
}
+ function CreateInstance(proto: Doc, data: Field, options: DocumentOptions) {
+ const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
+ if (!("author" in protoProps)) {
+ protoProps.author = CurrentUserUtils.email;
+ }
+ if (!("creationDate" in protoProps)) {
+ protoProps.creationDate = new DateField;
+ }
+
+ return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps);
+ }
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(imageProto, options, [new URL(url), ImageField]).MakeDelegate(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
+ return CreateInstance(imageProto, new ImageField(new URL(url)), options);
// let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
// [new URL(url), ImageField]);
// doc.SetText(KeyStore.Caption, "my caption...");
@@ -217,79 +189,83 @@ export namespace Documents {
// return doc;
}
export function VideoDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(videoProto, options, [new URL(url), VideoField]), options);
+ return CreateInstance(videoProto, new VideoField(new URL(url)), options);
}
export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(audioProto, options, [new URL(url), AudioField]), options);
+ return CreateInstance(audioProto, new AudioField(new URL(url)), options);
}
- export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}, id?: string, delegId?: string) {
- return assignToDelegate(SetInstanceOptions(histoProto, options, [histoOp, HistogramField], id).MakeDelegate(delegId), options);
+ export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
+ return CreateInstance(histoProto, new HistogramField(histoOp), options);
}
export function TextDocument(options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(textProto, options, ["", TextField]).MakeDelegate(), options);
+ return CreateInstance(textProto, "", options);
}
export function IconDocument(icon: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(iconProto, { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), layoutKeys: [KeyStore.Data], layout: IconBox.LayoutString(), ...options }, [icon, IconField]), options);
+ return CreateInstance(iconProto, new IconField(icon), options);
}
export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(pdfProto, options, [new URL(url), PDFField]).MakeDelegate(), options);
+ return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
}
export async function DBDocument(url: string, options: 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 = Documents.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
- let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]);
+ let schemaDoc = Docs.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
+ let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc));
+ if (!schemaDocuments) {
+ return;
+ }
+ const docs = schemaDocuments;
CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
- Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => {
- if (field instanceof Document) {
- schemaDocuments.push(field);
+ DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+ if (field instanceof Doc) {
+ docs.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(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias"));
+ docs.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
}
}));
});
return schemaDoc;
}
- return Documents.TreeDocument([], { width: 50, height: 100, title: schemaName });
+ return Docs.TreeDocument([], { width: 50, height: 100, title: schemaName });
}
export function WebDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(webProto, options, [new URL(url), WebField]).MakeDelegate(), options);
+ return CreateInstance(webProto, new WebField(new URL(url)), options);
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(webProto, options, [html, HtmlField]).MakeDelegate(), options);
+ return CreateInstance(webProto, new HtmlField(html), options);
}
- export function KVPDocument(document: Document, options: DocumentOptions = {}, id?: string) {
- return assignToDelegate(SetInstanceOptions(kvpProto, options, document, id), options);
+ export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
+ return CreateInstance(kvpProto, document, options);
}
- export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string, makePrototype: boolean = true) {
+ export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) {
if (!makePrototype) {
- return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id);
+ return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, new List(documents));
}
- return assignToDelegate(SetInstanceOptions(collProto, { columnsKey: [KeyStore.Title], ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id).MakeDelegate(), options);
+ return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
}
- export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) {
- return assignToDelegate(SetInstanceOptions(collProto, { columnsKey: [KeyStore.Title], ...options, viewType: CollectionViewType.Schema }, [documents, ListField], id), options);
+ export function SchemaDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Schema });
}
- export function TreeDocument(documents: Array<Document>, options: DocumentOptions, id?: string) {
- return assignToDelegate(SetInstanceOptions(collProto, { columnsKey: [KeyStore.Title], ...options, viewType: CollectionViewType.Tree }, [documents, ListField], id), options);
+ 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(config: string, options: DocumentOptions, id?: string) {
- return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Docking }, [config, TextField], id), options);
+ export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions) {
+ return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config });
}
- export function CaptionDocument(doc: Document) {
- const captionDoc = doc.CreateAlias();
- captionDoc.SetText(KeyStore.OverlayLayout, FixedCaption());
- captionDoc.SetNumber(KeyStore.Width, doc.GetNumber(KeyStore.Width, 0));
- captionDoc.SetNumber(KeyStore.Height, doc.GetNumber(KeyStore.Height, 0));
+ export function CaptionDocument(doc: Doc) {
+ const captionDoc = Doc.MakeAlias(doc);
+ captionDoc.overlayLayout = FixedCaption();
+ captionDoc.width = Cast(doc.width, "number", 0);
+ captionDoc.height = Cast(doc.height, "number", 0);
return captionDoc;
}
@@ -300,14 +276,14 @@ export namespace Documents {
+ ImageBox.LayoutString() +
`</div>
<div style="position:relative; height:15%; text-align:center; ">`
- + FormattedTextBox.LayoutString("CaptionKey") +
+ + FormattedTextBox.LayoutString("caption") +
`</div>
</div>`;
}
- export function FixedCaption(fieldName: string = "Caption") {
+ export function FixedCaption(fieldName: string = "caption") {
return `<div style="position:absolute; height:30px; bottom:0; width:100%">
<div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`
- + FormattedTextBox.LayoutString(fieldName + "Key") +
+ + FormattedTextBox.LayoutString(fieldName) +
`</div>
</div>`;
}
@@ -319,7 +295,7 @@ export namespace Documents {
{layout}
</div>
<div style="height:(100% + 25px); width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"CaptionKey"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
</div>
</div>
`);
@@ -331,7 +307,7 @@ export namespace Documents {
{layout}
</div>
<div style="height:25px; width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"CaptionKey"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
</div>
</div>
`);
@@ -354,7 +330,7 @@ export namespace Documents {
{layout}
</div>
<div style="height:15%; width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"CaptionKey"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
</div>
</div>
`);
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
new file mode 100644
index 000000000..56a71f1ac
--- /dev/null
+++ b/src/client/goldenLayout.js
@@ -0,0 +1,5359 @@
+(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.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[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/core/brusher/IBaseBrushable.ts b/src/client/northstar/core/brusher/IBaseBrushable.ts
index c46db4d22..87f4ba413 100644
--- a/src/client/northstar/core/brusher/IBaseBrushable.ts
+++ b/src/client/northstar/core/brusher/IBaseBrushable.ts
@@ -1,9 +1,9 @@
import { PIXIPoint } from '../../utils/MathUtil';
import { IEquatable } from '../../utils/IEquatable';
-import { Document } from '../../../../fields/Document';
+import { Doc } from '../../../../new_fields/Doc';
export interface IBaseBrushable<T> extends IEquatable {
- BrusherModels: Array<Document>;
+ BrusherModels: Array<Doc>;
BrushColors: Array<number>;
Position: PIXIPoint;
Size: PIXIPoint;
diff --git a/src/client/northstar/core/filter/FilterModel.ts b/src/client/northstar/core/filter/FilterModel.ts
index e2ba3f652..6ab96b33d 100644
--- a/src/client/northstar/core/filter/FilterModel.ts
+++ b/src/client/northstar/core/filter/FilterModel.ts
@@ -2,10 +2,9 @@ import { ValueComparison } from "./ValueComparision";
import { Utils } from "../../utils/Utils";
import { IBaseFilterProvider } from "./IBaseFilterProvider";
import { FilterOperand } from "./FilterOperand";
-import { KeyStore } from "../../../../fields/KeyStore";
-import { FieldWaiting } from "../../../../fields/Field";
-import { Document } from "../../../../fields/Document";
import { HistogramField } from "../../dash-fields/HistogramField";
+import { Cast, FieldValue } from "../../../../new_fields/Types";
+import { Doc } from "../../../../new_fields/Doc";
export class FilterModel {
public ValueComparisons: ValueComparison[];
@@ -52,12 +51,12 @@ export class FilterModel {
let children = new Array<string>();
let linkedGraphNodes = baseOperation.Links;
linkedGraphNodes.map(linkVm => {
- let filterDoc = linkVm.Get(KeyStore.LinkedFromDocs);
- if (filterDoc && filterDoc !== FieldWaiting && filterDoc instanceof Document) {
- let filterHistogram = filterDoc.GetT(KeyStore.Data, HistogramField);
- if (filterHistogram && filterHistogram !== FieldWaiting) {
- if (!visitedFilterProviders.has(filterHistogram.Data)) {
- let child = FilterModel.GetFilterModelsRecursive(filterHistogram.Data, visitedFilterProviders, filterModels, false);
+ let filterDoc = FieldValue(Cast(linkVm.linkedFrom, Doc));
+ if (filterDoc) {
+ let filterHistogram = Cast(filterDoc.data, HistogramField);
+ if (filterHistogram) {
+ if (!visitedFilterProviders.has(filterHistogram.HistoOp)) {
+ let child = FilterModel.GetFilterModelsRecursive(filterHistogram.HistoOp, visitedFilterProviders, filterModels, false);
if (child !== "") {
// if (linkVm.IsInverted) {
// child = "! " + child;
diff --git a/src/client/northstar/core/filter/IBaseFilterConsumer.ts b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
index 59d7adf4c..e7549d113 100644
--- a/src/client/northstar/core/filter/IBaseFilterConsumer.ts
+++ b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
@@ -1,10 +1,10 @@
import { FilterOperand } from '../filter/FilterOperand';
import { IEquatable } from '../../utils/IEquatable';
-import { Document } from "../../../../fields/Document";
+import { Doc } from '../../../../new_fields/Doc';
export interface IBaseFilterConsumer extends IEquatable {
FilterOperand: FilterOperand;
- Links: Document[];
+ Links: Doc[];
}
export function instanceOfIBaseFilterConsumer(object: any): object is IBaseFilterConsumer {
diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
index c699691a4..f01f08487 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -1,64 +1,55 @@
-import { action } from "mobx";
+import { observable } from "mobx";
+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 { BasicField } from "../../../fields/BasicField";
-import { Field, FieldId } from "../../../fields/Field";
+import { ObjectField, Copy } from "../../../new_fields/ObjectField";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { Types } from "../../../server/Message";
import { OmitKeys } from "../../../Utils";
-
-
-export class HistogramField extends BasicField<HistogramOperation> {
- constructor(data?: HistogramOperation, id?: FieldId, save: boolean = true) {
- super(data ? data : HistogramOperation.Empty, save, id);
- }
-
- toString(): string {
- return JSON.stringify(OmitKeys(this.Data, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']));
- }
-
- Copy(): Field {
- return new HistogramField(this.Data.Copy());
+import { Deserializable } from "../../util/SerializationHelper";
+
+function serialize(field: HistogramField) {
+ return OmitKeys(field.HistoOp, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit;
+}
+
+function deserialize(jp: any) {
+ let X: AttributeTransformationModel | undefined;
+ let Y: AttributeTransformationModel | undefined;
+ let V: AttributeTransformationModel | undefined;
+
+ let schema = CurrentUserUtils.GetNorthstarSchema(jp.SchemaName);
+ if (schema) {
+ CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
+ if (attr.displayName === jp.X.AttributeModel.Attribute.DisplayName) {
+ X = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.X.AggregateFunction);
+ }
+ if (attr.displayName === jp.Y.AttributeModel.Attribute.DisplayName) {
+ Y = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.Y.AggregateFunction);
+ }
+ if (attr.displayName === jp.V.AttributeModel.Attribute.DisplayName) {
+ V = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.V.AggregateFunction);
+ }
+ });
+ if (X && Y && V) {
+ return new HistogramField(new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization));
+ }
}
-
- ToScriptString(): string {
- return `new HistogramField("${this.Data}")`;
+ return new HistogramField(HistogramOperation.Empty);
+}
+
+@Deserializable("histogramField")
+export class HistogramField extends ObjectField {
+ @serializable(custom(serialize, deserialize)) @observable public readonly HistoOp: HistogramOperation;
+ constructor(data?: HistogramOperation) {
+ super();
+ this.HistoOp = data ? data : HistogramOperation.Empty;
}
-
- ToJson() {
- return {
- type: Types.HistogramOp,
- data: this.toString(),
- id: this.Id
- };
+ toString(): string {
+ return JSON.stringify(OmitKeys(this.HistoOp, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit);
}
- @action
- static FromJson(id: string, data: any): HistogramField {
- let jp = JSON.parse(data);
- let X: AttributeTransformationModel | undefined;
- let Y: AttributeTransformationModel | undefined;
- let V: AttributeTransformationModel | undefined;
-
- let schema = CurrentUserUtils.GetNorthstarSchema(jp.SchemaName);
- if (schema) {
- CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
- if (attr.displayName === jp.X.AttributeModel.Attribute.DisplayName) {
- X = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.X.AggregateFunction);
- }
- if (attr.displayName === jp.Y.AttributeModel.Attribute.DisplayName) {
- Y = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.Y.AggregateFunction);
- }
- if (attr.displayName === jp.V.AttributeModel.Attribute.DisplayName) {
- V = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.V.AggregateFunction);
- }
- });
- if (X && Y && V) {
- return new HistogramField(new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization), id, false);
- }
- }
- return new HistogramField(HistogramOperation.Empty, id, false);
+ [Copy]() {
+ return new HistogramField(this.HistoOp.Copy());
}
} \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index ac5f3c8cf..765ecf8f0 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -1,9 +1,6 @@
import React = require("react");
import { action, computed, observable, reaction, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
-import { FieldWaiting, Opt } from "../../../fields/Field";
-import { Document } from "../../../fields/Document";
-import { KeyStore } from "../../../fields/KeyStore";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange';
import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper";
@@ -20,11 +17,15 @@ 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";
@observer
export class HistogramBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr); }
+ public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(HistogramBox, fieldStr); }
private _dropXRef = React.createRef<HTMLDivElement>();
private _dropYRef = React.createRef<HTMLDivElement>();
private _dropXDisposer?: DragManager.DragDropDisposer;
@@ -47,9 +48,9 @@ export class HistogramBox extends React.Component<FieldViewProps> {
@action
dropX = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
- let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
- if (h && h !== FieldWaiting) {
- this.HistoOp.X = h.Data.X;
+ let h = Cast(de.data.draggedDocuments[0].data, HistogramField);
+ if (h) {
+ this.HistoOp.X = h.HistoOp.X;
}
e.stopPropagation();
e.preventDefault();
@@ -58,9 +59,9 @@ export class HistogramBox extends React.Component<FieldViewProps> {
@action
dropY = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
- let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
- if (h && h !== FieldWaiting) {
- this.HistoOp.Y = h.Data.X;
+ let h = Cast(de.data.draggedDocuments[0].data, HistogramField);
+ if (h) {
+ this.HistoOp.Y = h.HistoOp.X;
}
e.stopPropagation();
e.preventDefault();
@@ -110,25 +111,28 @@ export class HistogramBox extends React.Component<FieldViewProps> {
}
}
- activateHistogramOperation(catalog?: Catalog) {
+ async activateHistogramOperation(catalog?: Catalog) {
if (catalog) {
- this.props.Document.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => {
- this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty;
+ let histoOp = await Cast(this.props.Document[this.props.fieldKey], HistogramField);
+ runInAction(() => {
+ this.HistoOp = histoOp ? histoOp.HistoOp : HistogramOperation.Empty;
if (this.HistoOp !== HistogramOperation.Empty) {
- reaction(() => this.props.Document.GetList(KeyStore.LinkedFromDocs, [] as Document[]), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
- reaction(() => this.props.Document.GetList(KeyStore.BrushingDocs, []).length,
+ 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 = this.props.Document.GetList(KeyStore.BrushingDocs, [] as Document[]);
- let proto = this.props.Document.GetPrototype() as Document;
- this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => {
- brush.SetNumber(KeyStore.BackgroundColor, StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length]);
- let brushed = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
- return { l: brush, b: brushed[0].Id === proto.Id ? brushed[1] : brushed[0] };
- }));
+ let brushingDocs = Cast(this.props.Document.brushingDocs, listSpec(Doc), []);
+ 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), []);
+ return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] };
+ }));
+ }
}, { fireImmediately: true });
reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
}
- }));
+ });
}
}
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index 6a8c9d8cf..5c9c832c0 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -1,7 +1,4 @@
import { action, computed, observable, trace } from "mobx";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { ColumnAttributeModel } from "../core/attribute/AttributeModel";
import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
@@ -16,12 +13,14 @@ import { AggregateFunction, AggregateParameters, Attribute, AverageAggregatePara
import { ModelHelpers } from "../model/ModelHelpers";
import { ArrayUtil } from "../utils/ArrayUtil";
import { BaseOperation } from "./BaseOperation";
+import { Doc } from "../../../new_fields/Doc";
+import { Cast, NumCast } from "../../../new_fields/Types";
export class HistogramOperation extends BaseOperation implements IBaseFilterConsumer, IBaseFilterProvider {
public static Empty = new HistogramOperation("-empty schema-", new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())));
@observable public FilterOperand: FilterOperand = FilterOperand.AND;
- @observable public Links: Document[] = [];
- @observable public BrushLinks: { l: Document, b: Document }[] = [];
+ @observable public Links: Doc[] = [];
+ @observable public BrushLinks: { l: Doc, b: Doc }[] = [];
@observable public BrushColors: number[] = [];
@observable public BarFilterModels: FilterModel[] = [];
@@ -77,10 +76,10 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
trace();
let brushes: string[] = [];
this.BrushLinks.map(brushLink => {
- let brushHistogram = brushLink.b.GetT(KeyStore.Data, HistogramField);
- if (brushHistogram && brushHistogram !== FieldWaiting) {
+ let brushHistogram = Cast(brushLink.b.data, HistogramField);
+ if (brushHistogram) {
let filterModels: FilterModel[] = [];
- brushes.push(FilterModel.GetFilterModelsRecursive(brushHistogram.Data, new Set<IBaseFilterProvider>(), filterModels, false));
+ brushes.push(FilterModel.GetFilterModelsRecursive(brushHistogram.HistoOp, new Set<IBaseFilterProvider>(), filterModels, false));
}
});
return brushes;
@@ -148,7 +147,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
@action
public async Update(): Promise<void> {
- this.BrushColors = this.BrushLinks.map(e => e.l.GetNumber(KeyStore.BackgroundColor, 0));
+ this.BrushColors = this.BrushLinks.map(e => NumCast(e.l.backgroundColor));
return super.Update();
}
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 56669fb79..69964e2c9 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,9 +1,8 @@
import { computed, observable } from 'mobx';
-import { Document } from "../../fields/Document";
-import { FieldWaiting } from '../../fields/Field';
-import { KeyStore } from '../../fields/KeyStore';
-import { ListField } from '../../fields/ListField';
import { DocumentView } from '../views/nodes/DocumentView';
+import { Doc } from '../../new_fields/Doc';
+import { FieldValue, Cast } from '../../new_fields/Types';
+import { listSpec } from '../../new_fields/Schema';
export class DocumentManager {
@@ -25,26 +24,21 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
- public getDocumentView(toFind: Document): DocumentView | null {
+ public getDocumentView(toFind: Doc): DocumentView | null {
- let toReturn: DocumentView | null;
- toReturn = null;
+ let toReturn: DocumentView | null = null;
//gets document view that is in a freeform canvas collection
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document;
-
- if (doc === toFind) {
+ if (view.props.Document === toFind) {
toReturn = view;
return;
}
});
if (!toReturn) {
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document;
-
- let docSrc = doc.GetT(KeyStore.Prototype, Document);
- if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) {
+ let doc = view.props.Document.proto;
+ if (doc && Object.is(doc, toFind)) {
toReturn = view;
}
});
@@ -52,7 +46,7 @@ export class DocumentManager {
return toReturn;
}
- public getDocumentViews(toFind: Document): DocumentView[] {
+ public getDocumentViews(toFind: Doc): DocumentView[] {
let toReturn: DocumentView[] = [];
@@ -64,8 +58,8 @@ export class DocumentManager {
if (doc === toFind) {
toReturn.push(view);
} else {
- let docSrc = doc.GetT(KeyStore.Prototype, Document);
- if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) {
+ let docSrc = FieldValue(doc.proto);
+ if (docSrc && Object.is(docSrc, toFind)) {
toReturn.push(view);
}
}
@@ -77,20 +71,20 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
- let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField);
- if (linksList && linksList !== FieldWaiting && linksList.Data.length) {
- pairs.push(...linksList.Data.reduce((pairs, link) => {
- if (link instanceof Document) {
- let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
- if (linkToDoc && linkToDoc !== FieldWaiting) {
+ let linksList = Cast(dv.props.Document.linkedToDocs, listSpec(Doc));
+ if (linksList && linksList.length) {
+ pairs.push(...linksList.reduce((pairs, link) => {
+ if (link) {
+ let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
+ if (linkToDoc) {
DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
pairs.push({ a: dv, b: docView1, l: link }));
}
}
return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Document }[]));
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
}
return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Document }[]);
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
}
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 136852e12..a3dbe6e43 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,14 +1,14 @@
import { action } from "mobx";
-import { Document } from "../../fields/Document";
-import { FieldWaiting } from "../../fields/Field";
-import { KeyStore } from "../../fields/KeyStore";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { DocumentDecorations } from "../views/DocumentDecorations";
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 function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, moveFunc?: DragManager.MoveFunction, copyOnDrop: boolean = false) {
+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 => {
e.stopPropagation();
e.preventDefault();
@@ -16,7 +16,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
var dragData = new DragManager.DocumentDragData([docFunc()]);
- dragData.copyOnDrop = copyOnDrop;
+ dragData.dropAction = dropAction;
dragData.moveDocument = moveFunc;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
});
@@ -40,19 +40,19 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
return onItemDown;
}
-export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Document) {
- let srcTarg = sourceDoc.GetPrototype();
+export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
+ let srcTarg = sourceDoc.proto;
let draggedDocs = srcTarg ?
- srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc =>
- (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : [];
+ Cast(srcTarg.linkedToDocs, listSpec(Doc), []).map(linkDoc =>
+ Cast(linkDoc.linkedTo, Doc) as Doc) : [];
let draggedFromDocs = srcTarg ?
- srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc =>
- (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : [];
+ Cast(srcTarg.linkedFromDocs, listSpec(Doc), []).map(linkDoc =>
+ Cast(linkDoc.linkedFrom, Doc) as Doc) : [];
draggedDocs.push(...draggedFromDocs);
if (draggedDocs.length) {
- let moddrag = [] as Document[];
+ let moddrag: Doc[] = [];
for (const draggedDoc of draggedDocs) {
- let doc = await draggedDoc.GetTAsync(KeyStore.AnnotationOn, Document);
+ let doc = await Cast(draggedDoc.annotationOn, Doc);
if (doc) moddrag.push(doc);
}
let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
@@ -134,37 +134,42 @@ export namespace DragManager {
};
}
- export type MoveFunction = (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
export class DocumentDragData {
- constructor(dragDoc: Document[]) {
+ constructor(dragDoc: Doc[]) {
this.draggedDocuments = dragDoc;
this.droppedDocuments = dragDoc;
this.xOffset = 0;
this.yOffset = 0;
}
- draggedDocuments: Document[];
- droppedDocuments: Document[];
+ draggedDocuments: Doc[];
+ droppedDocuments: Doc[];
xOffset: number;
yOffset: number;
- aliasOnDrop?: boolean;
- copyOnDrop?: boolean;
+ dropAction: dropActionType;
+ userDropAction: dropActionType;
moveDocument?: MoveFunction;
[id: string]: any;
}
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag(eles, dragData, downX, downY, options,
- (dropData: { [id: string]: any }) => (dropData.droppedDocuments = dragData.aliasOnDrop ? dragData.draggedDocuments.map(d => d.CreateAlias()) : dragData.copyOnDrop ? dragData.draggedDocuments.map(d => d.Copy(true) as Document) : dragData.draggedDocuments));
+ (dropData: { [id: string]: any }) =>
+ (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.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
+ dragData.draggedDocuments));
}
export class LinkDragData {
- constructor(linkSourceDoc: Document, blacklist: Document[] = []) {
+ constructor(linkSourceDoc: Doc, blacklist: Doc[] = []) {
this.linkSourceDocument = linkSourceDoc;
this.blacklist = blacklist;
}
- droppedDocuments: Document[] = [];
- linkSourceDocument: Document;
- blacklist: Document[];
+ droppedDocuments: Doc[] = [];
+ linkSourceDocument: Doc;
+ blacklist: Doc[];
[id: string]: any;
}
@@ -188,7 +193,7 @@ export namespace DragManager {
let xs: number[] = [];
let ys: number[] = [];
- const docs: Document[] =
+ const docs: Doc[] =
dragData instanceof DocumentDragData ? dragData.draggedDocuments : [];
let dragElements = eles.map(ele => {
const w = ele.offsetWidth,
@@ -252,7 +257,7 @@ export namespace DragManager {
e.stopPropagation();
e.preventDefault();
if (dragData instanceof DocumentDragData) {
- dragData.aliasOnDrop = e.ctrlKey || e.altKey;
+ dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
}
if (e.shiftKey) {
AbortDrag();
@@ -301,15 +306,20 @@ export namespace DragManager {
function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
let removed = dragEles.map(dragEle => {
- let parent = dragEle.parentElement;
- if (parent) parent.removeChild(dragEle);
- return [dragEle, parent];
+ // let parent = dragEle.parentElement;
+ // if (parent) parent.removeChild(dragEle);
+ let ret = [dragEle, dragEle.style.width, dragEle.style.height];
+ dragEle.style.width = "0";
+ dragEle.style.height = "0";
+ return ret;
});
const target = document.elementFromPoint(e.x, e.y);
removed.map(r => {
- let dragEle = r[0];
- let parent = r[1];
- if (parent && dragEle) parent.appendChild(dragEle);
+ let dragEle = r[0] as HTMLElement;
+ dragEle.style.width = r[1] as string;
+ dragEle.style.height = r[2] as string;
+ // let parent = r[1];
+ // if (parent && dragEle) parent.appendChild(dragEle);
});
if (target) {
if (finishDrag) finishDrag(dragData);
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c67cc067a..e45f61c11 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,13 +1,5 @@
// import * as ts from "typescript"
let ts = (window as any).ts;
-import { Opt, Field } from "../../fields/Field";
-import { Document } from "../../fields/Document";
-import { NumberField } from "../../fields/NumberField";
-import { ImageField } from "../../fields/ImageField";
-import { TextField } from "../../fields/TextField";
-import { RichTextField } from "../../fields/RichTextField";
-import { KeyStore } from "../../fields/KeyStore";
-import { ListField } from "../../fields/ListField";
// // @ts-ignore
// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
// // @ts-ignore
@@ -15,8 +7,11 @@ import { ListField } from "../../fields/ListField";
// @ts-ignore
import * as typescriptlib from '!!raw-loader!./type_decls.d';
-import { Documents } from "../documents/Documents";
-import { Key } from "../../fields/Key";
+import { Docs } from "../documents/Documents";
+import { Doc, Field } from '../../new_fields/Doc';
+import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField';
+import { List } from '../../new_fields/List';
+import { RichTextField } from '../../new_fields/RichTextField';
export interface ScriptSucccess {
success: true;
@@ -50,9 +45,9 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
return { compiled: false, errors: diagnostics };
}
- let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField, Key];
- let paramNames = ["KeyStore", "Documents", ...fieldTypes.map(fn => fn.name)];
- let params: any[] = [KeyStore, Documents, ...fieldTypes];
+ let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField];
+ let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
+ let params: any[] = [Docs, ...fieldTypes];
let compiledFunction = new Function(...paramNames, `return ${script}`);
let { capturedVariables = {} } = options;
let run = (args: { [name: string]: any } = {}): ScriptResult => {
@@ -171,17 +166,4 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
return Run(outputText, paramNames, diagnostics, script, options);
-}
-
-export function OrLiteralType(returnType: string): string {
- return `${returnType} | string | number`;
-}
-
-export function ToField(data: any): Opt<Field> {
- if (typeof data === "string") {
- return new TextField(data);
- } else if (typeof data === "number") {
- return new NumberField(data);
- }
- return undefined;
} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index da66bf3fc..fe5acf4b4 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,5 +1,5 @@
-import { action, observable } from "mobx";
-import { Document } from "../../fields/Document";
+import { observable, action } from "mobx";
+import { Doc } from "../../new_fields/Doc";
import { MainOverlayTextBox } from "../views/MainOverlayTextBox";
import { DocumentView } from "../views/nodes/DocumentView";
@@ -49,7 +49,7 @@ export namespace SelectionManager {
return manager.SelectedDocuments.indexOf(doc) !== -1;
}
- export function DeselectAll(except?: Document): void {
+ export function DeselectAll(except?: Doc): void {
let found: DocumentView | undefined = undefined;
if (except) {
for (const view of manager.SelectedDocuments) {
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
new file mode 100644
index 000000000..7ded85e43
--- /dev/null
+++ b/src/client/util/SerializationHelper.ts
@@ -0,0 +1,130 @@
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
+import { Field } from "../../new_fields/Doc";
+
+export namespace SerializationHelper {
+ let serializing: number = 0;
+ export function IsSerializing() {
+ return serializing > 0;
+ }
+
+ export function Serialize(obj: Field): any {
+ if (obj === undefined || obj === null) {
+ return undefined;
+ }
+
+ if (typeof obj !== 'object') {
+ return obj;
+ }
+
+ serializing += 1;
+ if (!(obj.constructor.name in reverseMap)) {
+ throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
+ }
+
+ const json = serialize(obj);
+ json.__type = reverseMap[obj.constructor.name];
+ serializing -= 1;
+ return json;
+ }
+
+ export function Deserialize(obj: any): any {
+ if (obj === undefined || obj === null) {
+ return undefined;
+ }
+
+ if (typeof obj !== 'object') {
+ return obj;
+ }
+
+ serializing += 1;
+ if (!obj.__type) {
+ throw Error("No property 'type' found in JSON.");
+ }
+
+ if (!(obj.__type in serializationTypes)) {
+ throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
+ }
+
+ const value = deserialize(serializationTypes[obj.__type], obj);
+ serializing -= 1;
+ return value;
+ }
+}
+
+let serializationTypes: { [name: string]: any } = {};
+let reverseMap: { [ctor: string]: string } = {};
+
+export interface DeserializableOpts {
+ (constructor: { new(...args: any[]): any }): void;
+ withFields(fields: string[]): Function;
+}
+
+export function Deserializable(name: string): DeserializableOpts;
+export function Deserializable(constructor: { new(...args: any[]): any }): void;
+export function Deserializable(constructor: { new(...args: any[]): any } | string): DeserializableOpts | void {
+ function addToMap(name: string, ctor: { new(...args: any[]): any }) {
+ const schema = getDefaultModelSchema(ctor) as any;
+ if (schema.targetClass !== ctor) {
+ const newSchema = { ...schema, factory: () => new ctor() };
+ setDefaultModelSchema(ctor, newSchema);
+ }
+ if (!(name in serializationTypes)) {
+ serializationTypes[name] = ctor;
+ reverseMap[ctor.name] = name;
+ } else {
+ throw new Error(`Name ${name} has already been registered as deserializable`);
+ }
+ }
+ if (typeof constructor === "string") {
+ return Object.assign((ctor: { new(...args: any[]): any }) => {
+ addToMap(constructor, ctor);
+ }, { withFields: Deserializable.withFields });
+ }
+ addToMap(constructor.name, constructor);
+}
+
+export namespace Deserializable {
+ export function withFields(fields: string[]) {
+ return function (constructor: { new(...fields: any[]): any }) {
+ Deserializable(constructor);
+ let schema = getDefaultModelSchema(constructor);
+ if (schema) {
+ schema.factory = context => {
+ const args = fields.map(key => context.json[key]);
+ return new constructor(...args);
+ };
+ // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing
+ // fields.forEach(field => {
+ // if (field in schema.props) {
+ // let propSchema = schema.props[field];
+ // if (propSchema === false) {
+ // return;
+ // } else if (propSchema === true) {
+ // propSchema = primitive();
+ // }
+ // schema.props[field] = custom(propSchema.serializer,
+ // () => {
+ // return SKIP;
+ // });
+ // }
+ // });
+ } else {
+ schema = {
+ props: {},
+ factory: context => {
+ const args = fields.map(key => context.json[key]);
+ return new constructor(...args);
+ }
+ };
+ setDefaultModelSchema(constructor, schema);
+ }
+ };
+ }
+}
+
+export function autoObject(): PropSchema {
+ return custom(
+ (s) => SerializationHelper.Serialize(s),
+ (s) => SerializationHelper.Deserialize(s)
+ );
+} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 14af4bdfd..68a73375e 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -38,9 +38,9 @@ export class TooltipTextMenu {
private listTypeToIcon: Map<NodeType, string>;
private fontSizeIndicator: HTMLSpanElement = document.createElement("span");
//dropdown doms
- private fontSizeDom: Node;
- private fontStyleDom: Node;
- private listTypeBtnDom: Node;
+ private fontSizeDom?: Node;
+ private fontStyleDom?: Node;
+ private listTypeBtnDom?: Node;
constructor(view: EditorView, editorProps: FieldViewProps) {
this.view = view;
@@ -321,7 +321,7 @@ export class TooltipTextMenu {
this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
//UPDATE LIST ITEM DROPDOWN
- this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom);
+ this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom!);
//UPDATE FONT STYLE DROPDOWN
let activeStyles = this.activeMarksOnSelection(this.fontStyles);
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 27aed4bac..0b5280c4a 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,4 +1,4 @@
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import 'source-map-support/register';
import { Without } from "../../Utils";
import { string } from "prop-types";
@@ -140,10 +140,11 @@ export namespace UndoManager {
}
});
- export function RunInBatch(fn: () => void, batchName: string) {
+ //TODO Make this return the return value
+ export function RunInBatch<T>(fn: () => T, batchName: string) {
let batch = StartBatch(batchName);
try {
- fn();
+ return runInAction(fn);
} finally {
batch.end();
}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
new file mode 100644
index 000000000..d6562492f
--- /dev/null
+++ b/src/client/views/DocComponent.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import { Doc } from '../../new_fields/Doc';
+import { computed } from 'mobx';
+
+export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
+ class Component extends React.Component<P> {
+ //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+ @computed
+ get Document(): T {
+ return schemaCtor(this.props.Document);
+ }
+ }
+ return Component;
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 0dfa6f0ff..158b02b5a 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,5 +1,6 @@
@import "globalCssVariables";
+$linkGap : 3px;
.documentDecorations {
position: absolute;
}
@@ -72,6 +73,7 @@
}
}
+
.documentDecorations-closeButton {
background: $alt-accent;
opacity: 0.8;
@@ -104,35 +106,45 @@
}
.linkFlyout {
+ grid-column: 2/4;
+ margin-top: $linkGap;
+}
+
+.linkButton-empty:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.linkButton-nonempty:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.link-button-container {
grid-column: 1/4;
- margin-left: 25px;
+ width: auto;
+ height: auto;
+ display: flex;
+ flex-direction: row;
}
.linkButton-linker {
- position: absolute;
- bottom: 0px;
- left: 0px;
+ margin-left: 5px;
+ margin-top: $linkGap;
height: 20px;
width: 20px;
- margin-top: 10px;
- margin-right: 5px;
+ text-align: center;
border-radius: 50%;
- opacity: 0.9;
pointer-events: auto;
color: $dark-color;
border: $dark-color 1px solid;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 75%;
- transition: transform 0.2s;
- text-align: center;
- display: flex;
- justify-content: center;
- align-items: center;
}
-.linkButton-tray {
- grid-column: 1/4;
+.linkButton-linker:hover {
+ cursor: pointer;
+ transform: scale(1.05);
}
.linkButton-empty, .linkButton-nonempty {
@@ -164,38 +176,50 @@
bottom: 0;
left: 50px;
pointer-events: auto;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
- .templating-button {
- width: 20px;
- height: 20px;
- border-radius: 50%;
- opacity: 0.9;
- background-color: $dark-color;
- color: $light-color;
- text-align: center;
- cursor: pointer;
+.fa-icon-link {
+ margin-top: 3px;
+}
+.templating-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ opacity: 0.9;
+ font-size:14;
+ background-color: $dark-color;
+ color: $light-color;
+ text-align: center;
+ cursor: pointer;
- &:hover {
- background: $main-accent;
- transform: scale(1.05);
- }
+ &:hover {
+ background: $main-accent;
+ transform: scale(1.05);
}
+}
- #template-list {
- position: absolute;
- top: 0;
- left: 30px;
- width: 150px;
- line-height: 25px;
- max-height: 175px;
- font-family: $sans-serif;
- font-size: 12px;
- background-color: $light-color-secondary;
- padding: 2px 12px;
- list-style: none;
-
- input {
- margin-right: 10px;
- }
+#template-list {
+ position: absolute;
+ top: 0;
+ left: 30px;
+ width: 150px;
+ line-height: 25px;
+ max-height: 175px;
+ font-family: $sans-serif;
+ font-size: 12px;
+ background-color: $light-color-secondary;
+ padding: 2px 12px;
+ list-style: none;
+
+ input {
+ margin-right: 10px;
}
} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index de17e1aef..8ae71fdc8 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,29 +1,36 @@
import { action, computed, observable, runInAction, untracked, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Key } from "../../fields/Key";
-import { KeyStore } from "../../fields/KeyStore";
-import { ListField } from "../../fields/ListField";
-import { NumberField } from "../../fields/NumberField";
-import { TextField } from "../../fields/TextField";
-import { Document } from "../../fields/Document";
import { emptyFunction, Utils } from "../../Utils";
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 } from "./nodes/DocumentView";
+import { DocumentView, PositionDocument } from "./nodes/DocumentView";
import { LinkMenu } from "./nodes/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
import React = require("react");
import { Template, Templates } from "./Templates";
import { CompileScript } from "../util/Scripting";
import { IconBox } from "./nodes/IconBox";
-import { FieldValue, Field } from "../../fields/Field";
-import { Documents } from "../documents/Documents";
+import { Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types";
+import { Doc, FieldResult } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { Docs } from "../documents/Documents";
+import { List } from "../../new_fields/List";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
+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 { FieldView } from "./nodes/FieldView";
+
+library.add(faLink);
@observer
export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -32,50 +39,54 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _resizing = "";
private keyinput: React.RefObject<HTMLInputElement>;
private _resizeBorderWidth = 16;
- private _linkBoxHeight = 20;
+ private _linkBoxHeight = 20 + 3; // link button height + margin
private _titleHeight = 20;
private _linkButton = React.createRef<HTMLDivElement>();
private _linkerButton = React.createRef<HTMLDivElement>();
private _downX = 0;
private _downY = 0;
+ private _iconDoc?: Doc = undefined;
@observable private _minimizedX = 0;
@observable private _minimizedY = 0;
@observable private _title: string = "";
@observable private _edtingTitle = false;
- @observable private _fieldKey: Key = KeyStore.Title;
+ @observable private _fieldKey = "title";
@observable private _hidden = false;
@observable private _opacity = 1;
- @observable private _iconifying = false;
+ @observable private _removeIcon = false;
@observable public Interacting = false;
constructor(props: Readonly<{}>) {
super(props);
DocumentDecorations.Instance = this;
this.keyinput = React.createRef();
- reaction(() => SelectionManager.SelectedDocuments().slice(), (docs) => docs.length === 0 && (this._edtingTitle = false));
+ reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this._edtingTitle = false);
}
- @action titleChanged = (event: any) => { this._title = event.target.value; }
- @action titleBlur = () => { this._edtingTitle = false; }
+ @action titleChanged = (event: any) => { this._title = event.target.value; };
+ @action titleBlur = () => { this._edtingTitle = false; };
@action titleEntered = (e: any) => {
var key = e.keyCode || e.which;
// enter pressed
if (key === 13) {
var text = e.target.value;
if (text[0] === '#') {
- let command = text.slice(1, text.length);
- this._fieldKey = new Key(command);
+ this._fieldKey = text.slice(1, text.length);
this._title = this.selectionTitle;
}
else {
if (SelectionManager.SelectedDocuments().length > 0) {
- let field = SelectionManager.SelectedDocuments()[0].props.Document.Get(this._fieldKey);
- if (field instanceof NumberField) {
- SelectionManager.SelectedDocuments().forEach(d =>
- d.props.Document.SetNumber(this._fieldKey, +this._title));
- } else if (field instanceof TextField || true) {
- SelectionManager.SelectedDocuments().forEach(d =>
- d.props.Document.SetText(this._fieldKey, this._title));
+ let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
+ if (typeof field === "number") {
+ 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 => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = this._title;
+ });
}
}
}
@@ -142,7 +153,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
dragData.xOffset = xoff;
dragData.yOffset = yoff;
- dragData.aliasOnDrop = false;
dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
this.Interacting = true;
this._hidden = true;
@@ -193,9 +203,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@action
onMinimizeDown = (e: React.PointerEvent): void => {
e.stopPropagation();
+ this._iconDoc = undefined;
if (e.button === 0) {
this._downX = e.pageX;
this._downY = e.pageY;
+ this._removeIcon = false;
let selDoc = SelectionManager.SelectedDocuments()[0];
let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
this._minimizedX = selDocPos[0] + 12;
@@ -218,41 +230,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this._minimizedX = snapped ? selDocPos[0] + 4 : e.clientX;
this._minimizedY = snapped ? selDocPos[1] - 18 : e.clientY;
let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
- Promise.all(selectedDocs.map(async selDoc => await this.getIconDoc(selDoc))).then(minDocSet =>
- this.moveIconDocs(SelectionManager.SelectedDocuments())
- );
- this._iconifying = snapped;
- }
- }
-
- @action createIcon = (docView: DocumentView, layoutString: string): Document => {
- let doc = docView.props.Document;
- let iconDoc = Documents.IconDocument(layoutString);
- iconDoc.SetText(KeyStore.Title, "ICON" + doc.Title)
- iconDoc.SetBoolean(KeyStore.IsMinimized, false);
- iconDoc.SetNumber(KeyStore.NativeWidth, 0);
- iconDoc.SetNumber(KeyStore.NativeHeight, 0);
- iconDoc.SetNumber(KeyStore.X, doc.GetNumber(KeyStore.X, 0));
- iconDoc.SetNumber(KeyStore.Y, doc.GetNumber(KeyStore.Y, 0) - 24);
- iconDoc.Set(KeyStore.Prototype, doc);
- iconDoc.Set(KeyStore.MaximizedDoc, doc);
- doc.Set(KeyStore.MinimizedDoc, iconDoc);
- docView.props.addDocument && docView.props.addDocument(iconDoc, false);
- return iconDoc;
- }
- @action
- public getIconDoc = async (docView: DocumentView): Promise<Document | undefined> => {
- let doc = docView.props.Document;
- let iconDoc = await doc.GetTAsync(KeyStore.MinimizedDoc, Document).then(async mindoc =>
- mindoc ? mindoc :
- await doc.GetTAsync(KeyStore.BackgroundLayout, TextField).then(async field =>
- (field instanceof TextField) ? this.createIcon(docView, field.Data) :
- await doc.GetTAsync(KeyStore.Layout, TextField).then(field =>
- (field instanceof TextField) ? this.createIcon(docView, field.Data) : undefined)));
- if (SelectionManager.SelectedDocuments()[0].props.addDocument !== undefined)
- SelectionManager.SelectedDocuments()[0].props.addDocument!(iconDoc!);
- return iconDoc;
+ if (selectedDocs.length > 1) {
+ this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString());
+ this.moveIconDoc(this._iconDoc);
+ } else {
+ this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon));
+ }
+ this._removeIcon = snapped;
+ }
}
@action
onMinimizeUp = (e: PointerEvent): void => {
@@ -261,34 +247,52 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.removeEventListener("pointermove", this.onMinimizeMove);
document.removeEventListener("pointerup", this.onMinimizeUp);
let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
- Promise.all(selectedDocs.map(async selDoc => await this.getIconDoc(selDoc))).then(minDocSet => {
- let minDocs = minDocSet.filter(minDoc => minDoc instanceof Document).map(minDoc => minDoc as Document);
- minDocs.map(minDoc => {
- minDoc.SetNumber(KeyStore.X, minDocs[0].GetNumber(KeyStore.X, 0));
- minDoc.SetNumber(KeyStore.Y, minDocs[0].GetNumber(KeyStore.Y, 0));
- minDoc.SetData(KeyStore.LinkTags, minDocs, ListField);
- if (this._iconifying && selectedDocs[0].props.removeDocument) {
- selectedDocs[0].props.removeDocument(minDoc);
- (minDoc.Get(KeyStore.MaximizedDoc, false) as Document)!.Set(KeyStore.MinimizedDoc, undefined);
- }
- });
- runInAction(() => this._minimizedX = this._minimizedY = 0);
- if (!this._iconifying) selectedDocs[0].props.toggleMinimized();
- this._iconifying = false;
- });
+ 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());
+ this._removeIcon = false;
}
+ runInAction(() => this._minimizedX = this._minimizedY = 0);
}
- moveIconDocs(selViews: DocumentView[], minDocSet?: FieldValue<Field>[]) {
- selViews.map(selDoc => {
- let minDoc = selDoc.props.Document.Get(KeyStore.MinimizedDoc);
- if (minDoc instanceof Document) {
- let zoom = selDoc.props.Document.GetNumber(KeyStore.ZoomBasis, 1);
- let where = (selDoc.props.ScreenToLocalTransform()).scale(selDoc.props.ContentScaling()).scale(1 / zoom).
- transformPoint(this._minimizedX - 12, this._minimizedY - 12);
- minDoc.SetNumber(KeyStore.X, where[0] + selDoc.props.Document.GetNumber(KeyStore.X, 0));
- minDoc.SetNumber(KeyStore.Y, where[1] + selDoc.props.Document.GetNumber(KeyStore.Y, 0));
- }
- });
+
+ @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.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;
+ selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);
+ return iconDoc;
+ }
+ @action
+ public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => {
+ let doc = docView.props.Document;
+ let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
+ if (!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) {
+ let selView = SelectionManager.SelectedDocuments()[0];
+ let zoom = NumCast(selView.props.Document.zoomBasis, 1);
+ let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).scale(1 / zoom).
+ transformPoint(this._minimizedX - 12, this._minimizedY - 12);
+ iconDoc.x = where[0] + NumCast(selView.props.Document.x);
+ iconDoc.y = where[1] + NumCast(selView.props.Document.y);
}
@action
@@ -323,7 +327,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.removeEventListener("pointermove", this.onLinkerButtonMoved);
document.removeEventListener("pointerup", this.onLinkerButtonUp);
let selDoc = SelectionManager.SelectedDocuments()[0];
- let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.GetPrototype() : undefined;
+ let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined;
let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
@@ -412,28 +416,30 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect();
if (rect.width !== 0) {
- let doc = element.props.Document;
- let width = doc.GetNumber(KeyStore.Width, 0);
- let nwidth = doc.GetNumber(KeyStore.NativeWidth, 0);
- let nheight = doc.GetNumber(KeyStore.NativeHeight, 0);
- let height = doc.GetNumber(KeyStore.Height, nwidth ? nheight / nwidth * width : 0);
- let x = doc.GetOrCreate(KeyStore.X, NumberField);
- let y = doc.GetOrCreate(KeyStore.Y, NumberField);
+ 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 scale = width / rect.width;
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
- x.Data += dX * (actualdW - width);
- y.Data += dY * (actualdH - height);
- var nativeWidth = doc.GetNumber(KeyStore.NativeWidth, 0);
- var nativeHeight = doc.GetNumber(KeyStore.NativeHeight, 0);
+ 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) {
if (Math.abs(dW) > Math.abs(dH)) {
actualdH = nativeHeight / nativeWidth * actualdW;
}
else actualdW = nativeWidth / nativeHeight * actualdH;
}
- doc.SetNumber(KeyStore.Width, actualdW);
- doc.SetNumber(KeyStore.Height, actualdH);
+ doc.width = actualdW;
+ doc.height = actualdH;
}
});
}
@@ -455,12 +461,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@computed
get selectionTitle(): string {
if (SelectionManager.SelectedDocuments().length === 1) {
- let field = SelectionManager.SelectedDocuments()[0].props.Document.Get(this._fieldKey);
- if (field instanceof TextField) {
- return (field).GetValue();
+ let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
+ if (typeof field === "string") {
+ return field;
}
- else if (field instanceof NumberField) {
- return (field).GetValue().toString();
+ else if (typeof field === "number") {
+ return field.toString();
}
} else if (SelectionManager.SelectedDocuments().length > 1) {
return "-multiple-";
@@ -483,32 +489,39 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
let minimizeIcon = (
<div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>
- {SelectionManager.SelectedDocuments().length == 1 ? IconBox.DocumentIcon(SelectionManager.SelectedDocuments()[0].props.Document.GetText(KeyStore.Layout, "...")) : "..."}
+ {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
</div>);
let linkButton = null;
if (SelectionManager.SelectedDocuments().length > 0) {
let selFirst = SelectionManager.SelectedDocuments()[0];
- let linkToSize = selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length;
- let linkFromSize = selFirst.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []).length;
+ let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
+ let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
let linkCount = linkToSize + linkFromSize;
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
content={<LinkMenu docView={selFirst}
changeFlyout={this.changeFlyoutContent} />}>
- <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ||
- selFirst.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []).length
- ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
- </Flyout>);
+ <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
+ </Flyout >);
}
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 docTemps = SelectionManager.SelectedDocuments().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.Name === temp.Name) {
+ if (template.Layout === temp) {
checked = true;
}
});
@@ -547,10 +560,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
<div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-
- <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
- <div className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>∞</div>
- <TemplateMenu doc={doc} templates={templates} />
+ <div className="link-button-container">
+ <div className="linkButtonWrapper">
+ <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
+ </div>
+ <div className="linkButtonWrapper">
+ <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
+ <FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" />
+ </div>
+ </div>
+ <TemplateMenu docs={SelectionManager.SelectedDocuments()} templates={templates} />
+ </div>
</div >
</div>
);
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 2f17c6c51..73467eb9d 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -60,7 +60,7 @@ export class EditableView extends React.Component<EditableProps> {
return (
<div className="editableView-container-editing" style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
onClick={action(() => this.editing = true)} >
- {this.props.contents}
+ <span>{this.props.contents}</span>
</div>
);
}
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 83836aa59..1c0d13545 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -1,9 +1,5 @@
import { action, computed, trace, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../fields/Document";
-import { FieldWaiting } from "../../fields/Field";
-import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField";
-import { KeyStore } from "../../fields/KeyStore";
import { Utils } from "../../Utils";
import { Transform } from "../util/Transform";
import "./InkingCanvas.scss";
@@ -11,10 +7,13 @@ import { InkingControl } from "./InkingControl";
import { InkingStroke } from "./InkingStroke";
import React = require("react");
import { undoBatch, UndoManager } from "../util/UndoManager";
+import { StrokeData, InkField, InkTool } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
+import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
interface InkCanvasProps {
getScreenTransform: () => Transform;
- Document: Document;
+ Document: Doc;
children: () => JSX.Element[];
}
@@ -23,7 +22,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome
@observable inkMidX: number = 0;
@observable inkMidY: number = 0;
- private previousState?: StrokeMap;
+ private previousState?: Map<string, StrokeData>;
private _currentStrokeId: string = "";
public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean {
return stroke.pathData.reduce((inside: boolean, val) => inside ||
@@ -33,9 +32,9 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
componentDidMount() {
- this.props.Document.GetTAsync(KeyStore.Ink, InkField, ink => runInAction(() => {
+ PromiseValue(Cast(this.props.Document.ink, InkField)).then(ink => runInAction(() => {
if (ink) {
- let bounds = Array.from(ink.Data).reduce(([mix, max, miy, may], [id, strokeData]) =>
+ let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) =>
strokeData.pathData.reduce(([mix, max, miy, may], p) =>
[Math.min(mix, p.x), Math.max(max, p.x), Math.min(miy, p.y), Math.max(may, p.y)],
[mix, max, miy, may]),
@@ -47,13 +46,13 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
@computed
- get inkData(): StrokeMap {
- let map = this.props.Document.GetT(KeyStore.Ink, InkField);
- return !map || map === FieldWaiting ? new Map : new Map(map.Data);
+ get inkData(): Map<string, StrokeData> {
+ let map = Cast(this.props.Document.ink, InkField);
+ return !map ? new Map : new Map(map.inkData);
}
- set inkData(value: StrokeMap) {
- this.props.Document.SetDataOnPrototype(KeyStore.Ink, value, InkField);
+ set inkData(value: Map<string, StrokeData>) {
+ Doc.SetOnPrototype(this.props.Document, "ink", new InkField(value));
}
@action
@@ -78,7 +77,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
color: InkingControl.Instance.selectedColor,
width: InkingControl.Instance.selectedWidth,
tool: InkingControl.Instance.selectedTool,
- page: this.props.Document.GetNumber(KeyStore.CurPage, -1)
+ page: NumCast(this.props.Document.curPage, -1)
});
this.inkData = data;
}
@@ -137,26 +136,29 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
@computed
get drawnPaths() {
- let curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
+ 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) {
- paths.push(<InkingStroke key={id} id={id} line={strokeData.pathData}
+ paths.push(<InkingStroke key={id} id={id}
+ line={strokeData.pathData}
+ count={strokeData.pathData.length}
offsetX={this.maxCanvasDim - this.inkMidX}
offsetY={this.maxCanvasDim - this.inkMidY}
- color={strokeData.color} width={strokeData.width}
- tool={strokeData.tool} deleteCallback={this.removeLine} />);
+ color={strokeData.color}
+ width={strokeData.width}
+ tool={strokeData.tool}
+ deleteCallback={this.removeLine} />);
}
return paths;
}, [] as JSX.Element[]);
- return [
- <svg className={`inkingCanvas-paths-ink`} key="Pens"
- style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
- {paths.filter(path => path.props.tool !== InkTool.Highlighter)}
- </svg>,
- <svg className={`inkingCanvas-paths-markers`} key="Markers"
- style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
- {paths.filter(path => path.props.tool === InkTool.Highlighter)}
- </svg>];
+ return [<svg className={`inkingCanvas-paths-ink`} key="Pens"
+ style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
+ {paths.filter(path => path.props.tool !== InkTool.Highlighter)}
+ </svg>,
+ <svg className={`inkingCanvas-paths-markers`} key="Markers"
+ style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
+ {paths.filter(path => path.props.tool === InkTool.Highlighter)}
+ </svg>];
}
render() {
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 9a68f0671..4b3dbd4e0 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -2,15 +2,14 @@ import { observable, action, computed } from "mobx";
import { CirclePicker, ColorResult } from 'react-color';
import React = require("react");
-import { InkTool } from "../../fields/InkField";
import { observer } from "mobx-react";
import "./InkingControl.scss";
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
import { SelectionManager } from "../util/SelectionManager";
-import { KeyStore } from "../../fields/KeyStore";
-import { TextField } from "../../fields/TextField";
+import { InkTool } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
library.add(faPen, faHighlighter, faEraser, faBan);
@@ -39,7 +38,7 @@ export class InkingControl extends React.Component {
if (SelectionManager.SelectedDocuments().length === 1) {
var sdoc = SelectionManager.SelectedDocuments()[0];
if (sdoc.props.ContainingCollectionView) {
- sdoc.props.Document.SetDataOnPrototype(KeyStore.BackgroundColor, color.hex, TextField);
+ Doc.SetOnPrototype(sdoc.props.Document, "backgroundColor", color.hex);
}
}
}
diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss
new file mode 100644
index 000000000..cdbfdcff3
--- /dev/null
+++ b/src/client/views/InkingStroke.scss
@@ -0,0 +1,3 @@
+.inkingStroke-marker {
+ mix-blend-mode: multiply
+} \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 0f05da22c..37b1f5899 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,14 +1,16 @@
import { observer } from "mobx-react";
-import { observable } from "mobx";
+import { observable, trace } from "mobx";
import { InkingControl } from "./InkingControl";
-import { InkTool } from "../../fields/InkField";
import React = require("react");
+import { InkTool } from "../../new_fields/InkField";
+import "./InkingStroke.scss";
interface StrokeProps {
offsetX: number;
offsetY: number;
id: string;
+ count: number;
line: Array<{ x: number, y: number }>;
color: string;
width: string;
@@ -48,10 +50,12 @@ export class InkingStroke extends React.Component<StrokeProps> {
render() {
let pathStyle = this.createStyle();
let pathData = this.parseData(this.props.line);
+ let pathlength = this.props.count; // bcz: this is needed to force reactions to the line data changes
+ let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
return (
- <path d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
+ <path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />
);
}
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index fbc7c8ef3..2430e8f6c 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -131,7 +131,7 @@ button:hover {
// add nodes menu. Note that the + button is actually an input label, not an actual button.
#add-nodes-menu {
position: absolute;
- bottom: 24px;
+ bottom: 22px;
left: 24px;
label {
@@ -149,7 +149,6 @@ button:hover {
label p {
padding-left: 10.5px;
- padding-top: 3px;
}
label:hover {
@@ -183,6 +182,7 @@ button:hover {
top: 0;
left: 0;
overflow: scroll;
+ z-index: 1;
}
#mainContent-div {
@@ -206,7 +206,7 @@ button:hover {
ul#add-options-list {
list-style: none;
- padding: 0;
+ padding: 5 0 0 0;
li {
display: inline-block;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index f32e92aac..677902c5b 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,17 +8,10 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Measure from 'react-measure';
import * as request from 'request';
-import { Document } from '../../fields/Document';
-import { Field, FieldWaiting, Opt, FIELD_WAITING } from '../../fields/Field';
-import { KeyStore } from '../../fields/KeyStore';
-import { ListField } from '../../fields/ListField';
-import { WorkspacesMenu } from '../../server/authentication/controllers/WorkspacesMenu';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { MessageStore } from '../../server/Message';
import { RouteStore } from '../../server/RouteStore';
-import { ServerUtils } from '../../server/ServerUtil';
-import { emptyDocFunction, emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
-import { Documents } from '../documents/Documents';
+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';
@@ -26,10 +19,10 @@ import { AggregateFunction, Catalog } from '../northstar/model/idea/idea';
import '../northstar/model/ModelExtensions';
import { HistogramOperation } from '../northstar/operations/HistogramOperation';
import '../northstar/utils/Extensions';
-import { Server } from '../Server';
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';
@@ -40,6 +33,11 @@ import { DocumentView } from './nodes/DocumentView';
import { PreviewCursor } from './PreviewCursor';
import { SearchBox } from './SearchBox';
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 {
@@ -48,11 +46,13 @@ export class Main extends React.Component {
@observable public pwidth: number = 0;
@observable public pheight: number = 0;
- @computed private get mainContainer(): Document | undefined | FIELD_WAITING {
- return CurrentUserUtils.UserDocument.GetT(KeyStore.ActiveWorkspace, Document);
+ @computed private get mainContainer(): Opt<Doc> {
+ return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
}
- private set mainContainer(doc: Document | undefined | FIELD_WAITING) {
- doc && CurrentUserUtils.UserDocument.Set(KeyStore.ActiveWorkspace, doc);
+ private set mainContainer(doc: Opt<Doc>) {
+ if (doc) {
+ CurrentUserUtils.UserDocument.activeWorkspace = doc;
+ }
}
constructor(props: Readonly<{}>) {
@@ -85,11 +85,11 @@ export class Main extends React.Component {
this.initEventListeners();
this.initAuthenticationRouters();
- try {
- this.initializeNorthstar();
- } catch (e) {
+ // try {
+ // this.initializeNorthstar();
+ // } catch (e) {
- }
+ // }
}
componentDidMount() { window.onpopstate = this.onHistory; }
@@ -99,9 +99,8 @@ export class Main extends React.Component {
onHistory = () => {
if (window.location.pathname !== RouteStore.home) {
let pathname = window.location.pathname.split("/");
- CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
- Server.GetField(CurrentUserUtils.MainDocId, action((field: Opt<Field>) => {
- if (field instanceof Document) {
+ DocServer.GetRefField(pathname[pathname.length - 1]).then(action((field: Opt<Field>) => {
+ if (field instanceof Doc) {
this.openWorkspace(field, true);
}
}));
@@ -113,9 +112,9 @@ export class Main extends React.Component {
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") {
+ if (e.key === "Escape") {
DragManager.AbortDrag();
- SelectionManager.DeselectAll()
+ SelectionManager.DeselectAll();
}
}, false); // drag event handler
// click interactions for the context menu
@@ -126,56 +125,55 @@ export class Main extends React.Component {
}), true);
}
- initAuthenticationRouters = () => {
+ initAuthenticationRouters = async () => {
// Load the user's active workspace, or create a new one if initial session after signup
if (!CurrentUserUtils.MainDocId) {
- CurrentUserUtils.UserDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => {
- if (doc) {
- CurrentUserUtils.MainDocId = doc.Id;
- this.openWorkspace(doc);
- } else {
- this.createNewWorkspace();
- }
- });
+ const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
+ if (doc) {
+ this.openWorkspace(doc);
+ } else {
+ this.createNewWorkspace();
+ }
} else {
- Server.GetField(CurrentUserUtils.MainDocId).then(field =>
- field instanceof Document ? this.openWorkspace(field) :
+ DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
+ field instanceof Doc ? this.openWorkspace(field) :
this.createNewWorkspace(CurrentUserUtils.MainDocId));
}
}
@action
- createNewWorkspace = (id?: string): void => {
- CurrentUserUtils.UserDocument.GetTAsync<ListField<Document>>(KeyStore.Workspaces, ListField).then(action((list: Opt<ListField<Document>>) => {
- if (list) {
- let freeformDoc = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" });
- var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] };
- let mainDoc = Documents.DockDocument(JSON.stringify(dockingLayout), { title: `Main Container ${list.Data.length + 1}` }, id);
- list.Data.push(mainDoc);
- CurrentUserUtils.MainDocId = mainDoc.Id;
- // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
- setTimeout(() => {
- this.openWorkspace(mainDoc);
- let pendingDocument = Documents.SchemaDocument([], { title: "New Mobile Uploads" });
- mainDoc.Set(KeyStore.OptionalRightCollection, pendingDocument);
- }, 0);
- }
- }));
+ 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 = (doc: Document, fromHistory = false): void => {
+ openWorkspace = async (doc: Doc, fromHistory = false) => {
+ CurrentUserUtils.MainDocId = doc[Id];
this.mainContainer = doc;
- fromHistory || window.history.pushState(null, doc.Title, "/doc/" + doc.Id);
- CurrentUserUtils.UserDocument.GetTAsync(KeyStore.OptionalRightCollection, Document).then(col =>
- // 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(() =>
- col && col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) =>
- f && f.Data.length > 0 && CollectionDockingView.Instance.AddRightSplit(col))
- , 100)
- );
+ 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 mainContent() {
let pwidthFunc = () => this.pwidth;
@@ -196,35 +194,38 @@ export class Main extends React.Component {
PanelHeight={pheightFunc}
isTopMost={true}
selectOnLoad={false}
- focus={emptyDocFunction}
+ focus={emptyFunction}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
ContainingCollectionView={undefined} />}
+ <PresentationView key="presentation" />
</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. */
- @computed
- get nodesMenu() {
+ 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(() => Documents.TextDocument({ borderRounding: -1, width: 200, height: 50, title: "a text note" }));
- let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
- let addSchemaNode = action(() => Documents.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" }));
- let addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", copyDraggedItems: true }));
- let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, title: "video node" }));
- let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
- let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
- let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
- let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
+ 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, () => Document][] = [
+ 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],
@@ -256,37 +257,20 @@ export class Main extends React.Component {
/* @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 workspacesRef = React.createRef<HTMLDivElement>();
let logoutRef = React.createRef<HTMLDivElement>();
- let toggleWorkspaces = () => runInAction(() => this._workspacesShown = !this._workspacesShown);
- let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {}));
return [
- <button className="clear-db-button" key="clear-db" onClick={clearDatabase}>Clear Database</button>,
+ <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="workspaces" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}>
- <button onClick={toggleWorkspaces}>Workspaces</button></div>,
-
- <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={workspacesRef}> <SearchBox /> </div>,
-
- <div className="main-buttonDiv" key="logout" style={{ bottom: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}>
- <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
+ <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </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>
];
- }
- @computed
- get workspaceMenu() {
- let areWorkspacesShown = () => this._workspacesShown;
- let toggleWorkspaces = () => runInAction(() => this._workspacesShown = !this._workspacesShown);
- let workspaces = CurrentUserUtils.UserDocument.GetT<ListField<Document>>(KeyStore.Workspaces, ListField);
- return (!workspaces || workspaces === FieldWaiting || this.mainContainer === FieldWaiting) ? (null) :
- <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace}
- new={this.createNewWorkspace} allWorkspaces={workspaces.Data}
- isShown={areWorkspacesShown} toggle={toggleWorkspaces} />;
}
render() {
@@ -296,9 +280,8 @@ export class Main extends React.Component {
{this.mainContent}
<PreviewCursor />
<ContextMenu />
- {this.nodesMenu}
+ {this.nodesMenu()}
{this.miscButtons}
- {this.workspaceMenu}
<InkingControl />
<MainOverlayTextBox />
</div>
@@ -306,17 +289,17 @@ export class Main extends React.Component {
}
// --------------- Northstar hooks ------------- /
- private _northstarSchemas: Document[] = [];
+ private _northstarSchemas: Doc[] = [];
@action SetNorthstarCatalog(ctlog: Catalog) {
CurrentUserUtils.NorthstarDBCatalog = ctlog;
if (ctlog && ctlog.schemas) {
ctlog.schemas.map(schema => {
- let schemaDocuments: Document[] = [];
+ let schemaDocuments: Doc[] = [];
let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema);
Promise.all(attributesToBecomeDocs.reduce((promises, attr) => {
- promises.push(Server.GetField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
- if (field instanceof Document) {
+ 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);
@@ -324,12 +307,12 @@ export class Main extends React.Component {
new AttributeTransformationModel(atmod, AggregateFunction.None),
new AttributeTransformationModel(atmod, AggregateFunction.Count),
new AttributeTransformationModel(atmod, AggregateFunction.Count));
- schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias"));
+ schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
}
})));
return promises;
}, [] as Promise<void>[])).finally(() =>
- this._northstarSchemas.push(Documents.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })));
+ this._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })));
});
}
}
@@ -341,7 +324,7 @@ export class Main extends React.Component {
}
(async () => {
- await Documents.initProtos();
+ await Docs.initProtos();
await CurrentUserUtils.loadCurrentUser();
ReactDOM.render(<Main />, document.getElementById('root'));
})();
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 13e661b46..d32e3f21b 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -2,16 +2,15 @@ import { action, observable, trace } from 'mobx';
import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
-import { Document } from '../../fields/Document';
-import { Key } from '../../fields/Key';
-import { KeyStore } from '../../fields/KeyStore';
-import { emptyDocFunction, emptyFunction, returnTrue, returnZero } from '../../Utils';
+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 "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
+import { Doc } from '../../new_fields/Doc';
+import { NumCast } from '../../new_fields/Types';
interface MainOverlayTextBoxProps {
}
@@ -19,10 +18,10 @@ interface MainOverlayTextBoxProps {
@observer
export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
public static Instance: MainOverlayTextBox;
- @observable public TextDoc?: Document = undefined;
+ @observable public TextDoc?: Doc = undefined;
public TextScroll: number = 0;
@observable _textXf: () => Transform = () => Transform.Identity();
- private _textFieldKey: Key = KeyStore.Data;
+ private _textFieldKey: string = "data";
private _textColor: string | null = null;
private _textTargetDiv: HTMLDivElement | undefined;
private _textProxyDiv: React.RefObject<HTMLDivElement>;
@@ -34,7 +33,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
}
@action
- SetTextDoc(textDoc?: Document, textFieldKey?: Key, div?: HTMLDivElement, tx?: () => Transform) {
+ SetTextDoc(textDoc?: Doc, textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) {
if (this._textTargetDiv) {
this._textTargetDiv.style.color = this._textColor;
}
@@ -89,12 +88,12 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
if (this.TextDoc && this._textTargetDiv) {
let textRect = this._textTargetDiv.getBoundingClientRect();
let s = this._textXf().Scale;
- return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.x}px, ${textRect.y}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} >
+ 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}
selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
- ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyDocFunction} />
+ ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} />
</div>
</ div>;
}
diff --git a/src/client/views/PresentationView.scss b/src/client/views/PresentationView.scss
new file mode 100644
index 000000000..7c5677f0d
--- /dev/null
+++ b/src/client/views/PresentationView.scss
@@ -0,0 +1,68 @@
+.presentationView-cont {
+ position: absolute;
+ background: white;
+ z-index: 1;
+ box-shadow: #AAAAAA .2vw .2vw .4vw;
+ right: 0;
+ top:0;
+ bottom:0;
+}
+
+.presentationView-item {
+ width: 220px;
+ height: 40px;
+ vertical-align: center;
+ padding-top: 15px;
+ -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;
+}
+
+.presentationView-item:hover {
+ transition: all .1s;
+ background: #AAAAAA
+}
+
+.presentationView-heading {
+ margin-top: 0px;
+ height: 40px;
+ background: lightseagreen;
+ padding: 30px;
+}
+.presentationView-title {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ font-size: 25px;
+ float:left;
+}
+.presentation-icon{
+ float: right;
+ display: inline;
+ width: 10px;
+ margin-top: 7px;
+}
+.presentationView-header {
+ padding-top: 1px;
+ padding-bottom: 1px;
+ font-size: 15px;
+ float:left;
+ }
+
+ .presentation-next{
+ float: right;
+ }
+ .presentation-back{
+ float: left;
+ }
+ .presentation-next:hover{
+ transition: all .1s;
+ background: #AAAAAA
+}
+.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
new file mode 100644
index 000000000..4853eb151
--- /dev/null
+++ b/src/client/views/PresentationView.tsx
@@ -0,0 +1,191 @@
+import { observer } from "mobx-react";
+import React = require("react")
+import { observable, action, runInAction, reaction } from "mobx";
+import "./PresentationView.scss"
+import "./Main.tsx";
+import { DocumentManager } from "../util/DocumentManager";
+import { Utils } from "../../Utils";
+import { Doc } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { Cast, NumCast, FieldValue, PromiseValue } from "../../new_fields/Types";
+import { Id } from "../../new_fields/RefField";
+import { List } from "../../new_fields/List";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+
+export interface PresViewProps {
+ //Document: Doc;
+}
+
+
+@observer
+/**
+ * Component that takes in a document prop and a boolean whether it's collapsed or not.
+ */
+class PresentationViewItem extends React.Component<PresViewProps> {
+
+ @observable Document: Doc;
+ constructor(props: PresViewProps) {
+ super(props);
+ this.Document = FieldValue(Cast(FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc))!.presentationView, Doc))!;
+ }
+ //look at CollectionFreeformView.focusDocument(d)
+ @action
+ openDoc = (doc: Doc) => {
+ let docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.focus(docView.props.Document);
+ }
+ }
+
+ /**
+ * Removes a document from the presentation view
+ **/
+ @action
+ public RemoveDoc(doc: Doc) {
+ const value = Cast(this.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) {
+ let title = document.title;
+
+ //to get currently selected presentation doc
+ let selected = NumCast(this.Document.selectedDoc, 0);
+
+ // finally, if it's a normal document, then render it as such.
+ const children = Cast(this.Document.data, listSpec(Doc));
+ const styles: any = {};
+ if (children && children[selected] === document) {
+ //this doc is selected
+ styles.background = "gray";
+ }
+ 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>
+ );
+
+ }
+
+ render() {
+ const children = Cast(this.Document.data, listSpec(Doc), []);
+
+ return (
+ <div>
+ {children.map(value => this.renderChild(value))}
+ </div>
+ );
+ }
+}
+
+
+@observer
+export class PresentationView extends React.Component<PresViewProps> {
+ public static Instance: PresentationView;
+
+ //observable means render is re-called every time variable is changed
+ @observable
+ collapsed: boolean = false;
+ closePresentation = action(() => this.Document!.width = 0);
+ next = () => {
+ const current = NumCast(this.Document!.selectedDoc);
+ const allDocs = FieldValue(Cast(this.Document!.data, listSpec(Doc)));
+ if (allDocs && current < allDocs.length + 1) {
+ //can move forwards
+ this.Document!.selectedDoc = current + 1;
+ const doc = allDocs[current + 1];
+ let docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.focus(docView.props.Document);
+ }
+ }
+
+ }
+ back = () => {
+ const current = NumCast(this.Document!.selectedDoc);
+ const allDocs = FieldValue(Cast(this.Document!.data, listSpec(Doc)));
+ if (allDocs && current - 1 >= 0) {
+ //can move forwards
+ this.Document!.selectedDoc = current - 1;
+ const doc = allDocs[current - 1];
+ let docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.focus(docView.props.Document);
+ }
+ }
+ }
+
+ private ref = React.createRef<HTMLDivElement>();
+
+ @observable Document?: Doc;
+ //initilize class variables
+ constructor(props: PresViewProps) {
+ super(props);
+ let self = this;
+ reaction(() =>
+ CurrentUserUtils.UserDocument.activeWorkspace,
+ (activeW) => {
+ if (activeW && activeW instanceof Doc) {
+ PromiseValue(Cast(activeW.presentationView, Doc)).
+ then(pv => runInAction(() =>
+ self.Document = pv ? pv : (activeW.presentationView = new Doc())))
+ }
+ },
+ { fireImmediately: true });
+ PresentationView.Instance = this;
+ }
+
+ /**
+ * Adds a document to the presentation view
+ **/
+ @action
+ public PinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ const data = Cast(this.Document!.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ this.Document!.data = new List([doc]);
+ }
+
+ this.Document!.width = 300;
+ }
+
+ render() {
+ if (!this.Document)
+ return (null);
+ let titleStr = this.Document.Title;
+ let width = NumCast(this.Document.width);
+
+ //TODO: next and back should be icons
+ return (
+ <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>
+
+ </div>
+ <ul>
+ <PresentationViewItem />
+ </ul>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 4359ba093..4ac4b9c95 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -28,7 +28,7 @@ export class PreviewCursor extends React.Component<{}> {
//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") {
+ if ((!e.ctrlKey && !e.metaKey) || e.key === "v" || e.key === "q") {
PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e);
PreviewCursor.Visible = false;
}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 8eb2fc6c6..376feb5a5 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,9 +1,11 @@
-import { observable, computed, action } from "mobx";
+import { observable, computed, action, trace } from "mobx";
import React = require("react");
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";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -25,31 +27,40 @@ 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;
- @observable private _templates: Map<Template, boolean> = this.props.templates;
-
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
- this.props.doc.addTemplate(template);
- this._templates.set(template, true);
+ if (template.Name == "Bullet") {
+ this.props.docs[0].addTemplate(template);
+ this.props.docs[0].props.Document.maximizedDocs = new List<Doc>(this.props.docs.filter((v, i) => i !== 0).map(v => v.props.Document));
+ } 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);
- this._templates.set(template, false);
+ if (template.Name == "Bullet") {
+ this.props.docs[0].removeTemplate(template);
+ this.props.docs[0].props.Document.maximizedDocs = 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)));
}
}
@action
componentWillReceiveProps(nextProps: TemplateMenuProps) {
- this._templates = nextProps.templates;
+ // this._templates = nextProps.templates;
}
@action
@@ -59,9 +70,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
render() {
let templateMenu: Array<JSX.Element> = [];
- this._templates.forEach((checked, template) => {
- templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />);
- });
+ this.props.templates.forEach((checked, template) =>
+ templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
return (
<div className="templating-menu" >
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index fef69392d..51fca4c41 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -39,22 +39,28 @@ 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={CaptionKey} /></div></div>`
+ `<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 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={CaptionKey}/></div></div>`
+ `<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={CaptionKey}/></div> </div>`
+ `<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 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">{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; padding:2px 10px">{props.Document.title}</div></div>`
);
- export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption];
+ export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
+ `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div className="isBullet" style="height:25px; width:25px; margin-left:-25px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px"/></div>`
+ );
+
+ export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption, 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 aa8fce923..14b92af48 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,13 +1,13 @@
import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Document } from '../../../fields/Document';
-import { FieldValue, FieldWaiting } from '../../../fields/Field';
-import { KeyStore } from '../../../fields/KeyStore';
-import { ListField } from '../../../fields/ListField';
-import { NumberField } from '../../../fields/NumberField';
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 { listSpec } from '../../../new_fields/Schema';
+import { List } from '../../../new_fields/List';
+import { Id } from '../../../new_fields/RefField';
import { SelectionManager } from '../../util/SelectionManager';
export enum CollectionViewType {
@@ -19,9 +19,9 @@ export enum CollectionViewType {
}
export interface CollectionRenderProps {
- addDocument: (document: Document, allowDuplicates?: boolean) => boolean;
- removeDocument: (document: Document) => boolean;
- moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
}
@@ -38,11 +38,9 @@ export interface CollectionViewProps extends FieldViewProps {
export class CollectionBaseView extends React.Component<CollectionViewProps> {
get collectionViewType(): CollectionViewType | undefined {
let Document = this.props.Document;
- let viewField = Document.GetT(KeyStore.ViewType, NumberField);
- if (viewField === FieldWaiting) {
- return undefined;
- } else if (viewField) {
- return viewField.Data;
+ let viewField = Cast(Document.viewType, "number");
+ if (viewField !== undefined) {
+ return viewField;
} else {
return CollectionViewType.Invalid;
}
@@ -61,100 +59,77 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
this.props.whenActiveChanged(isActive);
}
- createsCycle(documentToAdd: Document, containerDocument: Document): boolean {
- if (!(documentToAdd instanceof Document)) {
+ createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean {
+ if (!(documentToAdd instanceof Doc)) {
return false;
}
- let data = documentToAdd.GetList(KeyStore.Data, [] as Document[]);
+ let data = Cast(documentToAdd.data, listSpec(Doc), []);
for (const doc of data.filter(d => d instanceof Document)) {
if (this.createsCycle(doc, containerDocument)) {
return true;
}
}
- let annots = documentToAdd.GetList(KeyStore.Annotations, [] as Document[]);
+ let annots = Cast(documentToAdd.annotations, listSpec(Doc), []);
for (const annot of annots) {
if (this.createsCycle(annot, containerDocument)) {
return true;
}
}
- for (let containerProto: FieldValue<Document> = containerDocument; containerProto && containerProto !== FieldWaiting; containerProto = containerProto.GetPrototype()) {
- if (containerProto.Id === documentToAdd.Id) {
+ for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) {
+ if (containerProto[Id] === documentToAdd[Id]) {
return true;
}
}
return false;
}
- @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
+ @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
@action.bound
- addDocument(doc: Document, allowDuplicates: boolean = false): boolean {
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
- doc.SetOnPrototype(KeyStore.Page, new NumberField(curPage));
+ addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
+ let props = this.props;
+ var curPage = Cast(props.Document.curPage, "number", -1);
+ Doc.SetOnPrototype(doc, "page", curPage);
if (curPage >= 0) {
- doc.SetOnPrototype(KeyStore.AnnotationOn, this.props.Document);
+ Doc.SetOnPrototype(doc, "annotationOn", props.Document);
}
- if (!this.createsCycle(doc, this.props.Document)) {
- let value = this.props.Document.Get(this.props.fieldKey) as ListField<Document>;
- if (value) {
- if (!value.Data.some(v => v.Id === doc.Id) || allowDuplicates) {
- value.Data.push(doc);
+ 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 instanceof Doc && v[Id] === doc[Id])) {
+ alreadyAdded = false;
+ value.push(doc);
}
} else {
- this.props.Document.Set(this.props.fieldKey, new ListField([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) {
- let zoom = this.props.Document.GetNumber(KeyStore.Scale, 1);
- doc.SetNumber(KeyStore.ZoomBasis, zoom);
+ if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
+ let zoom = NumCast(this.props.Document.scale, 1);
+ Doc.SetOnPrototype(doc, "zoomBasis", zoom);
}
}
return true;
- // bcz: What is this code trying to do?
- // else {
- // let proto = props.Document.GetPrototype();
- // if (!proto || proto === FieldWaiting || !this.createsCycle(proto, doc)) {
- // const field = new ListField([doc]);
- // // const script = CompileScript(`
- // // if(added) {
- // // console.log("added " + field.Title + " " + doc.Title);
- // // } else {
- // // console.log("removed " + field.Title + " " + doc.Title);
- // // }
- // // `, {
- // // addReturn: false,
- // // params: {
- // // field: Document.name,
- // // added: "boolean"
- // // },
- // // capturedVariables: {
- // // doc: this.props.Document
- // // }
- // // });
- // // if (script.compiled) {
- // // field.addScript(new ScriptField(script));
- // // }
- // props.Document.SetOnPrototype(props.fieldKey, field);
- // return true;
- // }
- // }
- return false;
}
@action.bound
- removeDocument(doc: Document): boolean {
+ removeDocument(doc: Doc): boolean {
const props = this.props;
//TODO This won't create the field if it doesn't already exist
- const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>());
+ 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;
}
}
- doc.GetTAsync(KeyStore.AnnotationOn, Document).then((annotationOn) => {
+ PromiseValue(Cast(doc.annotationOn, Doc)).then((annotationOn) => {
if (annotationOn === props.Document) {
- doc.Set(KeyStore.AnnotationOn, undefined, true);
+ doc.annotationOn = undefined;
}
});
@@ -169,7 +144,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
}
@action.bound
- moveDocument(doc: Document, targetCollection: Document, addDocument: (doc: Document) => boolean): boolean {
+ moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
if (this.props.Document === targetCollection) {
return true;
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 2a8191c3a..d894909d0 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,36 +1,35 @@
-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 } from "mobx";
+import { action, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
-import { Document } from "../../../fields/Document";
-import { KeyStore } from "../../../fields/KeyStore";
import Measure from "react-measure";
-import { FieldId, Opt, Field, FieldWaiting } from "../../../fields/Field";
-import { Utils, returnTrue, emptyFunction, emptyDocFunction, returnOne, returnZero } from "../../../Utils";
-import { Server } from "../../Server";
+import * as GoldenLayout from "../../../client/goldenLayout";
+import { Doc, Field, Opt } from "../../../new_fields/Doc";
+import { FieldId, Id } 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 { ServerUtils } from "../../../server/ServerUtil";
-import { DragManager, DragLinksAsDocuments } from "../../util/DragManager";
-import { TextField } from "../../../fields/TextField";
-import { ListField } from "../../../fields/ListField";
-import { Transform } from '../../util/Transform'
+import React = require("react");
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
public static Instance: CollectionDockingView;
- public static makeDocumentConfig(document: Document) {
+ public static makeDocumentConfig(document: Doc, width?: number) {
return {
type: 'react-component',
component: 'DocumentFrameRenderer',
- title: document.Title,
+ title: document.title,
+ width: width,
props: {
- documentId: document.Id,
+ documentId: document[Id],
//collectionDockingView: CollectionDockingView.Instance
}
};
@@ -38,7 +37,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
private _goldenLayout: any = null;
private _containerRef = React.createRef<HTMLDivElement>();
- private _fullScreen: any = null;
private _flush: boolean = false;
private _ignoreStateChange = "";
@@ -50,7 +48,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
hack: boolean = false;
undohack: any = null;
- public StartOtherDrag(dragDocs: Document[], e: any) {
+ public StartOtherDrag(dragDocs: Doc[], e: any) {
this.hack = true;
this.undohack = UndoManager.StartBatch("goldenDrag");
dragDocs.map(dragDoc =>
@@ -59,7 +57,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
@action
- public OpenFullScreen(document: Document) {
+ public OpenFullScreen(document: Doc) {
let newItemStackConfig = {
type: 'stack',
content: [CollectionDockingView.makeDocumentConfig(document)]
@@ -68,26 +66,52 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this._goldenLayout.root.contentItems[0].addChild(docconfig);
docconfig.callDownwards('_$init');
this._goldenLayout._$maximiseItem(docconfig);
- this._fullScreen = docconfig;
this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
this.stateChanged();
}
+
+ @undoBatch
@action
- public CloseFullScreen() {
- if (this._fullScreen) {
- this._goldenLayout._$minimiseItem(this._fullScreen);
- this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen);
- this._fullScreen = null;
- this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
- this.stateChanged();
+ public CloseRightSplit(document: Doc) {
+ if (this._goldenLayout.root.contentItems[0].isRow) {
+ this._goldenLayout.root.contentItems[0].contentItems.map((child: any, i: number) => {
+ if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+ child.contentItems[0].config.props.documentId == document[Id]) {
+ child.contentItems[0].remove();
+ this.layoutChanged(document);
+ this.stateChanged();
+ } else
+ child.contentItems.map((tab: any, j: number) => {
+ if (tab.config.component === "DocumentFrameRenderer" && tab.config.props.documentId === document[Id]) {
+ 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);
+ this.stateChanged();
+ }
+ });
+ })
}
}
+ @action
+ layoutChanged(removed?: Doc) {
+ this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
+ this._goldenLayout.emit('sbcreteChanged');
+ 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: Document, minimize: boolean = false) {
+ public AddRightSplit(document: Doc, minimize: boolean = false) {
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ if (docs) {
+ docs.push(document);
+ }
let newItemStackConfig = {
type: 'stack',
content: [CollectionDockingView.makeDocumentConfig(document)]
@@ -110,20 +134,18 @@ 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;
}
setupGoldenLayout() {
- var config = this.props.Document.GetText(KeyStore.Data, "");
+ var config = StrCast(this.props.Document.dockingConfig);
if (config) {
if (!this._goldenLayout) {
this._goldenLayout = new GoldenLayout(JSON.parse(config));
@@ -158,7 +180,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
componentDidMount: () => void = () => {
if (this._containerRef.current) {
reaction(
- () => this.props.Document.GetText(KeyStore.Data, ""),
+ () => StrCast(this.props.Document.dockingConfig),
() => {
if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) {
setTimeout(() => this.setupGoldenLayout(), 1);
@@ -206,8 +228,8 @@ 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;
- Server.GetField(docid, action(async (sourceDoc: Opt<Field>) =>
- (sourceDoc instanceof Document) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
+ DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
+ (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
} else
if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) {
e.stopPropagation();
@@ -216,12 +238,12 @@ 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;
- Server.GetField(docid, action((f: Opt<Field>) => {
- if (f instanceof Document) {
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
{
handlers: {
- dragComplete: action(emptyFunction),
+ dragComplete: emptyFunction,
},
hideSource: false
});
@@ -238,8 +260,13 @@ 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.SetText(KeyStore.Data, json);
+ this.props.Document.dockingConfig = json;
if (this.undohack && !this.hack) {
this.undohack.end();
this.undohack = undefined;
@@ -258,40 +285,42 @@ 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") {
- Server.GetField(tab.contentItem.config.props.documentId, action((f: Opt<Field>) => {
- if (f !== undefined && f instanceof Document) {
- f.GetTAsync(KeyStore.Title, TextField, (tfield) => {
- if (tfield !== undefined) {
- tab.titleElement[0].textContent = f.Title;
- }
- });
- f.GetTAsync(KeyStore.LinkedFromDocs, ListField).then(lf =>
- f.GetTAsync(KeyStore.LinkedToDocs, ListField).then(lt => {
- let count = (lf ? lf.Data.length : 0) + (lt ? lt.Data.length : 0);
- let counter: any = this.htmlToElement(`<div class="messageCounter">${count}</div>`);
- tab.element.append(counter);
- counter.DashDocId = tab.contentItem.config.props.documentId;
- tab.reactionDisposer = reaction(() => [f.GetT(KeyStore.LinkedFromDocs, ListField), f.GetT(KeyStore.LinkedToDocs, ListField)],
- (lists) => {
- let count = (lists.length > 0 && lists[0] && lists[0]!.Data ? lists[0]!.Data.length : 0) +
- (lists.length > 1 && lists[1] && lists[1]!.Data ? lists[1]!.Data.length : 0);
- counter.innerHTML = count;
- });
- }));
+ 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(`<div class="messageCounter">0</div>`);
+ tab.element.append(counter);
+ counter.DashDocId = tab.contentItem.config.props.documentId;
+ tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
+ () => {
+ const lf = Cast(doc.linkedFromDocs, listSpec(Doc), []);
+ const lt = Cast(doc.linkedToDocs, listSpec(Doc), []);
+ let count = (lf ? lf.length : 0) + (lt ? lt.length : 0);
+ counter.innerHTML = count;
+ tab.titleElement[0].textContent = doc.title;
+ }, { fireImmediately: true });
tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
}
- }));
+ });
}
tab.closeElement.off('click') //unbind the current click handler
- .click(function () {
+ .click(async function () {
if (tab.reactionDisposer) {
tab.reactionDisposer();
}
+ 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();
});
}
+ _removedDocs: Doc[] = [];
stackCreated = (stack: any) => {
//stack.header.controlsContainer.find('.lm_popout').hide();
@@ -300,13 +329,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 = ServerUtils.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");
}));
}
@@ -316,6 +353,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
);
}
+
}
interface DockedFrameProps {
@@ -324,36 +362,29 @@ interface DockedFrameProps {
}
@observer
export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
-
_mainCont = React.createRef<HTMLDivElement>();
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
- @observable private _document: Opt<Document>;
+ @observable private _document: Opt<Doc>;
constructor(props: any) {
super(props);
- Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document));
+ DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
}
- nativeWidth = () => {
- let pw = this._document!.GetNumber(KeyStore.NativeWidth, 0);
- return pw ? pw : this._panelWidth;
- }
- nativeHeight = () => {
- let pw = this._document!.GetNumber(KeyStore.NativeHeight, 0);
- return pw ? pw : this._panelHeight;
- }
+ nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
+ nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight);
contentScaling = () => {
- let wscale = this._panelWidth / (this.nativeWidth() ? this.nativeWidth() : this._panelWidth);
- if (wscale * this.nativeHeight() > this._panelHeight)
- return this._panelHeight / (this.nativeHeight() ? this.nativeHeight() : this._panelHeight);
- return wscale;
+ const nativeH = this.nativeHeight();
+ const nativeW = this.nativeWidth();
+ let wscale = this._panelWidth / nativeW;
+ return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
}
ScreenToLocalTransform = () => {
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;
+ 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 Transform.Identity();
@@ -361,10 +392,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
get content() {
+ 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!}
+ <DocumentView key={this._document![Id]} Document={this._document!}
toggleMinimized={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
@@ -376,15 +409,17 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
selectOnLoad={false}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
- focus={emptyDocFunction}
+ focus={emptyFunction}
+ bringToFront={emptyFunction}
ContainingCollectionView={undefined} />
- </div>);
+ </div >);
}
render() {
+ let theContent = this.content;
return !this._document ? (null) :
<Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}>
- {({ measureRef }) => <div ref={measureRef}> {this.content} </div>}
+ {({ measureRef }) => <div ref={measureRef}> {theContent} </div>}
</Measure>;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index 497c3ee3c..b3762206a 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,6 +1,5 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../fields/KeyStore";
import { ContextMenu } from "../ContextMenu";
import "./CollectionPDFView.scss";
import React = require("react");
@@ -8,21 +7,23 @@ import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormV
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";
@observer
export class CollectionPDFView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldKey: string = "DataKey") {
+ public static LayoutString(fieldKey: string = "data") {
return FieldView.LayoutString(CollectionPDFView, fieldKey);
}
@observable _inThumb = false;
- private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); }
- private set curPage(value: number) { this.props.Document.SetNumber(KeyStore.CurPage, value); }
- private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); }
- @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : -1;
- @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : -1;
+ private set curPage(value: number) { this.props.Document.curPage = value; }
+ private get curPage() { return NumCast(this.props.Document.curPage, -1); }
+ private get numPages() { return NumCast(this.props.Document.numPages); }
+ @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1;
+ @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.curPage = this.curPage + 1) : -1;
@action
onThumbDown = (e: React.PointerEvent) => {
@@ -43,8 +44,8 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
document.removeEventListener("pointermove", this.onThumbMove);
document.removeEventListener("pointerup", this.onThumbUp);
}
- nativeWidth = () => this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- nativeHeight = () => this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
+ nativeWidth = () => NumCast(this.props.Document.nativeWidth);
+ nativeHeight = () => NumCast(this.props.Document.nativeHeight);
private get uIButtons() {
let ratio = (this.curPage - 1) / this.numPages * 100;
return (
@@ -59,7 +60,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
+ 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 });
}
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index b3cc3fbd5..16818affd 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -5,17 +5,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, untracked, runInAction } from "mobx";
import { observer } from "mobx-react";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
-import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'
+import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
import "react-table/react-table.css";
-import { Document } from "../../../fields/Document";
-import { Field, Opt, FieldWaiting } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import { emptyDocFunction, emptyFunction, returnFalse, returnZero } from "../../../Utils";
-import { Server } from "../../Server";
+import { emptyFunction, returnFalse, returnZero } from "../../../Utils";
import { SetupDrag } from "../../util/DragManager";
-import { CompileScript, ToField } from "../../util/Scripting";
+import { CompileScript } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss";
import { anchorPoints, Flyout } from "../DocumentDecorations";
@@ -25,43 +19,46 @@ import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
+import { Opt, Field, Doc, DocListCast } from "../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+import { List } from "../../../new_fields/List";
+import { Id } from "../../../new_fields/RefField";
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@observer
-class KeyToggle extends React.Component<{ keyId: string, checked: boolean, toggle: (key: Key) => void }> {
- @observable key: Key | undefined;
-
+class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> {
constructor(props: any) {
super(props);
- Server.GetField(this.props.keyId, action((field: Opt<Field>) => field instanceof Key && (this.key = field)));
}
render() {
- return !this.key ? (null) :
- (<div key={this.key.Id}>
- <input type="checkbox" checked={this.props.checked} onChange={() => this.key && this.props.toggle(this.key)} />
- {this.key.Name}
- </div>);
+ return (
+ <div key={this.props.keyName}>
+ <input type="checkbox" checked={this.props.checked} onChange={() => this.props.toggle(this.props.keyName)} />
+ {this.props.keyName}
+ </div>
+ );
}
}
@observer
-export class CollectionSchemaView extends CollectionSubView {
+export class CollectionSchemaView extends CollectionSubView(doc => doc) {
private _mainCont?: HTMLDivElement;
private _startSplitPercent = 0;
private DIVIDER_WIDTH = 4;
- @observable _columns: Array<Key> = [KeyStore.Title, KeyStore.Data, KeyStore.Author];
+ @observable _columns: Array<string> = ["title", "data", "author"];
@observable _selectedIndex = 0;
@observable _columnsPercentage = 0;
- @observable _keys: Key[] = [];
+ @observable _keys: string[] = [];
@observable _newKeyName: string = "";
- @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0); }
- @computed get columns() { return this.props.Document.GetList(KeyStore.ColumnsKey, [] as Key[]); }
+ @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); }
+ @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); }
@computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
renderCell = (rowProps: CellInfo) => {
@@ -74,7 +71,7 @@ export class CollectionSchemaView extends CollectionSubView {
isTopMost: false,
selectOnLoad: false,
ScreenToLocalTransform: Transform.Identity,
- focus: emptyDocFunction,
+ focus: emptyFunction,
active: returnFalse,
whenActiveChanged: emptyFunction,
PanelHeight: returnZero,
@@ -85,34 +82,27 @@ export class CollectionSchemaView extends CollectionSubView {
);
let reference = React.createRef<HTMLDivElement>();
let onItemDown = SetupDrag(reference, () => props.Document, this.props.moveDocument);
- let applyToDoc = (doc: Document, run: (args?: { [name: string]: any }) => any) => {
+ let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => {
const res = run({ this: doc });
if (!res.success) return false;
const field = res.result;
- if (field instanceof Field) {
- doc.Set(props.fieldKey, field);
- return true;
- } else {
- let dataField = ToField(field);
- if (dataField) {
- doc.Set(props.fieldKey, dataField);
- return true;
- }
- }
- return false;
+ doc[props.fieldKey] = field;
+ return true;
};
return (
- <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document.Id} ref={reference}>
+ <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}>
<EditableView
display={"inline"}
contents={contents}
height={Number(MAX_ROW_HEIGHT)}
GetValue={() => {
- let field = props.Document.Get(props.fieldKey);
- if (field && field instanceof Field) {
- return field.ToScriptString();
+ let field = props.Document[props.fieldKey];
+ if (field) {
+ //TODO Types
+ // return field.ToScriptString();
+ return String(field);
}
- return field || "";
+ return "";
}}
SetValue={(value: string) => {
let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
@@ -121,21 +111,18 @@ export class CollectionSchemaView extends CollectionSubView {
}
return applyToDoc(props.Document, script.run);
}}
- OnFillDown={(value: string) => {
+ OnFillDown={async (value: string) => {
let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
if (!script.compiled) {
return;
}
const run = script.run;
//TODO This should be able to be refactored to compile the script once
- this.props.Document.GetTAsync<ListField<Document>>(this.props.fieldKey, ListField).then((val) => {
- if (val) {
- val.Data.forEach(doc => applyToDoc(doc, run));
- }
- });
+ const val = await DocListCast(this.props.Document[this.props.fieldKey])
+ val && val.forEach(doc => applyToDoc(doc, run));
}}>
</EditableView>
- </div>
+ </div >
);
}
@@ -165,41 +152,38 @@ export class CollectionSchemaView extends CollectionSubView {
super.CreateDropTarget(ele);
}
- toggleKey = (key: Key) => {
- this.props.Document.GetTAsync<ListField<Key>>(KeyStore.ColumnsKey, ListField).then(field =>
- runInAction(() => {
- if (field !== FieldWaiting) {
- if (field) {
- const index = field.Data.indexOf(key);
- if (index === -1) {
- this.columns.push(key);
- } else {
- this.columns.splice(index, 1);
- }
- } else {
- this.props.Document.SetData(KeyStore.ColumnsKey, [key], ListField);
- }
- }
- }));
+ @action
+ toggleKey = (key: string) => {
+ let list = Cast(this.props.Document.schemaColumns, listSpec("string"));
+ if (list === undefined) {
+ this.props.Document.schemaColumns = list = new List<string>([key]);
+ } else {
+ const index = list.indexOf(key);
+ if (index === -1) {
+ list.push(key);
+ } else {
+ list.splice(index, 1);
+ }
+ }
}
//toggles preview side-panel of schema
@action
toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0);
+ this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
}
@action
onDividerMove = (e: PointerEvent): void => {
let nativeWidth = this._mainCont!.getBoundingClientRect();
- this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)));
+ this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
}
@action
onDividerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
if (this._startSplitPercent === this.splitPercentage) {
- this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0);
+ this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
}
}
onDividerDown = (e: React.PointerEvent) => {
@@ -212,8 +196,7 @@ export class CollectionSchemaView extends CollectionSubView {
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (this.props.isSelected())
- e.stopPropagation();
+ if (this.props.isSelected()) e.stopPropagation();
else e.preventDefault();
}
}
@@ -226,7 +209,7 @@ export class CollectionSchemaView extends CollectionSubView {
@action
addColumn = () => {
- this.columns.push(new Key(this._newKeyName));
+ this.columns.push(this._newKeyName);
this._newKeyName = "";
}
@@ -241,21 +224,22 @@ export class CollectionSchemaView extends CollectionSubView {
this.previewScript = e.currentTarget.value;
}
- get previewDocument(): Document | undefined {
- const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]);
- const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
- return selected ? (this.previewScript ? selected.Get(new Key(this.previewScript)) as Document : selected) : undefined;
+ get previewDocument(): Doc | undefined {
+ const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ 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 = () => this.previewDocument!.GetNumber(KeyStore.NativeWidth, this.previewRegionWidth);
- private previewDocNativeHeight = () => this.previewDocument!.GetNumber(KeyStore.NativeHeight, this.previewRegionHeight);
+ 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)
+ if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) {
return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight);
+ }
return wscale;
}
private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling();
@@ -263,24 +247,26 @@ export class CollectionSchemaView extends CollectionSubView {
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).scale(1 / this.previewContentScaling())
@computed
get previewPanel() {
// let doc = CompileScript(this.previewScript, { this: selected }, true)();
- return !this.previewDocument ? (null) : (
+ 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={this.previewDocument} isTopMost={false} selectOnLoad={false}
+ <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={emptyDocFunction}
+ focus={emptyFunction}
parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction}
/>
</div>
<input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange}
@@ -290,24 +276,25 @@ export class CollectionSchemaView extends CollectionSubView {
}
get documentKeysCheckList() {
- const docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]);
- let keys: { [id: string]: boolean } = {};
+ const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ 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
// invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
// then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
// is displayed (unlikely) it won't show up until something else changes.
- untracked(() => docs.map(doc => doc.GetAllPrototypes().map(proto => proto._proxies.forEach((val: any, key: string) => keys[key] = false))));
+ //TODO Types
+ untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false))));
- this.columns.forEach(key => keys[key.Id] = true);
+ this.columns.forEach(key => keys[key] = true);
return Array.from(Object.keys(keys)).map(item =>
- (<KeyToggle checked={keys[item]} key={item} keyId={item} toggle={this.toggleKey} />));
+ (<KeyToggle checked={keys[item]} key={item} keyName={item} toggle={this.toggleKey} />));
}
get tableOptionsPanel() {
return !this.props.active() ? (null) :
(<Flyout
- anchorPoint={anchorPoints.LEFT_TOP}
+ anchorPoint={anchorPoints.RIGHT_TOP}
content={<div>
<div id="schema-options-header"><h5><b>Options</b></h5></div>
<div id="options-flyout-div">
@@ -335,16 +322,17 @@ export class CollectionSchemaView extends CollectionSubView {
render() {
library.add(faCog);
library.add(faPlus);
- const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]);
+ //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));
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.Name,
- accessor: (doc: Document) => [doc, col],
- id: col.Id
+ Header: col,
+ accessor: (doc: Doc) => [doc, col],
+ id: col
}))}
column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
getTrProps={this.getTrProps}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index f9fc7be5a..828ac880a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,28 +1,27 @@
import { action, runInAction } from "mobx";
-import { Document } from "../../../fields/Document";
-import { ListField } from "../../../fields/ListField";
import React = require("react");
-import { KeyStore } from "../../../fields/KeyStore";
-import { FieldWaiting, Opt } from "../../../fields/Field";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DragManager } from "../../util/DragManager";
-import { Documents, DocumentOptions } from "../../documents/Documents";
+import { Docs, DocumentOptions } from "../../documents/Documents";
import { RouteStore } from "../../../server/RouteStore";
-import { TupleField } from "../../../fields/TupleField";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { NumberField } from "../../../fields/NumberField";
-import { ServerUtils } from "../../../server/ServerUtil";
-import { Server } from "../../Server";
import { FieldViewProps } from "../nodes/FieldView";
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 { DocComponent } from "../DocComponent";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, PromiseValue, FieldValue } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { DocServer } from "../../DocServer";
+import { ObjectField } from "../../../new_fields/ObjectField";
export interface CollectionViewProps extends FieldViewProps {
- addDocument: (document: Document, allowDuplicates?: boolean) => boolean;
- removeDocument: (document: Document) => boolean;
- moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
PanelWidth: () => number;
PanelHeight: () => number;
}
@@ -33,176 +32,196 @@ export interface SubCollectionViewProps extends CollectionViewProps {
export type CursorEntry = TupleField<[string, string], [number, number]>;
-export class CollectionSubView extends React.Component<SubCollectionViewProps> {
- private dropDisposer?: DragManager.DragDropDisposer;
- protected createDropTarget = (ele: HTMLDivElement) => {
- if (this.dropDisposer) {
- this.dropDisposer();
+export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
+ class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
+ private dropDisposer?: DragManager.DragDropDisposer;
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
}
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ protected CreateDropTarget(ele: HTMLDivElement) {
+ this.createDropTarget(ele);
}
- }
- protected CreateDropTarget(ele: HTMLDivElement) {
- this.createDropTarget(ele);
- }
- @action
- protected setCursorPosition(position: [number, number]) {
- let ind;
- let doc = this.props.Document;
- let id = CurrentUserUtils.id;
- let email = CurrentUserUtils.email;
- if (id && email) {
- let textInfo: [string, string] = [id, email];
- doc.GetTAsync(KeyStore.Prototype, Document).then(proto => {
+ get children() {
+ //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));
+ }
+
+ @action
+ protected async setCursorPosition(position: [number, number]) {
+ return;
+ let ind;
+ let doc = this.props.Document;
+ let id = CurrentUserUtils.id;
+ let email = CurrentUserUtils.email;
+ if (id && email) {
+ let textInfo: [string, string] = [id, email];
+ const proto = await doc.proto;
if (!proto) {
return;
}
- proto.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, action((field: ListField<CursorEntry>) => {
- let cursors = field.Data;
- if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
- cursors[ind].Data[1] = position;
- } else {
- let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
- cursors.push(entry);
- }
- }));
- });
+ let cursors = await Cast(proto.cursors, listSpec(ObjectField));
+ if (!cursors) {
+ proto.cursors = cursors = new List<ObjectField>();
+ }
+ if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
+ cursors[ind].Data[1] = position;
+ } else {
+ let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
+ cursors.push(entry);
+ }
+ }
}
- }
- @undoBatch
- @action
- protected drop(e: Event, de: DragManager.DropEvent): boolean {
- if (de.data instanceof DragManager.DocumentDragData) {
- if (de.data.aliasOnDrop || de.data.copyOnDrop) {
- [KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key =>
- de.data.draggedDocuments.map((draggedDocument: Document, i: number) =>
- draggedDocument.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocuments[i].SetNumber(key, f.Data) : null)));
- }
- let added = false;
- if (de.data.aliasOnDrop || de.data.copyOnDrop) {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = this.props.addDocument(d);
- return moved || added;
- }, false);
- } else if (de.data.moveDocument) {
- const move = de.data.moveDocument;
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = move(d, this.props.Document, this.props.addDocument);
- return moved || added;
- }, false);
- } else {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = this.props.addDocument(d);
- return moved || added;
- }, false);
+ @undoBatch
+ @action
+ protected drop(e: Event, de: DragManager.DropEvent): boolean {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ if (de.data.dropAction || de.data.userDropAction) {
+ ["width", "height", "curPage"].map(key =>
+ de.data.draggedDocuments.map((draggedDocument: Doc, i: number) =>
+ PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f))));
+ }
+ let added = false;
+ if (de.data.dropAction || de.data.userDropAction) {
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = this.props.addDocument(d);
+ return moved || added;
+ }, false);
+ } else if (de.data.moveDocument) {
+ const move = de.data.moveDocument;
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = move(d, this.props.Document, this.props.addDocument);
+ return moved || added;
+ }, false);
+ } else {
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = this.props.addDocument(d);
+ return moved || added;
+ }, false);
+ }
+ e.stopPropagation();
+ return added;
}
- e.stopPropagation();
- return added;
+ return false;
}
- return false;
- }
- protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Document>> {
- let ctor: ((path: string, options: DocumentOptions) => (Document | Promise<Document | undefined>)) | undefined = undefined;
- if (type.indexOf("image") !== -1) {
- ctor = Documents.ImageDocument;
- }
- if (type.indexOf("video") !== -1) {
- ctor = Documents.VideoDocument;
- }
- if (type.indexOf("audio") !== -1) {
- ctor = Documents.AudioDocument;
- }
- if (type.indexOf("pdf") !== -1) {
- ctor = Documents.PdfDocument;
- options.nativeWidth = 1200;
- }
- if (type.indexOf("excel") !== -1) {
- ctor = Documents.DBDocument;
- options.copyDraggedItems = true;
- }
- if (type.indexOf("html") !== -1) {
- if (path.includes('localhost')) {
- let s = path.split('/');
- let id = s[s.length - 1];
- Server.GetField(id).then(field => {
- if (field instanceof Document) {
- let alias = field.CreateAlias();
- alias.SetNumber(KeyStore.X, options.x || 0);
- alias.SetNumber(KeyStore.Y, options.y || 0);
- alias.SetNumber(KeyStore.Width, options.width || 300);
- alias.SetNumber(KeyStore.Height, options.height || options.width || 300);
- this.props.addDocument(alias, false);
- }
- });
- return undefined;
+ protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
+ let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
+ if (type.indexOf("image") !== -1) {
+ ctor = Docs.ImageDocument;
+ }
+ if (type.indexOf("video") !== -1) {
+ ctor = Docs.VideoDocument;
+ }
+ if (type.indexOf("audio") !== -1) {
+ ctor = Docs.AudioDocument;
}
- ctor = Documents.WebDocument;
- options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ if (type.indexOf("pdf") !== -1) {
+ ctor = Docs.PdfDocument;
+ options.nativeWidth = 1200;
+ }
+ if (type.indexOf("excel") !== -1) {
+ ctor = Docs.DBDocument;
+ options.dropAction = "copy";
+ }
+ if (type.indexOf("html") !== -1) {
+ if (path.includes('localhost')) {
+ let s = path.split('/');
+ let id = s[s.length - 1];
+ DocServer.GetRefField(id).then(field => {
+ if (field instanceof Doc) {
+ let alias = Doc.MakeAlias(field);
+ alias.x = options.x || 0;
+ alias.y = options.y || 0;
+ alias.width = options.width || 300;
+ alias.height = options.height || options.width || 300;
+ this.props.addDocument(alias, false);
+ }
+ });
+ return undefined;
+ }
+ ctor = Docs.WebDocument;
+ options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ }
+ return ctor ? ctor(path, options) : undefined;
}
- return ctor ? ctor(path, options) : undefined;
- }
- @undoBatch
- @action
- protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
- let html = e.dataTransfer.getData("text/html");
- let text = e.dataTransfer.getData("text/plain");
+ @undoBatch
+ @action
+ protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ let html = e.dataTransfer.getData("text/html");
+ let text = e.dataTransfer.getData("text/plain");
- if (text && text.startsWith("<div")) {
- return;
- }
- e.stopPropagation();
- e.preventDefault();
+ if (text && text.startsWith("<div")) {
+ return;
+ }
+ e.stopPropagation();
+ e.preventDefault();
- if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
- this.props.addDocument(Documents.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }), false);
- return;
- }
+ if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
+ let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
+ this.props.addDocument(htmlDoc, false);
+ return;
+ }
- let batch = UndoManager.StartBatch("collection view drop");
- let promises: Promise<void>[] = [];
- // tslint:disable-next-line:prefer-for-of
- for (let i = 0; i < e.dataTransfer.items.length; i++) {
- const upload = window.location.origin + RouteStore.upload;
- let item = e.dataTransfer.items[i];
- if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
- let str: string;
- let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
- .then(action((s: string) => rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + (str = s)))))
- .then(result => {
- let type = result["content-type"];
- if (type) {
- this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
- .then(doc => doc && this.props.addDocument(doc, false));
- }
+ let batch = UndoManager.StartBatch("collection view drop");
+ let promises: Promise<void>[] = [];
+ // tslint:disable-next-line:prefer-for-of
+ for (let i = 0; i < e.dataTransfer.items.length; i++) {
+ const upload = window.location.origin + RouteStore.upload;
+ let item = e.dataTransfer.items[i];
+ if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
+ let str: string;
+ let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
+ .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+ .then(result => {
+ let type = result["content-type"];
+ if (type) {
+ this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
+ .then(doc => doc && this.props.addDocument(doc, false));
+ }
+ });
+ promises.push(prom);
+ }
+ let type = item.type;
+ if (item.kind === "file") {
+ let file = item.getAsFile();
+ let formData = new FormData();
+
+ if (file) {
+ formData.append('file', file);
+ }
+ let dropFileName = file ? file.name : "-empty-";
+
+ let prom = fetch(upload, {
+ method: 'POST',
+ body: formData
+ }).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 });
+
+ docPromise.then(doc => doc && this.props.addDocument(doc));
+ }));
});
- promises.push(prom);
- }
- let type = item.type;
- if (item.kind === "file") {
- let file = item.getAsFile();
- let dropFileName = file ? file.name : "-empty-";
- let formData = new FormData();
- if (file) formData.append('file', file);
-
- promises.push(fetch(upload, {
- method: 'POST',
- body: formData
- }).then(async (res: Response) =>
- (await res.json()).map(action((file: any) =>
- this.getDocumentFromType(type, window.location.origin + file, { ...options, nativeWidth: 600, width: 300, title: dropFileName }).
- then(doc => doc && this.props.addDocument(doc, false))))));
+ promises.push(prom);
+ }
}
- }
- if (promises.length) {
- Promise.all(promises).finally(() => batch.end());
- } else {
- batch.end();
+ if (promises.length) {
+ Promise.all(promises).finally(() => batch.end());
+ } else {
+ batch.end();
+ }
}
}
+ return CollectionSubView;
}
+
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 8ecc5b67b..411d67ff7 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -23,37 +23,37 @@
margin: 5px 0;
}
- .collection-child {
- margin-top: 10px;
- margin-bottom: 10px;
- }
.no-indent {
padding-left: 0;
}
.bullet {
- width: 1.5em;
- display: inline-block;
+ float:left;
+ position: relative;
+ width: 15px;
+ display: block;
color: $intermediate-color;
- }
-
- .coll-title {
- font-size: 24px;
- margin-bottom: 20px;
+ margin-top: 3px;
+ transform: scale(1.3,1.3);
}
.docContainer {
- display: inline-table;
+ margin-left: 10px;
+ display: block;
+ // width:100%;//width: max-content;
}
-
.docContainer:hover {
- .delete-button {
- display: inline;
- // width: auto;
+ .treeViewItem-openRight {
+ display:inline;
}
}
+
+ .editableView-container {
+ font-weight: bold;
+ }
+
.delete-button {
color: $intermediate-color;
// float: right;
@@ -61,4 +61,28 @@
// 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 905b48db7..6fa374464 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,24 +1,33 @@
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 } from "mobx";
+import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import { DragManager, SetupDrag } from "../../util/DragManager";
+import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
import { EditableView } from "../EditableView";
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, DocListCast } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/RefField';
+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 { Utils } from '../../../Utils';
+import { List } from '../../../new_fields/List';
+import { indexOf } from 'typescript-collections/dist/lib/arrays';
export interface TreeViewProps {
- document: Document;
- deleteDoc: (doc: Document) => void;
+ document: Doc;
+ deleteDoc: (doc: Doc) => void;
moveDocument: DragManager.MoveFunction;
- copyOnDrag: boolean;
+ dropAction: "alias" | "copy" | undefined;
}
export enum BulletType {
@@ -28,6 +37,7 @@ export enum BulletType {
}
library.add(faTrashAlt);
+library.add(faAngleRight);
library.add(faCaretDown);
library.add(faCaretRight);
@@ -39,13 +49,29 @@ 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) {
+ Main.Instance.openWorkspace(this.props.document);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(this.props.document);
+ }
+ };
+
+ 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) => {
- var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- if (children && children !== FieldWaiting) {
- children.Data.splice(children.Data.indexOf(document), 1);
+ let children = Cast(this.props.document.data, listSpec(Doc), []);
+ if (children) {
+ children.splice(children.indexOf(document), 1);
}
}
@@ -74,86 +100,132 @@ class TreeView extends React.Component<TreeViewProps> {
*/
renderTitle() {
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.copyOnDrag);
+ let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction);
let editableView = (titleString: string) =>
(<EditableView
display={"inline"}
contents={titleString}
height={36}
- GetValue={() => this.props.document.Title}
+ GetValue={() => StrCast(this.props.document.title)}
SetValue={(value: string) => {
- this.props.document.SetText(KeyStore.Title, value);
+ let target = this.props.document.proto ? this.props.document.proto : this.props.document;
+ target.title = value;
return true;
}}
/>);
+ let dataDocs = 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}>
- {editableView(this.props.document.Title)}
- <div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>
+ <div className="docContainer" ref={reference} onPointerDown={onItemDown}
+ style={{ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {editableView(StrCast(this.props.document.title))}
+ {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)) });
+ }
+ 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; }
+
render() {
let bulletType = BulletType.List;
- let childElements: JSX.Element | undefined = undefined;
- var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- if (children && children !== FieldWaiting) { // add children for a collection
- if (!this._collapsed) {
- bulletType = BulletType.Collapsible;
- childElements = <ul>
- {children.Data.map(value => <TreeView key={value.Id} document={value} deleteDoc={this.remove} moveDocument={this.move} copyOnDrag={this.props.copyOnDrag} />)}
- </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)));
}
- return <div className="treeViewItem-container" >
+ keys.map(key => {
+ let docList = Cast(this.props.document[key], listSpec(Doc));
+ if (docList instanceof List && docList.length && docList[0] instanceof Doc) {
+ if (!this._collapsed) {
+ bulletType = BulletType.Collapsible;
+ contentElement.push(<ul key={key + "more"}>
+ {(key === "data") ? (null) :
+ <span className="collectionTreeView-keyHeader" key={key}>{key}</span>}
+ {TreeView.GetChildElements(docList, key !== "data", this.remove, this.move, this.props.dropAction)}
+ </ul >);
+ } else
+ bulletType = BulletType.Collapsed;
+ }
+ });
+ return <div className="treeViewItem-container"
+ onContextMenu={this.onWorkspaceContextMenu}>
<li className="collection-child">
{this.renderBullet(bulletType)}
{this.renderTitle()}
- {childElements ? childElements : (null)}
+ {contentElement}
</li>
</div>;
}
+ public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
+ return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child =>
+ <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
+ }
}
@observer
-export class CollectionTreeView extends CollectionSubView {
-
+export class CollectionTreeView extends CollectionSubView(Document) {
@action
remove = (document: Document) => {
- var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- if (children && children !== FieldWaiting) {
- children.Data.splice(children.Data.indexOf(document), 1);
+ let children = Cast(this.props.Document.data, listSpec(Doc), []);
+ if (children) {
+ children.splice(children.indexOf(document), 1);
+ }
+ }
+ 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)) });
}
}
-
render() {
- let children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- let copyOnDrag = this.props.Document.GetBoolean(KeyStore.CopyDraggedItems, false);
- let childrenElement = !children || children === FieldWaiting ? (null) :
- (children.Data.map(value =>
- <TreeView document={value} key={value.Id} deleteDoc={this.remove} moveDocument={this.props.moveDocument} copyOnDrag={copyOnDrag} />)
- );
+ const children = this.children;
+ let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
+ if (!children) {
+ return (null);
+ }
+ let childElements = TreeView.GetChildElements(children, false, this.remove, this.props.moveDocument, dropAction);
return (
<div id="body" className="collectionTreeView-dropTarget"
style={{ borderRadius: "inherit" }}
+ onContextMenu={this.onContextMenu}
onWheel={(e: React.WheelEvent) => e.stopPropagation()}
onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
<div className="coll-title">
<EditableView
- contents={this.props.Document.Title}
+ contents={this.props.Document.title}
display={"inline"}
height={72}
- GetValue={() => this.props.Document.Title}
+ GetValue={() => StrCast(this.props.Document.title)}
SetValue={(value: string) => {
- this.props.Document.SetText(KeyStore.Title, value);
+ let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ target.title = value;
return true;
}} />
</div>
- <hr />
<ul className="no-indent">
- {childrenElement}
+ {childElements}
</ul>
</div >
);
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 779dc8fc3..9dee217cb 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,6 +1,5 @@
import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../fields/KeyStore";
import { ContextMenu } from "../ContextMenu";
import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView";
import React = require("react");
@@ -8,17 +7,18 @@ 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 { VideoBox } from "../nodes/VideoBox";
@observer
export class CollectionVideoView extends React.Component<FieldViewProps> {
- private _intervalTimer: any = undefined;
- private _player: HTMLVideoElement | undefined = undefined;
+ private _videoBox: VideoBox | undefined = undefined;
+ @observable _playTimer?: NodeJS.Timeout = undefined;
@observable _currentTimecode: number = 0;
- @observable _isPlaying: boolean = false;
- public static LayoutString(fieldKey: string = "DataKey") {
+ public static LayoutString(fieldKey: string = "data") {
return FieldView.LayoutString(CollectionVideoView, fieldKey);
}
private get uIButtons() {
@@ -29,7 +29,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
<span style={{ fontSize: 8 }}>{" " + Math.round((this._currentTimecode - Math.trunc(this._currentTimecode)) * 100)}</span>
</div>,
<div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- {this._isPlaying ? "\"" : ">"}
+ {this._playTimer ? "\"" : ">"}
</div>,
<div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
F
@@ -37,60 +37,37 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
]);
}
- _ele: HTMLDivElement | null = null;
@action
- mainCont = (ele: HTMLDivElement | null) => {
- this._ele = ele;
- if (ele) {
- this._player = ele.getElementsByTagName("video")[0];
- console.log(this._player);
- if (this.props.Document.GetNumber(KeyStore.CurPage, -1) >= 0) {
- this._currentTimecode = this.props.Document.GetNumber(KeyStore.CurPage, -1);
- }
+ updateTimecode = () => {
+ if (this._videoBox && this._videoBox.player) {
+ this._currentTimecode = this._videoBox.player.currentTime;
+ this.props.Document.curPage = Math.round(this._currentTimecode);
}
}
- componentDidMount() {
- this._intervalTimer = setInterval(this.updateTimecode, 1000);
- }
+ componentDidMount() { this.updateTimecode(); }
- componentWillUnmount() {
- clearInterval(this._intervalTimer);
- }
-
- @action
- updateTimecode = () => {
- this._player = this._player ? this._player : this._ele ? this._ele.getElementsByTagName("video")[0] : undefined;
- if (this._player) {
- let timecode = (this._player as any).hasOwnProperty("AHackBecauseSomethingResetsTheVideoToZero") ?
- (this._player as any).AHackBecauseSomethingResetsTheVideoToZero : -1;
- if (timecode !== -1 && Object) {
- this._player.currentTime = timecode;
- (this._player as any).AHackBecauseSomethingResetsTheVideoToZero = -1;
- } else {
- this._currentTimecode = this._player.currentTime;
- this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this._currentTimecode));
- }
- }
- }
+ componentWillUnmount() { if (this._playTimer) clearInterval(this._playTimer); }
@action
onPlayDown = () => {
- if (this._player) {
- if (this._player.paused) {
- this._player.play();
- this._isPlaying = true;
+ if (this._videoBox && this._videoBox.player) {
+ if (this._videoBox.player.paused) {
+ this._videoBox.player.play();
+ if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000);
} else {
- this._player.pause();
- this._isPlaying = false;
+ this._videoBox.player.pause();
+ if (this._playTimer) clearInterval(this._playTimer);
+ this._playTimer = undefined;
+
}
}
}
@action
onFullDown = (e: React.PointerEvent) => {
- if (this._player) {
- this._player.requestFullscreen();
+ if (this._videoBox && this._videoBox.player) {
+ this._videoBox.player.requestFullscreen();
e.stopPropagation();
e.preventDefault();
}
@@ -98,33 +75,34 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
@action
onResetDown = () => {
- if (this._player) {
- this._player.pause();
- this._player.currentTime = 0;
+ 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();
}
-
}
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
+ 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 });
}
}
+ setVideoBox = (player: VideoBox) => { this._videoBox = player; }
+
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
- return (
- <>
- <CollectionFreeFormView {...props} CollectionView={this} />
- {this.props.isSelected() ? this.uIButtons : (null)}
- </>
- );
+ return (<>
+ <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} />
+ {this.props.isSelected() ? this.uIButtons : (null)}
+ </>);
}
render() {
- trace();
return (
- <CollectionBaseView {...this.props} className="collectionVideoView-cont" contentRef={this.mainCont} onContextMenu={this.onContextMenu}>
+ <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}>
{this.subView}
</CollectionBaseView>);
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 675e720e2..8c1442d38 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -7,14 +7,15 @@ import { CollectionDockingView } from './CollectionDockingView';
import { CollectionTreeView } from './CollectionTreeView';
import { ContextMenu } from '../ContextMenu';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { KeyStore } from '../../../fields/KeyStore';
import { observer } from 'mobx-react';
import { undoBatch } from '../../util/UndoManager';
import { trace } from 'mobx';
+import { Id } from '../../../new_fields/RefField';
+import { Main } from '../Main';
@observer
export class CollectionView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(CollectionView, fieldStr); }
+ public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); }
private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
@@ -29,13 +30,13 @@ export class CollectionView extends React.Component<FieldViewProps> {
return (null);
}
- get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
+ get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's?
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.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform)) });
- ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema)) });
- ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree)) });
+ 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) });
}
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 8cd6c7624..3b700b053 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,18 +1,18 @@
import { observer } from "mobx-react";
-import { Document } from "../../../../fields/Document";
-import { KeyStore } from "../../../../fields/KeyStore";
import { Utils } from "../../../../Utils";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
import v5 = require("uuid/v5");
+import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types";
+import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
import { InkingControl } from "../../InkingControl";
export interface CollectionFreeFormLinkViewProps {
- A: Document;
- B: Document;
- LinkDocs: Document[];
- addDocument: (document: Document, allowDuplicates?: boolean) => boolean;
- removeDocument: (document: Document) => boolean;
+ A: Doc;
+ B: Doc;
+ LinkDocs: Doc[];
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
}
@observer
@@ -22,14 +22,14 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
if (e.button === 0 && !InkingControl.Instance.selectedTool) {
let a = this.props.A;
let b = this.props.B;
- let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Width() / 2);
- let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Height() / 2);
- let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Width() / 2);
- let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Height() / 2);
+ let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
this.props.LinkDocs.map(l => {
- let width = l.GetNumber(KeyStore.Width, 0);
- l.SetNumber(KeyStore.X, (x1 + x2) / 2 - width / 2);
- l.SetNumber(KeyStore.Y, (y1 + y2) / 2 + 10);
+ let width = l[WidthSym]();
+ l.x = (x1 + x2) / 2 - width / 2;
+ l.y = (y1 + y2) / 2 + 10;
if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
});
e.stopPropagation();
@@ -40,10 +40,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let l = this.props.LinkDocs;
let a = this.props.A;
let b = this.props.B;
- let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Width() / 2);
- let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Height() / 2);
- let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Width() / 2);
- let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Height() / 2);
+ 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);
return (
<>
<line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index b97df7556..2d815a302 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,8 +1,5 @@
import { computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../../fields/Document";
-import { KeyStore } from "../../../../fields/KeyStore";
-import { ListField } from "../../../../fields/ListField";
import { Utils } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
import { DocumentView } from "../../nodes/DocumentView";
@@ -10,57 +7,68 @@ import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
+import { Doc, 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";
@observer
export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
_brushReactionDisposer?: IReactionDisposer;
componentDidMount() {
- this._brushReactionDisposer = reaction(() => this.props.Document.GetList(this.props.fieldKey, [] as Document[]).map(doc => doc.GetNumber(KeyStore.X, 0)),
+ this._brushReactionDisposer = reaction(
() => {
- let views = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc.GetText(KeyStore.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 = srcDoc.GetNumber(KeyStore.X, 0);
- let x1w = srcDoc.GetNumber(KeyStore.Width, -1);
- let x2 = dstDoc.GetNumber(KeyStore.X, 0);
- let x2w = dstDoc.GetNumber(KeyStore.Width, -1);
- if (x1w < 0 || x2w < 0 || i === j) {
- continue;
- }
+ let doclist = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ return { doclist: doclist ? doclist : [], xs: doclist instanceof List ? doclist.map(d => d instanceof Doc && d.x) : [] };
+ },
+ async () => {
+ let doclist = await 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: ListField<Document>) => field.Data.findIndex(brush => {
- let bdocs = brush ? brush.GetList(KeyStore.BrushingDocs, [] as Document[]) : [];
- return (bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false);
- });
- let brushAction = (field: ListField<Document>) => {
- let found = findBrush(field);
- if (found !== -1) {
- console.log("REMOVE BRUSH " + srcTarg.Title + " " + dstTarg.Title);
- field.Data.splice(found, 1);
- }
- };
- if (Math.abs(x1 + x1w - x2) < 20) {
- let linkDoc: Document = new Document();
- linkDoc.SetText(KeyStore.Title, "Histogram Brush");
- linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title);
- linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField);
-
- brushAction = (field: ListField<Document>) => {
- if (findBrush(field) === -1) {
- console.log("ADD BRUSH " + srcTarg.Title + " " + dstTarg.Title);
- (findBrush(field) === -1) && field.Data.push(linkDoc);
+ let x1 = NumCast(srcDoc.x);
+ let x2 = NumCast(dstDoc.x);
+ let x1w = NumCast(srcDoc.width, -1);
+ let x2w = NumCast(dstDoc.width, -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);
}
};
- }
- dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
- srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
+ 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);
+ }
+ };
+ }
+ let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ if (dstBrushDocs === undefined) dstTarg.brushingDocs = dstBrushDocs = new List<Doc>();
+ else brushAction(dstBrushDocs);
+ if (srcBrushDocs === undefined) srcTarg.brushingDocs = srcBrushDocs = new List<Doc>();
+ else brushAction(srcBrushDocs);
+ }
+ })
+ })
});
}
componentWillUnmount() {
@@ -70,15 +78,15 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
}
documentAnchors(view: DocumentView) {
let equalViews = [view];
- let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document);
- if (containerDoc && containerDoc instanceof Document) {
- equalViews.push(...DocumentManager.Instance.getDocumentViews(containerDoc.GetPrototype()!));
+ let containerDoc = FieldValue(Cast(view.props.Document.annotationOn, Doc));
+ if (containerDoc) {
+ equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!);
}
if (view.props.ContainingCollectionView) {
- let collid = view.props.ContainingCollectionView.props.Document.Id;
- this.props.Document.GetList(this.props.fieldKey, [] as Document[]).
+ let collid = view.props.ContainingCollectionView.props.Document[Id];
+ Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).
filter(child =>
- child.Id === collid).map(view =>
+ child[Id] === collid).map(view =>
DocumentManager.Instance.getDocumentViews(view).map(view =>
equalViews.push(view)));
}
@@ -90,12 +98,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
let srcViews = this.documentAnchors(connection.a);
let targetViews = this.documentAnchors(connection.b);
- let possiblePairs: { a: Document, b: Document, }[] = [];
+ 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);
- if (match && !drawnPair.l.reduce((found, link) => found || link.Id === connection.l.Id, false)) {
+ if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
drawnPair.l.push(connection.l);
}
return match || found;
@@ -104,7 +112,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
);
return drawnPairs;
- }, [] as { a: Document, b: Document, l: Document[] }[]);
+ }, [] 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} />);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index cf0a6de00..036745eca 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -1,6 +1,5 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../../fields/KeyStore";
import { CollectionViewProps, CursorEntry } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 67a0e532c..cb849b325 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,91 +1,103 @@
@import "../../globalCssVariables";
-.collectionfreeformview {
- position: inherit;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- transform-origin: left top;
- pointer-events: none;
+
+.collectionfreeformview-ease {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: left top;
+ transition: transform 1s;
}
-.collectionfreeformview-container {
- .collectionfreeformview > .jsx-parser {
+
+.collectionfreeformview-none {
position: inherit;
- height: 100%;
+ top: 0;
+ left: 0;
width: 100%;
- }
+ height: 100%;
+ transform-origin: left top;
+}
- //nested freeform views
- // .collectionfreeformview-container {
+.collectionfreeformview-container {
+ .collectionfreeformview>.jsx-parser {
+ position: inherit;
+ height: 100%;
+ width: 100%;
+ }
+
+ //nested freeform views
+ // .collectionfreeformview-container {
// background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
// linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
// background-size: 30px 30px;
- // }
-
- border-width: $COLLECTION_BORDER_WIDTH;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
- border-color: $light-color-secondary;
- border-style: solid;
- border-radius: $border-radius;
- box-sizing: border-box;
- position: absolute;
- overflow: hidden;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
+ // }
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ border: 0px solid $light-color-secondary;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position: absolute;
+ overflow: hidden;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
}
+
+
.collectionfreeformview-overlay {
- .collectionfreeformview > .jsx-parser {
- position: inherit;
- height: 100%;
- }
- .formattedTextBox-cont {
- background: $light-color-secondary;
- overflow: visible;
- }
-
- opacity: 0.99;
- border-width: 0;
- border-color: transparent;
- border-style: solid;
- border-radius: $border-radius;
- box-sizing: border-box;
- position: absolute;
- overflow: hidden;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- .collectionfreeformview {
+ .collectionfreeformview>.jsx-parser {
+ position: inherit;
+ height: 100%;
+ }
+
.formattedTextBox-cont {
- background:yellow;
+ background: $light-color-secondary;
+ overflow: visible;
+ }
+
+ opacity: 0.99;
+ border: 0px solid transparent;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position:absolute;
+ overflow: hidden;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ .collectionfreeformview {
+ .formattedTextBox-cont {
+ background: yellow;
+ }
}
- }
}
// selection border...?
.border {
- border-style: solid;
- box-sizing: border-box;
- width: 98%;
- height: 98%;
- border-radius: $border-radius;
+ border-style: solid;
+ box-sizing: border-box;
+ width: 98%;
+ height: 98%;
+ border-radius: $border-radius;
}
//this is an animation for the blinking cursor!
@keyframes blink {
- 0% {
- opacity: 0;
- }
- 49% {
- opacity: 0;
- }
- 50% {
- opacity: 1;
- }
+ 0% {
+ opacity: 0;
+ }
+
+ 49% {
+ opacity: 0;
+ }
+
+ 50% {
+ opacity: 1;
+ }
}
#prevCursor {
- animation: blink 1s infinite;
-}
+ animation: blink 1s infinite;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 08c28e76f..7fa945891 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,7 +1,5 @@
-import { action, computed } from "mobx";
+import { action, computed, trace } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../../fields/Document";
-import { KeyStore } from "../../../../fields/KeyStore";
import { emptyFunction, returnFalse, returnOne } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
@@ -12,7 +10,7 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
import { InkingCanvas } from "../../InkingCanvas";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { DocumentViewProps } from "../../nodes/DocumentView";
+import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
import { CollectionSubView } from "../CollectionSubView";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
@@ -20,10 +18,23 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import v5 = require("uuid/v5");
-import { BooleanField } from "../../../../fields/BooleanField";
+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 { pageSchema } from "../../nodes/ImageBox";
+import { Id } from "../../../../new_fields/RefField";
+
+export const panZoomSchema = createSchema({
+ panX: "number",
+ panY: "number",
+ scale: "number"
+});
+
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
+const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema);
@observer
-export class CollectionFreeFormView extends CollectionSubView {
+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;
@@ -31,43 +42,38 @@ export class CollectionFreeFormView extends CollectionSubView {
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
- @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
- @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
+ @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
+ @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
- private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
- private panX = () => this.props.Document.GetNumber(KeyStore.PanX, 0);
- private panY = () => this.props.Document.GetNumber(KeyStore.PanY, 0);
- private zoomScaling = () => this.props.Document.GetNumber(KeyStore.Scale, 1);
+ 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 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 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: Document) => {
- this._selectOnLoaded = newBox.Id;// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+ private addLiveTextBox = (newBox: Doc) => {
+ this._selectOnLoaded = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
this.addDocument(newBox, false);
}
-
- public addDocument = (newBox: Document, allowDuplicates: boolean) => {
+ private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
this.props.addDocument(newBox, false);
this.bringToFront(newBox);
return true;
}
-
- private selectDocuments = (docs: Document[]) => {
+ private selectDocuments = (docs: Doc[]) => {
SelectionManager.DeselectAll;
docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv =>
SelectionManager.SelectDoc(dv!, true));
}
public getActiveDocuments = () => {
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
- return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).reduce((active, doc) => {
- var page = doc.GetNumber(KeyStore.Page, -1);
- if (page === curPage || page === -1) {
- active.push(doc);
- }
- return active;
- }, [] as Document[]);
+ const curPage = FieldValue(this.Document.curPage, -1);
+ return FieldValue(this.children, [] as Doc[]).filter(doc => {
+ var page = NumCast(doc.page, -1);
+ return page === curPage || page === -1;
+ });
}
@undoBatch
@@ -76,22 +82,22 @@ export class CollectionFreeFormView extends CollectionSubView {
if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) {
if (de.data.droppedDocuments.length) {
let dragDoc = de.data.droppedDocuments[0];
- let zoom = dragDoc.GetNumber(KeyStore.ZoomBasis, 1);
+ let zoom = NumCast(dragDoc.zoomBasis, 1);
let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
let x = xp - de.data.xOffset / zoom;
let y = yp - de.data.yOffset / zoom;
- let dropX = de.data.droppedDocuments[0].GetNumber(KeyStore.X, 0);
- let dropY = de.data.droppedDocuments[0].GetNumber(KeyStore.Y, 0);
+ let dropX = NumCast(de.data.droppedDocuments[0].x);
+ let dropY = NumCast(de.data.droppedDocuments[0].y);
de.data.droppedDocuments.map(d => {
- d.SetNumber(KeyStore.X, x + (d.GetNumber(KeyStore.X, 0) - dropX));
- d.SetNumber(KeyStore.Y, y + (d.GetNumber(KeyStore.Y, 0) - dropY));
- if (!d.GetNumber(KeyStore.Width, 0)) {
- d.SetNumber(KeyStore.Width, 300);
+ d.x = x + NumCast(d.x) - dropX;
+ d.y = y + NumCast(d.y) - dropY;
+ if (!NumCast(d.width)) {
+ d.width = 300;
}
- if (!d.GetNumber(KeyStore.Height, 0)) {
- let nw = d.GetNumber(KeyStore.NativeWidth, 0);
- let nh = d.GetNumber(KeyStore.NativeHeight, 0);
- d.SetNumber(KeyStore.Height, nw && nh ? nh / nw * d.Width() : 300);
+ if (!NumCast(d.height)) {
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
}
this.bringToFront(d);
});
@@ -103,14 +109,8 @@ export class CollectionFreeFormView extends CollectionSubView {
}
@action
- cleanupInteractions = () => {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
-
- @action
onPointerDown = (e: React.PointerEvent): void => {
- let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => {
+ 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);
@@ -119,7 +119,8 @@ export class CollectionFreeFormView extends CollectionSubView {
(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())))) {
- this.cleanupInteractions();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
this._lastX = e.pageX;
@@ -128,26 +129,27 @@ export class CollectionFreeFormView extends CollectionSubView {
}
onPointerUp = (e: PointerEvent): void => {
- this.cleanupInteractions();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
}
@action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble) {
- let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
- let y = this.props.Document.GetNumber(KeyStore.PanY, 0);
- let docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]);
+ let x = Cast(this.props.Document.panX, "number", 0);
+ let y = Cast(this.props.Document.panY, "number", 0);
+ let docs = this.children || [];
let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
if (!this.isAnnotationOverlay) {
- let minx = docs.length ? docs[0].GetNumber(KeyStore.X, 0) : 0;
- let maxx = docs.length ? docs[0].Width() + minx : minx;
- let miny = docs.length ? docs[0].GetNumber(KeyStore.Y, 0) : 0;
- let maxy = docs.length ? docs[0].Height() + miny : miny;
+ 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 ranges = docs.filter(doc => doc).reduce((range, doc) => {
- let x = doc.GetNumber(KeyStore.X, 0);
- let xe = x + doc.GetNumber(KeyStore.Width, 0);
- let y = doc.GetNumber(KeyStore.Y, 0);
- let ye = y + doc.GetNumber(KeyStore.Height, 0);
+ 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);
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]]);
@@ -171,10 +173,10 @@ export class CollectionFreeFormView extends CollectionSubView {
// if (!this.props.active()) {
// return;
// }
- let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => {
+ let childSelected = (this.children || []).filter(doc => doc).some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
- return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false);
- }, false);
+ return dv && SelectionManager.IsSelected(dv) ? true : false;
+ });
if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) {
return;
}
@@ -183,8 +185,8 @@ export class CollectionFreeFormView extends CollectionSubView {
if (e.ctrlKey) {
let deltaScale = (1 - (e.deltaY / coefficient));
- this.props.Document.SetNumber(KeyStore.NativeWidth, this.nativeWidth * deltaScale);
- this.props.Document.SetNumber(KeyStore.NativeHeight, this.nativeHeight * deltaScale);
+ this.props.Document.nativeWidth = this.nativeWidth * deltaScale;
+ this.props.Document.nativeHeight = this.nativeHeight * deltaScale;
e.stopPropagation();
e.preventDefault();
} else {
@@ -199,7 +201,7 @@ export class CollectionFreeFormView extends CollectionSubView {
let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
let safeScale = Math.abs(localTransform.Scale);
- this.props.Document.SetNumber(KeyStore.Scale, Math.abs(safeScale));
+ this.props.Document.scale = Math.abs(safeScale);
this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
e.stopPropagation();
}
@@ -210,8 +212,8 @@ export class CollectionFreeFormView extends CollectionSubView {
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));
- this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
- this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
+ this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
}
@action
@@ -223,25 +225,31 @@ export class CollectionFreeFormView extends CollectionSubView {
onDragOver = (): void => {
}
- @action
- bringToFront(doc: Document) {
- let docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).slice();
- docs.sort((doc1, doc2) => {
+ bringToFront = (doc: Doc) => {
+ const docs = (this.children || []);
+ docs.slice().sort((doc1, doc2) => {
if (doc1 === doc) return 1;
if (doc2 === doc) return -1;
- return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0);
- }).map((doc, index) => doc.SetNumber(KeyStore.ZIndex, index + 1));
- doc.SetNumber(KeyStore.ZIndex, docs.length + 1);
+ return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
+ }).forEach((doc, index) => doc.zIndex = index + 1);
+ doc.zIndex = docs.length + 1;
+ return doc;
}
- focusDocument = (doc: Document) => {
+ focusDocument = (doc: Doc) => {
+ SelectionManager.DeselectAll();
+ this.props.Document.panTransformType = "Ease";
this.setPan(
- doc.GetNumber(KeyStore.X, 0) + doc.Width() / 2,
- doc.GetNumber(KeyStore.Y, 0) + doc.Height() / 2);
+ 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: Document): DocumentViewProps {
+
+ getDocumentViewProps(document: Doc): DocumentViewProps {
return {
Document: document,
toggleMinimized: emptyFunction,
@@ -250,26 +258,29 @@ export class CollectionFreeFormView extends CollectionSubView {
moveDocument: this.props.moveDocument,
ScreenToLocalTransform: this.getTransform,
isTopMost: false,
- selectOnLoad: document.Id === this._selectOnLoaded,
- PanelWidth: document.Width,
- PanelHeight: document.Height,
+ selectOnLoad: document[Id] === this._selectOnLoaded,
+ PanelWidth: document[WidthSym],
+ PanelHeight: document[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
parentActive: this.props.active,
- whenActiveChanged: this.props.active,
+ whenActiveChanged: this.props.whenActiveChanged,
+ bringToFront: this.bringToFront,
};
}
- @computed
+ @computed.struct
get views() {
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
- let docviews = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => {
- var page = doc.GetNumber(KeyStore.Page, -1);
+ let curPage = FieldValue(this.Document.curPage, -1);
+ let docviews = (this.children || []).filter(doc => doc).reduce((prev, doc) => {
+ if (!(doc instanceof Doc)) return prev;
+ var page = NumCast(doc.page, -1);
if (page === curPage || page === -1) {
- let minim = doc.GetT(KeyStore.IsMinimized, BooleanField);
- if (minim === undefined || (minim && !minim.Data))
- prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
+ let minim = Cast(doc.isMinimized, "boolean");
+ if (minim === undefined || !minim) {
+ prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
+ }
}
return prev;
}, [] as JSX.Element[]);
@@ -287,26 +298,23 @@ export class CollectionFreeFormView extends CollectionSubView {
private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />];
render() {
const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
+ const easing = () => this.props.Document.panTransformType === "Ease";
return (
<div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel}
style={{ borderRadius: "inherit" }}
onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} >
- {/* <svg viewBox="0 0 180 18" style={{ top: "50%", opacity: 0.05, position: "absolute" }}>
- <text y="15" >
- {this.props.Document.Title}
- </text>
- </svg> */}
<MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
<CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
- zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+ easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+
<CollectionFreeFormLinksView {...this.props} key="freeformLinks">
<InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
{this.childViews}
</InkingCanvas>
</CollectionFreeFormLinksView>
- <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
+ {/* <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> */}
</CollectionFreeFormViewPannableContents>
<CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
</MarqueeView>
@@ -318,9 +326,9 @@ export class CollectionFreeFormView extends CollectionSubView {
@observer
class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
@computed get overlayView() {
- let overlayLayout = this.props.Document.GetText(KeyStore.OverlayLayout, "");
+ let overlayLayout = Cast(this.props.Document.overlayLayout, "string", "");
return !overlayLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={KeyStore.OverlayLayout}
+ (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />);
}
render() {
@@ -331,9 +339,9 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
@observer
class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get backgroundView() {
- let backgroundLayout = this.props.Document.GetText(KeyStore.BackgroundLayout, "");
+ let backgroundLayout = Cast(this.props.Document.backgroundLayout, "string", "");
return !backgroundLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={KeyStore.BackgroundLayout}
+ (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
@@ -347,17 +355,19 @@ interface CollectionFreeFormViewPannableContentsProps {
panX: () => number;
panY: () => number;
zoomScaling: () => number;
+ easing: () => boolean;
}
@observer
class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{
render() {
+ let freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none");
const cenx = this.props.centeringShiftX();
const ceny = this.props.centeringShiftY();
const panx = -this.props.panX();
const pany = -this.props.panY();
const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire
- return <div className="collectionfreeformview" style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}>
+ return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}>
{this.props.children}
</div>;
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index da1170759..c9b0b28f7 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,29 +1,34 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../../fields/Document";
-import { FieldWaiting } from "../../../../fields/Field";
-import { InkField, StrokeData } from "../../../../fields/InkField";
-import { KeyStore } from "../../../../fields/KeyStore";
-import { Documents } from "../../../documents/Documents";
+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";
import "./MarqueeView.scss";
import React = require("react");
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 { emitKeypressEvents } from "readline";
+import { listSpec } from "../../../../new_fields/Schema";
+import { undo } from "prosemirror-history";
+import { FormattedTextBox } from "../../nodes/FormattedTextBox";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
getTransform: () => Transform;
container: CollectionFreeFormView;
- addDocument: (doc: Document, allowDuplicates: false) => boolean;
- activeDocuments: () => Document[];
- selectDocuments: (docs: Document[]) => void;
- removeDocument: (doc: Document) => boolean;
- addLiveTextDocument: (doc: Document) => void;
+ addDocument: (doc: Doc, allowDuplicates: false) => boolean;
+ activeDocuments: () => Doc[];
+ selectDocuments: (docs: Doc[]) => void;
+ removeDocument: (doc: Doc) => boolean;
+ addLiveTextDocument: (doc: Doc) => void;
isSelected: () => boolean;
}
@@ -47,12 +52,32 @@ 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 = Documents.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 = await navigator.clipboard.readText();
+ let ns = text.split("\n").filter(t => t != "\r");
+ for (let i = 0; i < ns.length - 1; i++) {
+ while (!(ns[i].endsWith("-\r") || ns[i].endsWith(".\r") || ns[i].endsWith(":\r")) && i < ns.length - 1) {
+ ns.splice(i, 2, ns[i].substr(0, ns[i].length - 1) + ns[i + 1].trimLeft());
+ }
+ }
+ 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 {
+ let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ this.props.addLiveTextDocument(newBox);
+ }
e.stopPropagation();
}
@action
@@ -67,8 +92,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
document.addEventListener("pointerup", this.onPointerUp, true);
document.addEventListener("keydown", this.marqueeCommand, true);
}
- if (e.altKey)
+ if (e.altKey) {
e.preventDefault();
+ }
}
@action
@@ -85,8 +111,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
e.preventDefault();
}
}
- if (e.altKey)
+ if (e.altKey) {
e.preventDefault();
+ }
}
@action
@@ -99,17 +126,16 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
}
this.cleanupInteractions(true);
- if (e.altKey)
+ if (e.altKey) {
e.preventDefault();
+ }
}
@action
onClick = (e: React.MouseEvent): void => {
if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- if (this.props.isSelected()) {
- PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress);
- }
+ PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress);
// let the DocumentView stopPropagation of this event when it selects this document
} else { // why do we get a click event when the cursor have moved a big distance?
// let's cut it off here so no one else has to deal with it.
@@ -140,47 +166,69 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
if (e.key === "Backspace" || e.key === "Delete" || e.key == "d") {
this._commandExecuted = true;
this.marqueeSelect().map(d => this.props.removeDocument(d));
- let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
- if (ink && ink !== FieldWaiting) {
- this.marqueeInkDelete(ink.Data);
+ let ink = Cast(this.props.container.props.Document.ink, InkField);
+ if (ink) {
+ this.marqueeInkDelete(ink.inkData);
}
this.cleanupInteractions(false);
e.stopPropagation();
}
- if (e.key === "c" || e.key === "r" || e.key === "e") {
+ if (e.key === "c" || e.key === "r" || e.key === "R" || e.key === "e") {
this._commandExecuted = true;
e.stopPropagation();
let bounds = this.Bounds;
let selected = this.marqueeSelect().map(d => {
- this.props.removeDocument(d);
- d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2);
- d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2);
- d.SetNumber(KeyStore.Page, -1);
+ if (e.key !== "R") {
+ 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 ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
- let inkData = ink && ink !== FieldWaiting ? ink.Data : undefined;
- let zoomBasis = this.props.container.props.Document.GetNumber(KeyStore.Scale, 1);
- let newCollection = Documents.FreeformDocument(selected, {
+ 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);
+ let newCollection = Docs.FreeformDocument(selected, {
x: bounds.left,
y: bounds.top,
- panx: 0,
- pany: 0,
+ panX: 0,
+ panY: 0,
borderRounding: e.key === "e" ? -1 : undefined,
scale: zoomBasis,
width: bounds.width * zoomBasis,
height: bounds.height * zoomBasis,
- ink: inkData ? this.marqueeInkSelect(inkData) : undefined,
+ ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
title: "a nested collection"
});
this.marqueeInkDelete(inkData);
// SelectionManager.DeselectAll();
- if (e.key === "r") {
- let summary = Documents.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
- summary.GetPrototype()!.CreateLink(newCollection.GetPrototype()!);
- this.props.addLiveTextDocument(summary);
+ if (e.key === "r" || e.key === "R") {
e.preventDefault();
+ let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top);
+ let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
+
+ if (e.key === "r") {
+ summary.proto!.maximizeOnRight = true;
+ let list = Cast(newCollection.data, listSpec(Doc));
+ if (list && list.length === 1) {
+ selected = list;
+ } else {
+ selected = [newCollection];
+ this.props.addDocument(newCollection, false);
+ }
+ }
+ summary.proto!.maximizedDocs = new List<Doc>(selected);
+ summary.proto!.isButton = true;
+ 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])
+ });
+ this.props.addLiveTextDocument(summary);
}
else {
this.props.addDocument(newCollection, false);
@@ -194,9 +242,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let bounds = this.Bounds;
let selected = this.marqueeSelect();
SelectionManager.DeselectAll();
- let summary = Documents.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
+ 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.map(select => summary.GetPrototype()!.CreateLink(select.GetPrototype()!));
+ selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!));
this.cleanupInteractions(false);
}
@@ -231,19 +279,19 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let idata = new Map();
ink.forEach((value: StrokeData, key: string, map: any) =>
!InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
- this.props.container.props.Document.SetDataOnPrototype(KeyStore.Ink, idata, InkField);
+ Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata));
}
}
marqueeSelect() {
let selRect = this.Bounds;
- let selection: Document[] = [];
+ let selection: Doc[] = [];
this.props.activeDocuments().map(doc => {
- var z = doc.GetNumber(KeyStore.ZoomBasis, 1);
- var x = doc.GetNumber(KeyStore.X, 0);
- var y = doc.GetNumber(KeyStore.Y, 0);
- var w = doc.Width() / z;
- var h = doc.Height() / z;
+ var z = NumCast(doc.zoomBasis, 1);
+ var x = NumCast(doc.x);
+ var y = NumCast(doc.y);
+ var w = NumCast(doc.width) / z;
+ var h = NumCast(doc.height) / z;
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
index 4f68b71b0..cb4d1ad87 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -1,7 +1,7 @@
@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);
+$light-color-secondary:#f1efeb;
$main-accent: #61aaa3;
// $alt-accent: #cdd5ec;
// $alt-accent: #cdeceb;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 1493ff25b..be12dced3 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,36 +1,19 @@
import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
-import { FieldWaiting } from '../../../fields/Field';
import { observer } from "mobx-react";
-import { ContextMenu } from "../../views/ContextMenu";
-import { observable, action } from 'mobx';
-import { KeyStore } from '../../../fields/KeyStore';
-import { AudioField } from "../../../fields/AudioField";
import "./AudioBox.scss";
-import { NumberField } from "../../../fields/NumberField";
+import { Cast } from "../../../new_fields/Types";
+import { AudioField } from "../../../new_fields/URLField";
+const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
@observer
export class AudioBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(AudioBox); }
- constructor(props: FieldViewProps) {
- super(props);
- }
-
-
-
- componentDidMount() {
- }
-
- componentWillUnmount() {
- }
-
-
render() {
- let field = this.props.Document.Get(this.props.fieldKey);
- let path = field === FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3" :
- field instanceof AudioField ? field.Data.href : "http://techslides.com/demos/samples/sample.mp3";
+ let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
+ let path = field.url.href;
return (
<div>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index bad78cbd5..df78d92e2 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,55 +1,68 @@
-import { computed, trace, action } from "mobx";
+import { computed, trace, action, reaction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../fields/KeyStore";
-import { NumberField } from "../../../fields/NumberField";
-import { Document } from "../../../fields/Document";
import { Transform } from "../../util/Transform";
-import { DocumentView, DocumentViewProps } from "./DocumentView";
+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 { SelectionManager } from "../../util/SelectionManager";
-import { ListField } from "../../../fields/ListField";
-import { BooleanField } from "../../../fields/BooleanField";
-import { matchedData } from "express-validator/filter";
+import { Doc, DocListCast, HeightSym } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
}
+const schema = createSchema({
+ zoomBasis: "number",
+ zIndex: "number"
+});
+
+//TODO Types: The import order is wrong, so positionSchema is undefined
+type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>;
+const FreeformDocument = makeInterface(schema, positionSchema);
+
@observer
-export class CollectionFreeFormDocumentView extends React.Component<CollectionFreeFormDocumentViewProps> {
+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() {
return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `;
}
- @computed get X() { return this.props.Document.GetNumber(KeyStore.X, 0); }
- @computed get Y() { return this.props.Document.GetNumber(KeyStore.Y, 0); }
- @computed get zoom() { return 1 / this.props.Document.GetNumber(KeyStore.ZoomBasis, 1); }
- @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
- @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
- @computed get width() { return this.props.Document.Width(); }
- @computed get height() { return this.props.Document.Height(); }
- @computed get zIndex() { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); }
+
+ @computed get X() { return FieldValue(this.Document.x, 0); }
+ @computed get Y() { return FieldValue(this.Document.y, 0); }
+ @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); }
+
set width(w: number) {
- this.props.Document.SetData(KeyStore.Width, w, NumberField);
+ this.Document.width = w;
if (this.nativeWidth && this.nativeHeight) {
- this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w);
+ this.Document.height = this.nativeHeight / this.nativeWidth * w;
}
}
set height(h: number) {
- this.props.Document.SetData(KeyStore.Height, h, NumberField);
+ this.Document.height = h;
if (this.nativeWidth && this.nativeHeight) {
- this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h);
+ this.Document.width = this.nativeWidth / this.nativeHeight * h;
}
}
set zIndex(h: number) {
- this.props.Document.SetData(KeyStore.ZIndex, h, NumberField);
+ this.Document.zIndex = h;
}
- contentScaling = () => (this.nativeWidth > 0 ? this.width / this.nativeWidth : 1);
+ contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
toggleMinimized = () => this.toggleIcon();
@@ -59,7 +72,7 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr
@computed
get docView() {
- return <DocumentView {...OmitKeys(this.props, ['zoomFade'])}
+ return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit}
toggleMinimized={this.toggleMinimized}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
@@ -68,98 +81,138 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr
/>;
}
- animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Document, maximizing: boolean) {
+ componentDidMount() {
+ this._bringToFrontDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => {
+ 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);
+ }
+ }, { 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) {
+ if (first) {
+ if (maximizing) target.width = target.height = 1;
+ }
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.SetNumber(KeyStore.Width, maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress);
- target.SetNumber(KeyStore.Height, maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress);
- target.SetNumber(KeyStore.X, pval[0]);
- target.SetNumber(KeyStore.Y, pval[1]);
- if (first) {
- target.SetBoolean(KeyStore.IsMinimized, false);
- }
+ 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];
if (now < stime + 200) {
this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing);
}
else {
if (!maximizing) {
- target.SetBoolean(KeyStore.IsMinimized, true);
- target.SetNumber(KeyStore.X, targ[0]);
- target.SetNumber(KeyStore.Y, targ[1]);
- target.SetNumber(KeyStore.Width, width);
- target.SetNumber(KeyStore.Height, height);
+ target.isMinimized = true;
+ target.x = targ[0];
+ target.y = targ[1];
+ target.width = width;
+ target.height = height;
}
- (target as any).isIconAnimating = false;
+ target.isIconAnimating = undefined;
}
},
2);
}
@action
public toggleIcon = async (): Promise<void> => {
+ UndoManager.GetOpenBatches().forEach(batch => console.log(batch.batchName));
SelectionManager.DeselectAll();
let isMinimized: boolean | undefined;
- let minimizedDocSet = await this.props.Document.GetTAsync(KeyStore.LinkTags, ListField);
- if (!minimizedDocSet) return;
- minimizedDocSet.Data.map(async minimizedDoc => {
- if (minimizedDoc instanceof Document) {
- this.props.addDocument && this.props.addDocument(minimizedDoc, false);
- let maximizedDoc = await minimizedDoc.GetTAsync(KeyStore.MaximizedDoc, Document);
- if (maximizedDoc instanceof Document && !(maximizedDoc as any).isIconAnimating) {
- (maximizedDoc as any).isIconAnimating = true;
+ let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
+ let minimizedDoc: Doc | undefined = this.props.Document;
+ if (!maximizedDocs) {
+ minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
+ if (minimizedDoc) maximizedDocs = await DocListCast(minimizedDoc.maximizedDocs);
+ }
+ if (minimizedDoc && maximizedDocs) {
+ let minimizedTarget = minimizedDoc;
+ CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+ maximizedDocs.forEach(maximizedDoc => {
+ let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
+ if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
if (isMinimized === undefined) {
- let maximizedDocMinimizedState = await maximizedDoc.GetTAsync(KeyStore.IsMinimized, BooleanField);
- isMinimized = (maximizedDocMinimizedState && maximizedDocMinimizedState.Data) ? true : false;
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
}
- let minx = await minimizedDoc.GetTAsync(KeyStore.X, NumberField);
- let miny = await minimizedDoc.GetTAsync(KeyStore.Y, NumberField);
- let maxx = await maximizedDoc.GetTAsync(KeyStore.X, NumberField);
- let maxy = await maximizedDoc.GetTAsync(KeyStore.Y, NumberField);
- let maxw = await maximizedDoc.GetTAsync(KeyStore.Width, NumberField);
- let maxh = await maximizedDoc.GetTAsync(KeyStore.Height, NumberField);
+ let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale / 2;
+ let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale / 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)
- this.animateBetweenIcon(
- true,
- [minx.Data, miny.Data], [maxx.Data, maxy.Data], maxw.Data, maxh.Data,
- Date.now(), maximizedDoc, isMinimized);
+ maxw !== undefined && maxh !== undefined) {
+ let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
+ if (isMinimized) maximizedDoc.isMinimized = false;
+ maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
+ }
}
-
- }
- })
+ });
+ setTimeout(() => {
+ CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end();
+ CollectionFreeFormDocumentView._undoBatch = undefined;
+ }, 500);
+ }
}
+ static _undoBatch?: UndoManager.Batch = undefined;
onPointerDown = (e: React.PointerEvent): void => {
this._downX = e.clientX;
this._downY = e.clientY;
e.stopPropagation();
}
- onClick = (e: React.MouseEvent): void => {
+ onClick = async (e: React.MouseEvent) => {
e.stopPropagation();
+ let altKey = e.altKey;
if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- this.props.Document.GetTAsync(KeyStore.MaximizedDoc, Document).then(maxdoc => {
- if (maxdoc instanceof Document) { // bcz: need a better way to associate behaviors with click events on widget-documents
- this.props.addDocument && this.props.addDocument(maxdoc, false);
- this.toggleIcon();
+ if (BoolCast(this.props.Document.isButton, false) || (e.target as any).className === "isBullet") {
+ let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
+ if (maximizedDocs) { // bcz: need a better way to associate behaviors with click events on widget-documents
+ if ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight)) {
+ let dataDocs = await DocListCast(CollectionDockingView.Instance.props.Document.data);
+ if (dataDocs) {
+ SelectionManager.DeselectAll();
+ maximizedDocs.forEach(maxDoc => {
+ if (!dataDocs || dataDocs.indexOf(maxDoc) == -1) {
+ CollectionDockingView.Instance.AddRightSplit(maxDoc);
+ } else {
+ CollectionDockingView.Instance.CloseRightSplit(maxDoc);
+ }
+ });
+ }
+ } else {
+ this.props.addDocument && maximizedDocs.forEach(async maxDoc => this.props.addDocument!(await 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 = this.props.Document.GetNumber(KeyStore.BorderRounding, 0);
+ let br = NumCast(this.props.Document.borderRounding);
return br >= 0 ? br :
- this.props.Document.GetNumber(KeyStore.NativeWidth, 0) === 0 ?
+ NumCast(this.props.Document.nativeWidth) === 0 ?
Math.min(this.props.PanelWidth(), this.props.PanelHeight())
- :
- Math.min(this.props.Document.GetNumber(KeyStore.NativeWidth, 0), this.props.Document.GetNumber(KeyStore.NativeHeight, 0));
+ : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0);
}
render() {
- let maximizedDoc = this.props.Document.GetT(KeyStore.MaximizedDoc, Document);
+ 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();
@@ -167,7 +220,7 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr
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 = 1800;
+ 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;
@@ -175,8 +228,12 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr
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",
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index e9c46aa9d..bbc927b5a 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,9 +1,5 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { FieldWaiting, Field } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
@@ -19,55 +15,67 @@ import { IconBox } from "./IconBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
import { VideoBox } from "./VideoBox";
+import { FieldView } from "./FieldView";
import { WebBox } from "./WebBox";
import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import React = require("react");
-import { Document } from "../../../fields/Document";
import { FieldViewProps } from "./FieldView";
import { Without, OmitKeys } from "../../../Utils";
+import { Cast, StrCast } 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?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
export interface JsxBindings {
props: BindingProps;
- [keyName: string]: BindingProps | Field;
}
+class ObserverJsxParser1 extends JsxParser {
+ constructor(props: any) {
+ super(props);
+ observer(this as any);
+ }
+}
+
+const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
+
@observer
export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: () => boolean,
select: (ctrl: boolean) => void,
- layoutKey: Key
+ layoutKey: string
}> {
- @computed get layout(): string { return this.props.Document.GetText(this.props.layoutKey, "<p>Error loading layout data</p>"); }
- @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); }
- @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); }
-
+ @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "<p>Error loading layout data</p>"); }
CreateBindings(): JsxBindings {
- let bindings: JsxBindings = { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive) };
+ return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
+ }
- let keys: Key[] = [];
- keys.push(...this.layoutKeys, KeyStore.Caption) // bcz: hack to get templates to work
- for (const key of keys) {
- bindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data
- }
- for (const key of this.layoutFields) {
- let field = this.props.Document.Get(key);
- bindings[key.Name] = field && field !== FieldWaiting ? field.GetValue() : field;
+ @computed get templates(): List<string> {
+ let field = this.props.Document.templates;
+ if (field && field instanceof List) {
+ return field;
}
- return bindings;
+ return new List<string>();
+ }
+ set templates(templates: List<string>) { this.props.Document.templates = templates; }
+ get finalLayout() {
+ const baseLayout = this.layout;
+ let base = baseLayout;
+ let layout = baseLayout;
+
+ this.templates.forEach(template => {
+ layout = template.replace("{layout}", base);
+ base = layout;
+ });
+ return layout;
}
render() {
- let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField);
- if (!lkeys || lkeys === FieldWaiting) {
- return <p>Error loading layout keys</p>;
- }
- return <JsxParser
- components={{ FormattedTextBox, ImageBox, IconBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ return <ObserverJsxParser
+ components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
bindings={this.CreateBindings()}
- jsx={this.layout}
+ jsx={this.finalLayout}
showWarnings={true}
onError={(test: any) => { console.log(test); }}
/>;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e8e4e8d3f..a20a8a93b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,16 +1,9 @@
import { action, computed, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { Field, FieldWaiting, Opt } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import { TemplateField } from "../../../fields/TemplateField";
-import { ServerUtils } from "../../../server/ServerUtil";
import { emptyFunction, Utils } from "../../../Utils";
-import { Documents } from "../../documents/Documents";
+import { Docs } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
-import { DragManager } from "../../util/DragManager";
+import { DragManager, dropActionType } from "../../util/DragManager";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
@@ -23,14 +16,31 @@ import { Template, Templates } 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 { MarqueeView } from "../collections/collectionFreeForm/MarqueeView";
-import { TextField } from "../../../fields/TextField";
+import { DocServer } from "../../DocServer";
+import { Id } from "../../../new_fields/RefField";
+import { PresentationView } from "../PresentationView";
+
+const linkSchema = createSchema({
+ title: "string",
+ linkDescription: "string",
+ linkTags: "string",
+ linkedTo: Doc,
+ linkedFrom: Doc
+});
+
+type LinkDoc = makeInterface<[typeof linkSchema]>;
+const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- Document: Document;
+ Document: Doc;
addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean;
removeDocument?: (doc: Document) => boolean;
moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
@@ -44,49 +54,33 @@ export interface DocumentViewProps {
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
toggleMinimized: () => void;
-}
-export interface JsxArgs extends DocumentViewProps {
- Keys: { [name: string]: Key };
- Fields: { [name: string]: Field };
+ bringToFront: (doc: Doc) => void;
}
-/*
-This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that
-jsx-to-string can recover the jsx from
-Example usage of this function:
- public static LayoutString() {
- let args = FakeJsxArgs(["Data"]);
- return jsxToString(
- <CollectionFreeFormView
- doc={args.Document}
- fieldKey={args.Keys.Data}
- DocumentViewForField={args.DocumentView} />,
- { useFunctionCode: true, functionNameOnly: true }
- )
- }
-*/
-export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs {
- let Keys: { [name: string]: any } = {};
- let Fields: { [name: string]: any } = {};
- for (const key of keys) {
- Object.defineProperty(emptyFunction, "name", { value: key + "Key" });
- Keys[key] = emptyFunction;
- }
- for (const field of fields) {
- Object.defineProperty(emptyFunction, "name", { value: field });
- Fields[field] = emptyFunction;
- }
- let args: JsxArgs = {
- Document: function Document() { },
- DocumentView: function DocumentView() { },
- Keys,
- Fields
- } as any;
- return args;
-}
+const schema = createSchema({
+ layout: "string",
+ nativeWidth: "number",
+ nativeHeight: "number",
+ backgroundColor: "string"
+});
+
+export const positionSchema = createSchema({
+ nativeWidth: "number",
+ nativeHeight: "number",
+ width: "number",
+ height: "number",
+ x: "number",
+ y: "number",
+});
+
+export type PositionDocument = makeInterface<[typeof positionSchema]>;
+export const PositionDocument = makeInterface(positionSchema);
+
+type Document = makeInterface<[typeof schema]>;
+const Document = makeInterface(schema);
@observer
-export class DocumentView extends React.Component<DocumentViewProps> {
+export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
private _downX: number = 0;
private _downY: number = 0;
private _mainCont = React.createRef<HTMLDivElement>();
@@ -95,14 +89,14 @@ export class DocumentView extends React.Component<DocumentViewProps> {
public get ContentDiv() { return this._mainCont.current; }
@computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
@computed get topMost(): boolean { return this.props.isTopMost; }
- @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); }
- @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); }
- @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); }
- @computed get templates(): Array<Template> {
- let field = this.props.Document.GetT(KeyStore.Templates, TemplateField);
- return !field || field === FieldWaiting ? [] : field.Data;
+ @computed get templates(): List<string> {
+ let field = this.props.Document.templates;
+ if (field && field instanceof List) {
+ return field;
+ }
+ return new List<string>();
}
- set templates(templates: Array<Template>) { this.props.Document.SetData(KeyStore.Templates, templates, TemplateField); }
+ set templates(templates: List<string>) { this.props.Document.templates = templates; }
screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
@action
@@ -137,12 +131,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
e.stopPropagation();
}
- startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) {
+ startDragging(x: number, y: number, dropAction: dropActionType) {
if (this._mainCont.current) {
const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
let dragData = new DragManager.DocumentDragData([this.props.Document]);
const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
- dragData.aliasOnDrop = dropAliasOfDraggedDoc;
+ dragData.dropAction = dropAction;
dragData.xOffset = xoff;
dragData.yOffset = yoff;
dragData.moveDocument = this.props.moveDocument;
@@ -150,13 +144,13 @@ export class DocumentView extends React.Component<DocumentViewProps> {
handlers: {
dragComplete: action(emptyFunction)
},
- hideSource: !dropAliasOfDraggedDoc
+ hideSource: !dropAction
});
}
}
onClick = (e: React.MouseEvent): void => {
- if (CurrentUserUtils.MainDocId != this.props.Document.Id &&
+ 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);
@@ -170,9 +164,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
if (e.shiftKey && e.buttons === 1) {
if (this.props.isTopMost) {
- this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey);
- } else {
- CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e);
+ this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined);
+ } else if (this.props.Document) {
+ CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
}
e.stopPropagation();
} else if (this.active) {
@@ -180,7 +174,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
- e.preventDefault();
}
}
onPointerMove = (e: PointerEvent): void => {
@@ -189,7 +182,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
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);
+ this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined);
}
}
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
@@ -205,33 +198,35 @@ export class DocumentView extends React.Component<DocumentViewProps> {
this.props.removeDocument && this.props.removeDocument(this.props.Document);
}
fieldsClicked = (e: React.MouseEvent): void => {
- let kvp = Documents.KVPDocument(this.props.Document, { width: 300, height: 300 });
+ let kvp = Docs.KVPDocument(this.props.Document, { title: this.props.Document.title + ".kvp", width: 300, height: 300 });
CollectionDockingView.Instance.AddRightSplit(kvp);
}
- fullScreenClicked = (e: React.MouseEvent): void => {
- CollectionDockingView.Instance.OpenFullScreen((this.props.Document.GetPrototype() as Document).MakeDelegate());
- ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked });
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ makeButton = (e: React.MouseEvent): void => {
+ let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ doc.isButton = !BoolCast(doc.isButton, false);
+ if (doc.isButton && !doc.nativeWidth) {
+ doc.nativeWidth = doc[WidthSym]();
+ doc.nativeHeight = doc[HeightSym]();
+ }
}
- closeFullScreenClicked = (e: React.MouseEvent): void => {
- CollectionDockingView.Instance.CloseFullScreen();
+ fullScreenClicked = (e: React.MouseEvent): void => {
+ const doc = Doc.MakeDelegate(FieldValue(this.Document.proto));
+ if (doc) {
+ CollectionDockingView.Instance.OpenFullScreen(doc);
+ }
ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked });
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ SelectionManager.DeselectAll();
}
-
@undoBatch
@action
- drop = (e: Event, de: DragManager.DropEvent) => {
+ drop = async (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.Document;
- destDoc.GetTAsync(KeyStore.Prototype, Document).then(protoDest =>
- sourceDoc.GetTAsync(KeyStore.Prototype, Document).then(protoSrc =>
- (protoSrc ? protoSrc : sourceDoc).CreateLink(protoDest ? protoDest : destDoc))
- );
+ const protoDest = destDoc.proto;
+ const protoSrc = sourceDoc.proto;
+ Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
e.stopPropagation();
}
}
@@ -240,52 +235,30 @@ export class DocumentView extends React.Component<DocumentViewProps> {
onDrop = (e: React.DragEvent) => {
let text = e.dataTransfer.getData("text/plain");
if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
- let oldLayout = this.props.Document.GetText(KeyStore.Layout, "");
+ let oldLayout = FieldValue(this.Document.layout) || "";
let layout = text.replace("{layout}", oldLayout);
- this.props.Document.SetText(KeyStore.Layout, layout);
+ this.Document.layout = layout;
e.stopPropagation();
e.preventDefault();
}
}
- updateLayout = async () => {
- const baseLayout = await this.props.Document.GetTAsync(KeyStore.BaseLayout, TextField);
- if (baseLayout) {
- let base = baseLayout.Data;
- let layout = baseLayout.Data;
-
- this.templates.forEach(template => {
- let temp = template.Layout;
- layout = temp.replace("{layout}", base);
- base = layout;
- });
-
- this.props.Document.SetText(KeyStore.Layout, layout);
- }
- }
@action
addTemplate = (template: Template) => {
- let templates = this.templates;
- templates.push(template);
- templates = templates.splice(0, templates.length).sort(Templates.sortTemplates);
- this.templates = templates;
- this.updateLayout();
+ this.templates.push(template.Layout);
+ this.templates = this.templates;
}
@action
removeTemplate = (template: Template) => {
- let templates = this.templates;
- for (let i = 0; i < templates.length; i++) {
- let temp = templates[i];
- if (temp.Name === template.Name) {
- templates.splice(i, 1);
+ for (let i = 0; i < this.templates.length; i++) {
+ if (this.templates[i] === template.Layout) {
+ this.templates.splice(i, 1);
break;
}
}
- templates = templates.splice(0, templates.length).sort(Templates.sortTemplates);
- this.templates = templates;
- this.updateLayout();
+ this.templates = this.templates;
}
@action
@@ -299,23 +272,32 @@ export class DocumentView extends React.Component<DocumentViewProps> {
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(ServerUtils.prepend("/doc/" + this.props.Document.Id)) });
- ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document.Id) });
+ 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 });
+ if (!this.topMost) {
+ // DocumentViews should stop propagation of this event
+ e.stopPropagation();
+ }
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
- if (!SelectionManager.IsSelected(this))
+ if (!SelectionManager.IsSelected(this)) {
SelectionManager.SelectDoc(this, false);
+ }
}
+
isSelected = () => SelectionManager.IsSelected(this);
select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed);
- @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
- @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
- @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={KeyStore.Layout} />); }
+
+ @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); }
render() {
var scaling = this.props.ContentScaling();
@@ -327,8 +309,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
ref={this._mainCont}
style={{
borderRadius: "inherit",
- background: this.props.Document.GetText(KeyStore.BackgroundColor, ""),
- width: nativeWidth, height: nativeHeight,
+ background: this.Document.backgroundColor || "",
+ width: nativeWidth,
+ height: nativeHeight,
transform: `scale(${scaling}, ${scaling})`
}}
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
@@ -337,4 +320,4 @@ export class DocumentView extends React.Component<DocumentViewProps> {
</div>
);
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 9e175b0d1..613c24fa4 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,30 +1,23 @@
import React = require("react");
import { observer } from "mobx-react";
import { computed } from "mobx";
-import { Field, FieldWaiting, FieldValue, Opt } from "../../../fields/Field";
-import { Document } from "../../../fields/Document";
-import { TextField } from "../../../fields/TextField";
-import { NumberField } from "../../../fields/NumberField";
-import { RichTextField } from "../../../fields/RichTextField";
-import { ImageField } from "../../../fields/ImageField";
-import { VideoField } from "../../../fields/VideoField";
-import { Key } from "../../../fields/Key";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
-import { WebBox } from "./WebBox";
import { VideoBox } from "./VideoBox";
import { AudioBox } from "./AudioBox";
-import { AudioField } from "../../../fields/AudioField";
-import { ListField } from "../../../fields/ListField";
import { DocumentContentsView } from "./DocumentContentsView";
import { Transform } from "../../util/Transform";
-import { KeyStore } from "../../../fields/KeyStore";
-import { returnFalse, emptyDocFunction, emptyFunction, returnOne, returnZero } from "../../../Utils";
+import { returnFalse, emptyFunction } from "../../../Utils";
import { CollectionView } from "../collections/CollectionView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
-import { IconField } from "../../../fields/IconFIeld";
import { IconBox } from "./IconBox";
+import { Opt, Doc, FieldResult } from "../../../new_fields/Doc";
+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";
//
@@ -33,43 +26,44 @@ import { IconBox } from "./IconBox";
// See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc.
//
export interface FieldViewProps {
- fieldKey: Key;
+ fieldKey: string;
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- Document: Document;
+ Document: Doc;
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
isTopMost: boolean;
selectOnLoad: boolean;
- addDocument?: (document: Document, allowDuplicates?: boolean) => boolean;
- removeDocument?: (document: Document) => boolean;
- moveDocument?: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument?: (document: Doc) => boolean;
+ moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- focus: (doc: Document) => void;
+ focus: (doc: Doc) => void;
PanelWidth: () => number;
PanelHeight: () => number;
+ setVideoBox?: (player: VideoBox) => void;
}
@observer
export class FieldView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") {
- return `<${fieldType.name} {...props} fieldKey={${fieldStr}} />`;
+ public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
+ return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`;
}
@computed
- get field(): FieldValue<Field> {
- const { Document: doc, fieldKey } = this.props;
- return doc.Get(fieldKey);
+ get field(): FieldResult {
+ const { Document, fieldKey } = this.props;
+ return Document[fieldKey];
}
render() {
const field = this.field;
- if (!field) {
+ if (field === undefined) {
return <p>{'<null>'}</p>;
}
- if (field instanceof TextField) {
- return <p>{field.Data}</p>;
- }
+ // if (typeof field === "string") {
+ // return <p>{field}</p>;
+ // }
else if (field instanceof RichTextField) {
return <FormattedTextBox {...this.props} />;
}
@@ -84,8 +78,10 @@ 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 Document) {
+ else if (field instanceof Doc) {
return (
<DocumentContentsView Document={field}
addDocument={undefined}
@@ -96,30 +92,27 @@ export class FieldView extends React.Component<FieldViewProps> {
PanelHeight={() => 100}
isTopMost={true} //TODO Why is this top most?
selectOnLoad={false}
- focus={emptyDocFunction}
+ focus={emptyFunction}
isSelected={this.props.isSelected}
select={returnFalse}
- layoutKey={KeyStore.Layout}
+ layoutKey={"layout"}
ContainingCollectionView={this.props.ContainingCollectionView}
parentActive={this.props.active}
toggleMinimized={emptyFunction}
whenActiveChanged={this.props.whenActiveChanged} />
);
}
- else if (field instanceof ListField) {
+ else if (field instanceof List) {
return (<div>
- {(field as ListField<Field>).Data.map(f => f instanceof Document ? f.Title : f.GetValue().toString()).join(", ")}
+ {field.map(f => f instanceof Doc ? f.title : f.toString()).join(", ")}
</div>);
}
// bcz: this belongs here, but it doesn't render well so taking it out for now
// else if (field instanceof HtmlField) {
// return <WebBox {...this.props} />
// }
- else if (field instanceof NumberField) {
- return <p>{field.Data}</p>;
- }
- else if (field !== FieldWaiting) {
- return <p>{JSON.stringify(field.GetValue())}</p>;
+ else if (!(field instanceof Promise)) {
+ return <p>{JSON.stringify(field)}</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 727d3c0b2..9e58a8e7f 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -11,7 +11,7 @@
}
.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {
- background: $light-color-secondary;
+ background: inherit;
padding: 0;
border-width: 0px;
border-radius: inherit;
@@ -30,11 +30,11 @@
pointer-events: none;
}
.formattedTextBox-inner-rounded {
- height: calc(100% - 40px);
+ height: calc(100% - 25px);
width: calc(100% - 40px);
position: absolute;
- overflow: scroll;
- top: 20;
+ overflow: auto;
+ top: 15;
left: 20;
}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 41ee24498..8d2f1c780 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -4,11 +4,6 @@ import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { FieldWaiting, Opt } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { RichTextField } from "../../../fields/RichTextField";
-import { TextField } from "../../../fields/TextField";
-import { Document } from "../../../fields/Document";
import buildKeymap from "../../util/ProsemirrorKeymap";
import { inpRules } from "../../util/RichTextRules";
import { schema } from "../../util/RichTextSchema";
@@ -20,12 +15,21 @@ 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";
+import { UndoManager } from "../../util/UndoManager";
+const { buildMenuItems } = require("prosemirror-example-setup");
+const { menuBar } = require("prosemirror-menu");
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
-// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name + "Key"}
+// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name}
//
// In Code, the node's HTML is specified in the document's parameterized structure as:
// document.SetField(KeyStore.Layout, "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />");
@@ -44,9 +48,16 @@ export interface FormattedTextBoxOverlay {
isOverlay?: boolean;
}
+const richTextSchema = createSchema({
+ documentText: "string"
+});
+
+type RichTextDocument = makeInterface<[typeof richTextSchema]>;
+const RichTextDocument = makeInterface(richTextSchema);
+
@observer
-export class FormattedTextBox extends React.Component<(FieldViewProps & FormattedTextBoxOverlay)> {
- public static LayoutString(fieldStr: string = "DataKey") {
+export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxOverlay), RichTextDocument>(RichTextDocument) {
+ public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
private _ref: React.RefObject<HTMLDivElement>;
@@ -62,7 +73,6 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
this._ref = React.createRef();
this._proseRef = React.createRef();
- this.onChange = this.onChange.bind(this);
}
_applyingChange: boolean = false;
@@ -73,17 +83,15 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
const state = this._lastState = this._editorView.state.apply(tx);
this._editorView.updateState(state);
this._applyingChange = true;
- this.props.Document.SetDataOnPrototype(
- this.props.fieldKey,
- JSON.stringify(state.toJSON()),
- RichTextField
- );
- this.props.Document.SetDataOnPrototype(KeyStore.DocumentText, state.doc.textBetween(0, state.doc.content.size, "\n\n"), TextField);
+ Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON())));
+ Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n"));
this._applyingChange = false;
- if (this.props.Document.Title.startsWith("-") && this._editorView) {
+ let title = StrCast(this.props.Document.title);
+ if (title && title.startsWith("-") && this._editorView) {
let str = this._editorView.state.doc.textContent;
let titlestr = str.substr(0, Math.min(40, str.length));
- this.props.Document.SetText(KeyStore.Title, "-" + titlestr + (str.length > 40 ? "..." : ""));
+ let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
};
}
}
@@ -111,7 +119,7 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
};
if (this.props.isOverlay) {
- this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc.Id,
+ this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc[Id],
() => {
if (this._editorView) {
this._editorView.destroy();
@@ -127,8 +135,8 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
this._reactionDisposer = reaction(
() => {
- const field = this.props.Document ? this.props.Document.GetT(this.props.fieldKey, RichTextField) : undefined;
- return field && field !== FieldWaiting ? field.Data : undefined;
+ const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined;
+ return field ? field.Data : undefined;
},
field => field && this._editorView && !this._applyingChange &&
this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)))
@@ -136,13 +144,18 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
this.setupEditor(config, this.props.Document);
}
- private setupEditor(config: any, doc?: Document) {
- let field = doc ? doc.GetT(this.props.fieldKey, RichTextField) : undefined;
- if (this._ref.current) {
- this._editorView = new EditorView(this._ref.current, {
+ private setupEditor(config: any, doc?: Doc) {
+ let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined;
+ 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
});
+ 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.substr(3)));
+ }
}
if (this.props.selectOnLoad) {
@@ -166,13 +179,6 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
}
}
- @action
- onChange(e: React.ChangeEvent<HTMLInputElement>) {
- const { fieldKey, Document } = this.props;
- Document.SetOnPrototype(fieldKey, new RichTextField(e.target.value));
- // doc.SetData(fieldKey, e.target.value, RichTextField);
- }
- @action
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
@@ -196,22 +202,30 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
if (!this.props.isOverlay) {
MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform);
} else {
- if (this._ref.current) {
- this._ref.current.scrollTop = MainOverlayTextBox.Instance.TextScroll;
+ if (this._proseRef.current) {
+ this._proseRef.current.scrollTop = MainOverlayTextBox.Instance.TextScroll;
}
}
}
//REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- textCapability = (e: React.MouseEvent): void => { };
+ 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: "Text Capability",
+ description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze",
event: this.textCapability
});
@@ -238,6 +252,11 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
onClick = (e: React.MouseEvent): void => {
this._proseRef.current!.focus();
}
+ onMouseDown = (e: React.MouseEvent): void => {
+ if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
+ e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible
+ }
+ }
tooltipTextMenuPlugin() {
let myprops = this.props;
@@ -258,10 +277,15 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
}
});
}
-
- @action
+ onBlur = (e: any) => {
+ if (this._undoTyping) {
+ this._undoTyping.end();
+ this._undoTyping = undefined;
+ }
+ }
+ public _undoTyping?: UndoManager.Batch;
onKeyPress = (e: React.KeyboardEvent) => {
- if (e.key == "Escape") {
+ if (e.key === "Escape") {
SelectionManager.DeselectAll();
}
e.stopPropagation();
@@ -269,14 +293,23 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
// stop propagation doesn't seem to stop propagation of native keyboard events.
// so we set a flag on the native event that marks that the event's been handled.
(e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
+ if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) {
+ let str = this._editorView.state.doc.textContent;
+ 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 ? "..." : "");
+ }
+ if (!this._undoTyping) {
+ this._undoTyping = UndoManager.StartBatch("undoTyping");
+ }
}
render() {
- let style = this.props.isOverlay ? "-scroll" : "-hidden";
- let rounded = this.props.Document.GetNumber(KeyStore.BorderRounding, 0) < 0 ? "-rounded" : "";
- let color = this.props.Document.GetText(KeyStore.BackgroundColor, "");
- let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
+ 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}
+ <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
pointerEvents: interactive ? "all" : "none",
background: color,
@@ -285,13 +318,15 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
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}
>
- <div className={`formattedTextBox-inner${rounded}`} ref={this._proseRef} />
+ <div className={`formattedTextBox-inner${rounded}`} style={{ 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 ce0ee2e09..85bbdeb59 100644
--- a/src/client/views/nodes/IconBox.scss
+++ b/src/client/views/nodes/IconBox.scss
@@ -4,9 +4,19 @@
position: absolute;
left:0;
top:0;
+ height: 100%;
+ width: max-content;
+ // overflow: hidden;
+ pointer-events: all;
svg {
- width: 100% !important;
+ width: $MINIMIZED_ICON_SIZE !important;
height: 100%;
background: white;
}
+ .iconBox-label {
+ position: inherit;
+ width:max-content;
+ font-size: 14px;
+ margin-top: 3px;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 9c90c0a0e..19abec4af 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -2,14 +2,17 @@ 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 } from "mobx";
+import { computed, observable, runInAction, reaction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
-import { Document } from '../../../fields/Document';
-import { IconField } from "../../../fields/IconFIeld";
-import { KeyStore } from "../../../fields/KeyStore";
-import { SelectionManager } from "../../util/SelectionManager";
import { FieldView, FieldViewProps } from './FieldView';
import "./IconBox.scss";
+import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
+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);
@@ -21,9 +24,19 @@ library.add(faFilm);
@observer
export class IconBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(IconBox); }
+ _reactionDisposer?: IReactionDisposer;
+ componentDidMount() {
+ this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs],
+ async () => {
+ let maxDoc = await DocListCast(this.props.Document.maximizedDocs);
+ this.props.Document.title = (maxDoc && maxDoc.length === 1 ? maxDoc[0].title + ".icon" : "");
+ }, { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ if (this._reactionDisposer) this._reactionDisposer();
+ }
- @computed get maximized() { return this.props.Document.GetT(KeyStore.MaximizedDoc, Document); }
- @computed get layout(): string { return this.props.Document.GetData(this.props.fieldKey, IconField, "<p>Error loading layout data</p>" as string); }
+ @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
@computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); }
public static DocumentIcon(layout: string) {
@@ -33,13 +46,35 @@ export class IconBox extends React.Component<FieldViewProps> {
layout.indexOf("Video") !== -1 ? faFilm :
layout.indexOf("Collection") !== -1 ? faObjectGroup :
faCaretUp;
- return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />
+ return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
}
+ setLabelField = (e: React.MouseEvent): void => {
+ this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({
+ description: BoolCast(this.props.Document.hideLabel) ? "show label" : "hide label",
+ event: this.setLabelField
+ });
+ }
+ @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 firstDoc = maxDoc && maxDoc.length > 0 && maxDoc[0] instanceof Doc ? maxDoc[0] as Doc : undefined;
+ let label = !hideLabel && firstDoc && labelField ? firstDoc[labelField] : "";
return (
- <div className="iconBox-container">
+ <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)); })}>
+ {({ measureRef }) =>
+ <span ref={measureRef} className="iconBox-label">{label}</span>
+ }
+ </Measure>
</div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 9fe211df0..2316a050e 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -10,6 +10,8 @@
}
.imageBox-cont-interactive {
pointer-events: all;
+ width:100%;
+ height:auto;
}
.imageBox-dot {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 2cbb0fa90..0e9e904a8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -3,11 +3,6 @@ import { action, observable } 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
-import { Document } from '../../../fields/Document';
-import { FieldWaiting } from '../../../fields/Field';
-import { ImageField } from '../../../fields/ImageField';
-import { KeyStore } from '../../../fields/KeyStore';
-import { ListField } from '../../../fields/ListField';
import { Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
@@ -15,14 +10,27 @@ import { ContextMenu } from "../../views/ContextMenu";
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
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 { ImageField } from '../../../new_fields/URLField';
+import { List } from '../../../new_fields/List';
import { InkingControl } from '../InkingControl';
-import { NumberField } from '../../../fields/NumberField';
+import { Doc } from '../../../new_fields/Doc';
+
+export const pageSchema = createSchema({
+ curPage: "number"
+});
+
+type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>;
+const ImageDocument = makeInterface(pageSchema, positionSchema);
@observer
-export class ImageBox extends React.Component<FieldViewProps> {
+export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
public static LayoutString() { return FieldView.LayoutString(ImageBox); }
- private _imgRef: React.RefObject<HTMLImageElement>;
+ private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
private _downX: number = 0;
private _downY: number = 0;
private _lastTap: number = 0;
@@ -30,20 +38,13 @@ export class ImageBox extends React.Component<FieldViewProps> {
@observable private _isOpen: boolean = false;
private dropDisposer?: DragManager.DragDropDisposer;
- constructor(props: FieldViewProps) {
- super(props);
-
- this._imgRef = React.createRef();
- }
-
@action
onLoad = (target: any) => {
var h = this._imgRef.current!.naturalHeight;
var w = this._imgRef.current!.naturalWidth;
if (this._photoIndex === 0) {
- this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w);
- this.props.Document.GetTAsync(KeyStore.Width, NumberField, field =>
- field && this.props.Document.SetNumber(KeyStore.Height, field.Data * h / w));
+ this.Document.nativeHeight = FieldValue(this.Document.nativeWidth, 0) * h / w;
+ this.Document.height = FieldValue(this.Document.width, 0) * h / w;
}
}
@@ -66,19 +67,18 @@ export class ImageBox extends React.Component<FieldViewProps> {
@undoBatch
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
- de.data.droppedDocuments.map(action((drop: Document) => {
- let layout = drop.GetText(KeyStore.BackgroundLayout, "");
+ de.data.droppedDocuments.forEach(action((drop: Doc) => {
+ let layout = StrCast(drop.backgroundLayout);
if (layout.indexOf(ImageBox.name) !== -1) {
- let imgData = this.props.Document.Get(KeyStore.Data);
- if (imgData instanceof ImageField && imgData) {
- this.props.Document.SetOnPrototype(KeyStore.Data, new ListField([imgData]));
+ let imgData = this.props.Document[this.props.fieldKey];
+ if (imgData instanceof ImageField) {
+ Doc.SetOnPrototype(this.props.Document, "data", new List([imgData]));
}
- let imgList = this.props.Document.GetList(KeyStore.Data, [] as any[]);
+ let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]);
if (imgList) {
- let field = drop.Get(KeyStore.Data);
- if (field === FieldWaiting) { }
- else if (field instanceof ImageField) imgList.push(field);
- else if (field instanceof ListField) imgList.push(field.Data);
+ let field = drop.data;
+ if (field instanceof ImageField) imgList.push(field);
+ else if (field instanceof List) imgList.concat(field);
}
e.stopPropagation();
}
@@ -128,9 +128,9 @@ export class ImageBox extends React.Component<FieldViewProps> {
}
specificContextMenu = (e: React.MouseEvent): void => {
- let field = this.props.Document.GetT(this.props.fieldKey, ImageField);
- if (field && field !== FieldWaiting) {
- let url = field.Data.href;
+ 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);
@@ -142,27 +142,26 @@ export class ImageBox extends React.Component<FieldViewProps> {
@action
onDotDown(index: number) {
this._photoIndex = index;
- this.props.Document.SetNumber(KeyStore.CurPage, index);
+ this.Document.curPage = index;
}
dots(paths: string[]) {
- let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1);
+ let nativeWidth = FieldValue(this.Document.nativeWidth, 1);
let dist = Math.min(nativeWidth / paths.length, 40);
let left = (nativeWidth - paths.length * dist) / 2;
return paths.map((p, i) =>
<div className="imageBox-placer" key={i} >
- <div className="imageBox-dot" style={{ background: (i == this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
+ <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
</div>
);
}
render() {
- let field = this.props.Document.Get(this.props.fieldKey);
+ let field = this.Document[this.props.fieldKey];
let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"];
- if (field === FieldWaiting) paths = ["https://image.flaticon.com/icons/svg/66/66163.svg"];
- else if (field instanceof ImageField) paths = [field.Data.href];
- else if (field instanceof ListField) paths = field.Data.filter(val => val as ImageField).map(p => (p as ImageField).Data.href);
- let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1);
+ 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);
let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
return (
<div className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index ddbec014b..876a3c173 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,24 +2,22 @@
import { action, computed, 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 { Document } from '../../../fields/Document';
-import { Field, FieldWaiting } from '../../../fields/Field';
-import { Key } from '../../../fields/Key';
-import { KeyStore } from '../../../fields/KeyStore';
-import { CompileScript, ToField } from "../../util/Scripting";
+import { CompileScript } from "../../util/Scripting";
import { FieldView, FieldViewProps } from './FieldView';
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";
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
private _mainCont = React.createRef<HTMLDivElement>();
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+ public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
@observable private _keyInput: string = "";
@observable private _valueInput: string = "";
- @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 50); }
+ @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
constructor(props: FieldViewProps) {
@@ -30,8 +28,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
onEnterKey = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
if (this._keyInput && this._valueInput) {
- let doc = this.props.Document.GetT(KeyStore.Data, Document);
- if (!doc || doc === FieldWaiting) {
+ let doc = FieldValue(Cast(this.props.Document.data, Doc));
+ if (!doc) {
return;
}
let realDoc = doc;
@@ -43,13 +41,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let res = script.run();
if (!res.success) return;
const field = res.result;
- if (field instanceof Field) {
- realDoc.Set(new Key(this._keyInput), field);
- } else {
- let dataField = ToField(field);
- if (dataField) {
- realDoc.Set(new Key(this._keyInput), dataField);
- }
+ if (IsField(field)) {
+ realDoc[this._keyInput] = field;
}
this._keyInput = "";
this._valueInput = "";
@@ -67,16 +60,16 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
createTable = () => {
- let doc = this.props.Document.GetT(KeyStore.Data, Document);
- if (!doc || doc === FieldWaiting) {
+ let doc = FieldValue(Cast(this.props.Document.data, Doc));
+ if (!doc) {
return <tr><td>Loading...</td></tr>;
}
let realDoc = doc;
let ids: { [key: string]: string } = {};
- let protos = doc.GetAllPrototypes();
+ let protos = Doc.GetAllPrototypes(doc);
for (const proto of protos) {
- proto._proxies.forEach((val: any, key: string) => {
+ Object.keys(proto).forEach(key => {
if (!(key in ids)) {
ids[key] = key;
}
@@ -86,7 +79,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let rows: JSX.Element[] = [];
let i = 0;
for (let key in ids) {
- rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />);
+ rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
}
@@ -116,7 +109,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@action
onDividerMove = (e: PointerEvent): void => {
let nativeWidth = this._mainCont.current!.getBoundingClientRect();
- this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)));
+ this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
}
@action
onDividerUp = (e: PointerEvent): void => {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index d480eb5af..203fb5625 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,48 +1,33 @@
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 { Document } from '../../../fields/Document';
-import { Field, Opt } from '../../../fields/Field';
-import { Key } from '../../../fields/Key';
-import { emptyDocFunction, emptyFunction, returnFalse } from '../../../Utils';
-import { Server } from "../../Server";
-import { CompileScript, ToField } from "../../util/Scripting";
+import { emptyFunction, returnFalse, returnZero } from '../../../Utils';
+import { CompileScript } from "../../util/Scripting";
import { Transform } from '../../util/Transform';
import { EditableView } from "../EditableView";
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import "./KeyValuePair.scss";
import React = require("react");
+import { Doc, Opt, IsField } from '../../../new_fields/Doc';
+import { FieldValue } from '../../../new_fields/Types';
// Represents one row in a key value plane
export interface KeyValuePairProps {
rowStyle: string;
- fieldId: string;
- doc: Document;
+ keyName: string;
+ doc: Doc;
keyWidth: number;
}
@observer
export class KeyValuePair extends React.Component<KeyValuePairProps> {
- @observable private key: Opt<Key>;
-
- constructor(props: KeyValuePairProps) {
- super(props);
- Server.GetField(this.props.fieldId,
- action((field: Opt<Field>) => field instanceof Key && (this.key = field)));
-
- }
-
-
render() {
- if (!this.key) {
- return <tr><td>error</td><td /></tr>;
- }
let props: FieldViewProps = {
Document: this.props.doc,
ContainingCollectionView: undefined,
- fieldKey: this.key,
+ fieldKey: this.props.keyName,
isSelected: returnFalse,
select: emptyFunction,
isTopMost: false,
@@ -50,29 +35,35 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
active: returnFalse,
whenActiveChanged: emptyFunction,
ScreenToLocalTransform: Transform.Identity,
- focus: emptyDocFunction,
+ focus: emptyFunction,
+ PanelWidth: returnZero,
+ PanelHeight: returnZero,
};
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 = props.Document.Get(props.fieldKey);
- field && field instanceof Field && props.Document.Set(props.fieldKey, undefined);
+ let field = FieldValue(props.Document[props.fieldKey]);
+ field && (props.Document[props.fieldKey] = undefined);
}}>
X
</button>
- <div className="keyValuePair-keyField">{this.key.Name}</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 = props.Document.Get(props.fieldKey);
- if (field && field instanceof Field) {
- return field.ToScriptString();
+
+ let field = FieldValue(props.Document[props.fieldKey]);
+ if (field) {
+ //TODO Types
+ return String(field);
+ // return field.ToScriptString();
}
- return field || "";
+ return "";
}}
SetValue={(value: string) => {
let script = CompileScript(value, { addReturn: true });
@@ -82,15 +73,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
let res = script.run();
if (!res.success) return false;
const field = res.result;
- if (field instanceof Field) {
- props.Document.Set(props.fieldKey, field);
+ if (IsField(field)) {
+ props.Document[props.fieldKey] = field;
return true;
- } else {
- let dataField = ToField(field);
- if (dataField) {
- props.Document.Set(props.fieldKey, dataField);
- return true;
- }
}
return false;
}}>
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index 8bc70b48f..639f83b38 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -1,14 +1,14 @@
@import "../globalCssVariables";
.link-container {
width: 100%;
- height: 35px;
+ height: 50px;
display: flex;
flex-direction: row;
border-top: 0.5px solid #bababa;
}
.info-container {
- width: 55%;
+ width: 65%;
padding-top: 5px;
padding-left: 5px;
display: flex;
@@ -24,7 +24,8 @@
}
.button-container {
- width: 45%;
+ width: 35%;
+ padding-top: 8px;
display: flex;
flex-direction: row;
}
@@ -49,17 +50,17 @@
cursor: pointer;
}
-.fa-icon-view {
- margin-left: 3px;
- margin-top: 5px;
-}
+// .fa-icon-view {
+// margin-left: 3px;
+// margin-top: 5px;
+// }
.fa-icon-edit {
- margin-left: 5px;
- margin-top: 5px;
+ margin-left: 6px;
+ margin-top: 6px;
}
.fa-icon-delete {
- margin-left: 6px;
- margin-top: 5px;
+ margin-left: 7px;
+ margin-top: 6px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 1c0e316e8..08cfa590b 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -2,15 +2,15 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { KeyStore } from '../../../fields/KeyStore';
-import { ListField } from "../../../fields/ListField";
-import { NumberField } from "../../../fields/NumberField";
import { DocumentManager } from "../../util/DocumentManager";
import { undoBatch } from "../../util/UndoManager";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import './LinkBox.scss';
import React = require("react");
+import { Doc } from '../../../new_fields/Doc';
+import { Cast, NumCast } from '../../../new_fields/Types';
+import { listSpec } from '../../../new_fields/Schema';
+import { action } from 'mobx';
library.add(faEye);
@@ -18,9 +18,9 @@ library.add(faEdit);
library.add(faTimes);
interface Props {
- linkDoc: Document;
+ linkDoc: Doc;
linkName: String;
- pairedDoc: Document;
+ pairedDoc: Doc;
type: String;
showEditor: () => void;
}
@@ -29,62 +29,54 @@ interface Props {
export class LinkBox extends React.Component<Props> {
@undoBatch
- onViewButtonPressed = (e: React.PointerEvent): void => {
+ 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 {
- this.props.pairedDoc.GetAsync(KeyStore.AnnotationOn, (contextDoc: any) => {
- if (!contextDoc) {
- CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc.MakeDelegate());
- } else if (contextDoc instanceof Document) {
- this.props.pairedDoc.GetTAsync(KeyStore.Page, NumberField).then((pfield: any) => {
- contextDoc.GetTAsync(KeyStore.CurPage, NumberField).then((cfield: any) => {
- if (pfield !== cfield) {
- contextDoc.SetNumber(KeyStore.CurPage, pfield.Data);
- }
- let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
- if (contextView) {
- contextView.props.focus(contextDoc);
- } else {
- CollectionDockingView.Instance.AddRightSplit(contextDoc);
- }
- });
- });
+ 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);
+ }
+ }
}
}
onEditButtonPressed = (e: React.PointerEvent): void => {
- console.log("edit down");
e.stopPropagation();
this.props.showEditor();
}
- onDeleteButtonPressed = (e: React.PointerEvent): void => {
- console.log("delete down");
+ @action
+ onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- this.props.linkDoc.GetTAsync(KeyStore.LinkedFromDocs, Document, field => {
- if (field) {
- field.GetTAsync<ListField<Document>>(KeyStore.LinkedToDocs, ListField, field => {
- if (field) {
- field.Data.splice(field.Data.indexOf(this.props.linkDoc));
- }
- });
+ const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
+ if (linkedFrom) {
+ const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
+ if (linkedToDocs) {
+ linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
}
- });
- this.props.linkDoc.GetTAsync(KeyStore.LinkedToDocs, Document, field => {
- if (field) {
- field.GetTAsync<ListField<Document>>(KeyStore.LinkedFromDocs, ListField, field => {
- if (field) {
- field.Data.splice(field.Data.indexOf(this.props.linkDoc));
- }
- });
+ }
+ if (linkedTo) {
+ const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
+ if (linkedFromDocs) {
+ linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
}
- });
+ }
}
render() {
@@ -102,8 +94,8 @@ export class LinkBox extends React.Component<Props> {
</div>
<div className="button-container">
- <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
- <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div>
+ {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
+ <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */}
<div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
<FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div>
<div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index ea2e7289c..9629585d7 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -22,7 +22,7 @@
.save-button {
width: 50px;
- height: 20px;
+ height: 22px;
pointer-events: auto;
background-color: $dark-color;
color: $light-color;
@@ -38,6 +38,5 @@
.save-button:hover {
background: $main-accent;
- transform: scale(1.05);
cursor: pointer;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index bde50fed8..71a423338 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -3,31 +3,30 @@ import React = require("react");
import { SelectionManager } from "../../util/SelectionManager";
import { observer } from "mobx-react";
import './LinkEditor.scss';
-import { KeyStore } from '../../../fields/KeyStore';
import { props } from "bluebird";
import { DocumentView } from "./DocumentView";
-import { Document } from "../../../fields/Document";
-import { TextField } from "../../../fields/TextField";
import { link } from "fs";
+import { StrCast } from "../../../new_fields/Types";
+import { Doc } from "../../../new_fields/Doc";
interface Props {
- linkDoc: Document;
+ linkDoc: Doc;
showLinks: () => void;
}
@observer
export class LinkEditor extends React.Component<Props> {
- @observable private _nameInput: string = this.props.linkDoc.GetText(KeyStore.Title, "");
- @observable private _descriptionInput: string = this.props.linkDoc.GetText(KeyStore.LinkDescription, "");
+ @observable private _nameInput: string = StrCast(this.props.linkDoc.title);
+ @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription);
onSaveButtonPressed = (e: React.PointerEvent): void => {
- console.log("view down");
e.stopPropagation();
- this.props.linkDoc.SetData(KeyStore.Title, this._nameInput, TextField);
- this.props.linkDoc.SetData(KeyStore.LinkDescription, this._descriptionInput, TextField);
+ 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 ac09da305..e21adebbc 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -1,15 +1,14 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from '../../../fields/KeyStore';
-import { ListField } from "../../../fields/ListField";
import { DocumentView } from "./DocumentView";
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";
interface Props {
docView: DocumentView;
@@ -19,28 +18,28 @@ interface Props {
@observer
export class LinkMenu extends React.Component<Props> {
- @observable private _editingLink?: Document;
+ @observable private _editingLink?: Doc;
- renderLinkItems(links: Document[], key: Key, type: string) {
+ renderLinkItems(links: Doc[], key: string, type: string) {
return links.map(link => {
- let doc = link.GetT(key, Document);
- if (doc && doc !== FieldWaiting) {
- return <LinkBox key={doc.Id} linkDoc={link} linkName={link.Title} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
+ 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} />;
}
});
}
render() {
//get list of links from document
- let linkFrom: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []);
- let linkTo: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []);
+ let linkFrom: Doc[] = Cast(this.props.docView.props.Document.linkedFromDocs, listSpec(Doc), []);
+ let linkTo: Doc[] = Cast(this.props.docView.props.Document.linkedToDocs, listSpec(Doc), []);
if (this._editingLink === undefined) {
return (
<div id="linkMenu-container">
<input id="linkMenu-searchBar" type="text" placeholder="Search..."></input>
<div id="linkMenu-list">
- {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Destination: ")}
- {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Source: ")}
+ {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
+ {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
</div>
</div>
);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 226dfba11..eb45ea273 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -6,10 +6,6 @@ import Measure from "react-measure";
//@ts-ignore
import { Document, Page } from "react-pdf";
import 'react-pdf/dist/Page/AnnotationLayer.css';
-import { FieldWaiting, Opt } from '../../../fields/Field';
-import { ImageField } from '../../../fields/ImageField';
-import { KeyStore } from '../../../fields/KeyStore';
-import { PDFField } from '../../../fields/PDFField';
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from '../../../Utils';
import { Annotation } from './Annotation';
@@ -17,6 +13,13 @@ import { FieldView, FieldViewProps } from './FieldView';
import "./PDFBox.scss";
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";
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
@@ -41,8 +44,12 @@ import { InkingControl } from "../InkingControl";
*
* written by: Andrew Kim
*/
+
+type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
+const PdfDocument = makeInterface(positionSchema, pageSchema);
+
@observer
-export class PDFBox extends React.Component<FieldViewProps> {
+export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
public static LayoutString() { return FieldView.LayoutString(PDFBox); }
private _mainDiv = React.createRef<HTMLDivElement>();
@@ -58,8 +65,8 @@ export class PDFBox extends React.Component<FieldViewProps> {
@observable private _interactive: boolean = false;
@observable private _loaded: boolean = false;
- @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 1); }
- @computed private get thumbnailPage() { return this.props.Document.GetNumber(KeyStore.ThumbnailPage, -1); }
+ @computed private get curPage() { return FieldValue(this.Document.curPage, 1); }
+ @computed private get thumbnailPage() { return Cast(this.props.Document.thumbnailPage, "number", -1); }
componentDidMount() {
this._reactionDisposer = reaction(
@@ -240,14 +247,13 @@ export class PDFBox extends React.Component<FieldViewProps> {
saveThumbnail = () => {
this._renderAsSvg = false;
setTimeout(() => {
- var me = this;
- let nwidth = me.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- let nheight = me.props.Document.GetNumber(KeyStore.NativeHeight, 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 })
.then(action((dataUrl: string) => {
- me.props.Document.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField);
- me.props.Document.SetNumber(KeyStore.ThumbnailPage, me.props.Document.GetNumber(KeyStore.CurPage, -1));
- me._renderAsSvg = true;
+ this.props.Document.thumbnail = new ImageField(new URL(dataUrl));
+ this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
+ this._renderAsSvg = true;
}))
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
@@ -258,7 +264,7 @@ export class PDFBox extends React.Component<FieldViewProps> {
@action
onLoaded = (page: any) => {
// bcz: the number of pages should really be set when the document is imported.
- this.props.Document.SetNumber(KeyStore.NumPages, page._transport.numPages);
+ this.props.Document.numPages = page._transport.numPages;
if (this._perPageInfo.length === 0) { //Makes sure it only runs once
this._perPageInfo = [...Array(page._transport.numPages)];
}
@@ -270,11 +276,11 @@ export class PDFBox extends React.Component<FieldViewProps> {
// bcz: the nativeHeight should really be set when the document is imported.
// also, the native dimensions could be different for different pages of the canvas
// so this design is flawed.
- var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- if (!this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) {
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ if (!FieldValue(this.Document.nativeHeight, 0)) {
var nativeHeight = nativeWidth * r.entry.height / r.entry.width;
- this.props.Document.SetNumber(KeyStore.Height, nativeHeight / nativeWidth * this.props.Document.GetNumber(KeyStore.Width, 0));
- this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight);
+ this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
+ this.props.Document.nativeHeight = nativeHeight;
}
}
renderHeight = 2400;
@@ -284,9 +290,11 @@ export class PDFBox extends React.Component<FieldViewProps> {
}
@computed
get pdfContent() {
- let pdfUrl = this.props.Document.GetT(this.props.fieldKey, PDFField);
- let xf = this.props.Document.GetNumber(KeyStore.NativeHeight, 0) / this.renderHeight;
- let body = (this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) ?
+ 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}>
{({ measureRef }) =>
@@ -305,8 +313,8 @@ export class PDFBox extends React.Component<FieldViewProps> {
@computed
get pdfRenderer() {
let proxy = this._loaded ? (null) : this.imageProxyRenderer;
- let pdfUrl = this.props.Document.GetT(this.props.fieldKey, PDFField);
- if ((!this._interactive && proxy) || !pdfUrl || pdfUrl === FieldWaiting) {
+ let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
+ if ((!this._interactive && proxy) || !pdfUrl) {
return proxy;
}
return [
@@ -319,10 +327,10 @@ export class PDFBox extends React.Component<FieldViewProps> {
@computed
get imageProxyRenderer() {
- let thumbField = this.props.Document.Get(KeyStore.Thumbnail);
+ let thumbField = this.props.Document.thumbnail;
if (thumbField) {
- let path = thumbField === FieldWaiting || this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- thumbField instanceof ImageField ? thumbField.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
+ 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%" />;
}
return (null);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b34f1dc08..422508f90 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,75 +1,80 @@
import React = require("react");
-import { action, computed, IReactionDisposer, trace } from "mobx";
import { observer } from "mobx-react";
-import Measure from "react-measure";
-import { FieldWaiting, Opt } from '../../../fields/Field';
-import { KeyStore } from "../../../fields/KeyStore";
-import { VideoField } from '../../../fields/VideoField';
import { FieldView, FieldViewProps } from './FieldView';
import "./VideoBox.scss";
+import { action, computed, trace } 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 { VideoField } from "../../../new_fields/URLField";
+import Measure from "react-measure";
+import "./VideoBox.scss";
+import { Field, FieldResult, Opt } from "../../../new_fields/Doc";
+
+type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
+const VideoDocument = makeInterface(positionSchema, pageSchema);
@observer
-export class VideoBox extends React.Component<FieldViewProps> {
+export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
- private _videoRef = React.createRef<HTMLVideoElement>();
+ private _videoRef: HTMLVideoElement | null = null;
+ private _loaded: boolean = false;
+ private get initialTimecode() { return FieldValue(this.Document.curPage, -1); }
public static LayoutString() { return FieldView.LayoutString(VideoBox); }
- constructor(props: FieldViewProps) {
- super(props);
+ public get player(): HTMLVideoElement | undefined {
+ if (this._videoRef) {
+ return this._videoRef;
+ }
}
-
- @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); }
-
-
- _loaded: boolean = false;
-
@action
setScaling = (r: any) => {
if (this._loaded) {
// bcz: the nativeHeight should really be set when the document is imported.
- // also, the native dimensions could be different for different pages of the PDF
- // so this design is flawed.
- var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
+ 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.props.Document.SetNumber(KeyStore.Height, newNativeHeight / nativeWidth * this.props.Document.GetNumber(KeyStore.Width, 0));
- this.props.Document.SetNumber(KeyStore.NativeHeight, newNativeHeight);
+ this.Document.height = newNativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
+ this.Document.nativeHeight = newNativeHeight;
}
} else {
this._loaded = true;
}
}
- get player(): HTMLVideoElement | undefined {
- return this._videoRef.current ? this._videoRef.current.getElementsByTagName("video")[0] : undefined;
+ componentDidMount() {
+ if (this.props.setVideoBox) this.props.setVideoBox(this);
}
@action
setVideoRef = (vref: HTMLVideoElement | null) => {
- if (this.curPage >= 0 && vref) {
- vref.currentTime = this.curPage;
- (vref as any).AHackBecauseSomethingResetsTheVideoToZero = this.curPage;
+ this._videoRef = vref;
+ if (this.initialTimecode >= 0 && vref) {
+ vref.currentTime = this.initialTimecode;
}
}
videoContent(path: string) {
return <video className="videobox-cont" ref={this.setVideoRef}>
<source src={path} type="video/mp4" />
Not supported.
- </video>;
+ </video>;
}
render() {
- let field = this.props.Document.GetT(this.props.fieldKey, VideoField);
- if (!field || field === FieldWaiting) {
+ let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ if (!field) {
return <div>Loading</div>;
}
- return (this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) ?
- this.videoContent(field.data.href) :
+ 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}>
- {this.videoContent(field.data.href)}
+ {content}
</div>
}
</Measure>;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a7c6fda8b..2239a8e38 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,11 +1,10 @@
import "./WebBox.scss";
import React = require("react");
-import { WebField } from '../../../fields/WebField';
import { FieldViewProps, FieldView } from './FieldView';
-import { FieldWaiting, Opt } from '../../../fields/Field';
+import { HtmlField } from "../../../new_fields/HtmlField";
+import { WebField } from "../../../new_fields/URLField";
import { observer } from "mobx-react";
import { computed, reaction, IReactionDisposer } from 'mobx';
-import { KeyStore } from '../../../fields/KeyStore';
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
@@ -14,8 +13,6 @@ export class WebBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(WebBox); }
- @computed get html(): string { return this.props.Document.GetHtml(KeyStore.Data, ""); }
-
_ignore = 0;
onPreWheel = (e: React.WheelEvent) => {
this._ignore = e.timeStamp;
@@ -34,14 +31,18 @@ export class WebBox extends React.Component<FieldViewProps> {
}
}
render() {
- let field = this.props.Document.Get(this.props.fieldKey);
- let path = field === FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- field instanceof WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu";
-
+ let field = this.props.Document[this.props.fieldKey];
+ let view;
+ if (field instanceof HtmlField) {
+ view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
+ } else if (field instanceof WebField) {
+ view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+ } else {
+ view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+ }
let content =
<div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
- {this.html ? <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: this.html }} /> :
- <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }} />}
+ {view}
</div>;
let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 11f2b0c4e..57221aa39 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,29 +1,83 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-import JsxParser from 'react-jsx-parser';
+import { SerializationHelper } from '../client/util/SerializationHelper';
+import { createSchema, makeInterface, makeStrictInterface, listSpec } from '../new_fields/Schema';
+import { ImageField } from '../new_fields/URLField';
+import { Doc } from '../new_fields/Doc';
+import { List } from '../new_fields/List';
-class Hello extends React.Component<{ firstName: string, lastName: string }> {
- render() {
- return <div>Hello {this.props.firstName} {this.props.lastName}</div>;
- }
-}
+
+const schema1 = createSchema({
+ hello: "number",
+ test: "string",
+ fields: "boolean",
+ url: ImageField,
+ testDoc: Doc
+});
+
+type TestDoc = makeInterface<[typeof schema1]>;
+const TestDoc: (doc?: Doc) => TestDoc = makeInterface(schema1);
+
+const schema2 = createSchema({
+ hello: ImageField,
+ test: "boolean",
+ fields: listSpec("number"),
+ url: "number",
+ testDoc: ImageField
+});
+
+const Test2Doc = makeStrictInterface(schema2);
+type Test2Doc = makeStrictInterface<typeof schema2>;
+
+const assert = (bool: boolean) => {
+ if (!bool) throw new Error();
+};
class Test extends React.Component {
+ onClick = () => {
+ const url = new ImageField(new URL("http://google.com"));
+ const doc = new Doc();
+ const doc2 = new Doc();
+ doc.hello = 5;
+ doc.fields = "test";
+ doc.test = "hello doc";
+ doc.url = url;
+ //doc.testDoc = doc2;
+
+
+ const test1: TestDoc = TestDoc(doc);
+ assert(test1.hello === 5);
+ assert(test1.fields === undefined);
+ assert(test1.test === "hello doc");
+ assert(test1.url === url);
+ assert(test1.testDoc === doc2);
+ test1.myField = 20;
+ assert(test1.myField === 20);
+
+ const test2: Test2Doc = Test2Doc(doc);
+ assert(test2.hello === undefined);
+ // assert(test2.fields === "test");
+ assert(test2.test === undefined);
+ assert(test2.url === undefined);
+ assert(test2.testDoc === undefined);
+ test2.url = 35;
+ assert(test2.url === 35);
+ const l = new List<Doc>();
+ //TODO push, and other array functions don't go through the proxy
+ l.push(doc2);
+ //TODO currently length, and any other string fields will get serialized
+ doc.list = l;
+ console.log(l.slice());
+ }
+
render() {
- let jsx = "<Hello {...props}/>";
- let bindings = {
- props: {
- firstName: "First",
- lastName: "Last"
- }
- };
- return <JsxParser jsx={jsx} bindings={bindings} components={{ Hello }}></JsxParser>;
+ return <div><button onClick={this.onClick}>Click me</button>
+ {/* <input onKeyPress={this.onEnter}></input> */}
+ </div>;
}
}
-ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <Test />
- </div>),
+ReactDOM.render(
+ <Test />,
document.getElementById('root')
); \ No newline at end of file
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 857da1ebb..4cac09dee 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -3,190 +3,184 @@ import "normalize.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { observer } from 'mobx-react';
-import { Document } from '../fields/Document';
-import { BasicField } from '../fields/BasicField';
-import { ListField } from '../fields/ListField';
-import { Key } from '../fields/Key';
-import { Opt, Field } from '../fields/Field';
-import { Server } from '../client/Server';
-
-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
+
+// 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
diff --git a/src/fields/AudioField.ts b/src/fields/AudioField.ts
deleted file mode 100644
index 87e47a715..000000000
--- a/src/fields/AudioField.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class AudioField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://techslides.com/demos/samples/sample.mp3") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
-
- ToScriptString(): string {
- return `new AudioField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new AudioField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Audio,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts
deleted file mode 100644
index 17b1fc4e8..000000000
--- a/src/fields/BasicField.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Field, FieldId } from "./Field";
-import { observable, computed, action } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-
-export abstract class BasicField<T> extends Field {
- constructor(data: T, save: boolean, id?: FieldId) {
- super(id);
-
- this.data = data;
- if (save) {
- Server.UpdateField(this);
- }
- }
-
- UpdateFromServer(data: any) {
- if (this.data !== data) {
- this.data = data;
- }
- }
-
- @observable
- protected data: T;
-
- @computed
- get Data(): T {
- return this.data;
- }
-
- set Data(value: T) {
- if (this.data === value) {
- return;
- }
- let oldValue = this.data;
- this.setData(value);
- UndoManager.AddEvent({
- undo: () => this.Data = oldValue,
- redo: () => this.Data = value
- });
- Server.UpdateField(this);
- }
-
- protected setData(value: T) {
- this.data = value;
- }
-
- @action
- TrySetValue(value: any): boolean {
- if (typeof value === typeof this.data) {
- this.Data = value;
- return true;
- }
- return false;
- }
-
- GetValue(): any {
- return this.Data;
- }
-}
diff --git a/src/fields/BooleanField.ts b/src/fields/BooleanField.ts
deleted file mode 100644
index d49bfe82b..000000000
--- a/src/fields/BooleanField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class BooleanField extends BasicField<boolean> {
- constructor(data: boolean = false as boolean, id?: FieldId, save: boolean = true as boolean) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new BooleanField("${this.Data}")`;
- }
-
- Copy() {
- return new BooleanField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Boolean,
- data: this.Data,
- id: this.Id
- };
- }
-}
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
deleted file mode 100644
index 339f370cc..000000000
--- a/src/fields/Document.ts
+++ /dev/null
@@ -1,460 +0,0 @@
-import { Key } from "./Key";
-import { KeyStore } from "./KeyStore";
-import { Field, Cast, FieldWaiting, FieldValue, FieldId, Opt } from "./Field";
-import { NumberField } from "./NumberField";
-import { ObservableMap, computed, action, runInAction } from "mobx";
-import { TextField } from "./TextField";
-import { ListField } from "./ListField";
-import { Server } from "../client/Server";
-import { Types } from "../server/Message";
-import { UndoManager } from "../client/util/UndoManager";
-import { HtmlField } from "./HtmlField";
-import { BooleanField } from "./BooleanField";
-import { allLimit } from "async";
-import { prototype } from "nodemailer/lib/smtp-pool";
-import { HistogramField } from "../client/northstar/dash-fields/HistogramField";
-import { Documents } from "../client/documents/Documents";
-
-export class Document extends Field {
- //TODO tfs: We should probably store FieldWaiting in fields when we request it from the server so that we don't set up multiple server gets for the same document and field
- public fields: ObservableMap<string, { key: Key; field: Field }> = new ObservableMap();
- public _proxies: ObservableMap<string, FieldId> = new ObservableMap();
-
- constructor(id?: string, save: boolean = true) {
- super(id);
-
- if (save) {
- Server.UpdateField(this);
- }
- }
- static FromJson(data: any, id: string, save: boolean): Document {
- let doc = new Document(id, save);
- let fields = data as { key: string, field: string }[];
- fields.forEach(pair => doc._proxies.set(pair.key, pair.field));
- return doc;
- }
-
- UpdateFromServer(data: { key: string, field: string }[]) {
- for (const kv of data) {
- this._proxies.set(kv.key, kv.field);
- }
- }
-
- public Width = () => this.GetNumber(KeyStore.Width, 0);
- public Height = () => this.GetNumber(KeyStore.Height, this.GetNumber(KeyStore.NativeWidth, 0) ? (this.GetNumber(KeyStore.NativeHeight, 0) / this.GetNumber(KeyStore.NativeWidth, 0)) * this.GetNumber(KeyStore.Width, 0) : 0);
- public Scale = () => this.GetNumber(KeyStore.Scale, 1);
-
- @computed
- public get Title(): string {
- let title = this.Get(KeyStore.Title, true);
- if (title || title === FieldWaiting) {
- if (title !== FieldWaiting && title instanceof TextField) {
- return title.Data;
- }
- else return "-waiting-";
- }
- let parTitle = this.GetT(KeyStore.Title, TextField);
- if (parTitle || parTitle === FieldWaiting) {
- if (parTitle !== FieldWaiting) return parTitle.Data + ".alias";
- else return "-waiting-.alias";
- }
- return "-untitled-";
- }
-
- @computed
- public get Fields() {
- return this.fields;
- }
-
- /**
- * Get the field in the document associated with the given key. If the
- * associated field has not yet been filled in from the server, a request
- * to the server will automatically be sent, the value will be filled in
- * when the request is completed, and {@link Field.ts#FieldWaiting} will be returned.
- * @param key - The key of the value to get
- * @param ignoreProto - If true, ignore any prototype this document
- * might have and only search for the value on this immediate document.
- * If false (default), search up the prototype chain, starting at this document,
- * for a document that has a field associated with the given key, and return the first
- * one found.
- *
- * @returns If the document does not have a field associated with the given key, returns `undefined`.
- * If the document does have an associated field, but the field has not been fetched from the server, returns {@link Field.ts#FieldWaiting}.
- * If the document does have an associated field, and the field has not been fetched from the server, returns the associated field.
- */
- Get(key: Key, ignoreProto: boolean = false): FieldValue<Field> {
- let field: FieldValue<Field>;
- if (ignoreProto) {
- if (this.fields.has(key.Id)) {
- field = this.fields.get(key.Id)!.field;
- } else if (this._proxies.has(key.Id)) {
- Server.GetDocumentField(this, key);
- /*
- The field might have been instantly filled from the cache
- Maybe we want to just switch back to returning the value
- from Server.GetDocumentField if it's in the cache
- */
- if (this.fields.has(key.Id)) {
- field = this.fields.get(key.Id)!.field;
- } else {
- field = FieldWaiting;
- }
- }
- } else {
- let doc: FieldValue<Document> = this;
- while (doc && field !== FieldWaiting) {
- let curField = doc.fields.get(key.Id);
- let curProxy = doc._proxies.get(key.Id);
- if (!curField || (curProxy && curField.field.Id !== curProxy)) {
- if (curProxy) {
- Server.GetDocumentField(doc, key);
- /*
- The field might have been instantly filled from the cache
- Maybe we want to just switch back to returning the value
- from Server.GetDocumentField if it's in the cache
- */
- if (this.fields.has(key.Id)) {
- field = this.fields.get(key.Id)!.field;
- } else {
- field = FieldWaiting;
- }
- break;
- }
- if (
- doc.fields.has(KeyStore.Prototype.Id) ||
- doc._proxies.has(KeyStore.Prototype.Id)
- ) {
- doc = doc.GetPrototype();
- } else {
- break;
- }
- } else {
- field = curField.field;
- break;
- }
- }
- if (doc === FieldWaiting) field = FieldWaiting;
- }
-
- return field;
- }
-
- /**
- * Tries to get the field associated with the given key, and if there is an
- * associated field, calls the given callback with that field.
- * @param key - The key of the value to get
- * @param callback - A function that will be called with the associated field, if it exists,
- * once it is fetched from the server (this may be immediately if the field has already been fetched).
- * Note: The callback will not be called if there is no associated field.
- * @returns `true` if the field exists on the document and `callback` will be called, and `false` otherwise
- */
- GetAsync(key: Key, callback: (field: Opt<Field>) => void): void {
- //TODO: This currently doesn't deal with prototypes
- let field = this.fields.get(key.Id);
- if (field && field.field) {
- callback(field.field);
- } else if (this._proxies.has(key.Id)) {
- Server.GetDocumentField(this, key, callback);
- } else if (this._proxies.has(KeyStore.Prototype.Id)) {
- this.GetTAsync(KeyStore.Prototype, Document, proto => {
- if (proto) {
- proto.GetAsync(key, callback);
- } else {
- callback(undefined);
- }
- });
- } else {
- callback(undefined);
- }
- }
-
- GetTAsync<T extends Field>(key: Key, ctor: { new(): T }): Promise<Opt<T>>;
- GetTAsync<T extends Field>(
- key: Key,
- ctor: { new(): T },
- callback: (field: Opt<T>) => void
- ): void;
- GetTAsync<T extends Field>(
- key: Key,
- ctor: { new(): T },
- callback?: (field: Opt<T>) => void
- ): Promise<Opt<T>> | void {
- let fn = (cb: (field: Opt<T>) => void) => {
- return this.GetAsync(key, field => {
- cb(Cast(field, ctor));
- });
- };
- if (callback) {
- fn(callback);
- } else {
- return new Promise(fn);
- }
- }
-
- /**
- * Same as {@link Document#GetAsync}, except a field of the given type
- * will be created if there is no field associated with the given key,
- * or the field associated with the given key is not of the given type.
- * @param ctor - Constructor of the field type to get. E.g., TextField, ImageField, etc.
- */
- GetOrCreateAsync<T extends Field>(
- key: Key,
- ctor: { new(): T },
- callback: (field: T) => void
- ): void {
- //This currently doesn't deal with prototypes
- if (this._proxies.has(key.Id) || this.fields.has(key.Id)) {
- Server.GetDocumentField(this, key, field => {
- if (field && field instanceof ctor) {
- callback(field);
- } else {
- let newField = new ctor();
- this.Set(key, newField);
- callback(newField);
- }
- });
- } else {
- let newField = new ctor();
- this.Set(key, newField);
- callback(newField);
- }
- }
-
- /**
- * Same as {@link Document#Get}, except that it will additionally
- * check if the field is of the given type.
- * @param ctor - Constructor of the field type to get. E.g., `TextField`, `ImageField`, etc.
- * @returns Same as {@link Document#Get}, except will return `undefined`
- * if there is an associated field but it is of the wrong type.
- */
- GetT<T extends Field = Field>(
- key: Key,
- ctor: { new(...args: any[]): T },
- ignoreProto: boolean = false
- ): FieldValue<T> {
- var getfield = this.Get(key, ignoreProto);
- if (getfield !== FieldWaiting) {
- return Cast(getfield, ctor);
- }
- return FieldWaiting;
- }
-
- GetOrCreate<T extends Field>(
- key: Key,
- ctor: { new(): T },
- ignoreProto: boolean = false
- ): T {
- const field = this.GetT(key, ctor, ignoreProto);
- if (field && field !== FieldWaiting) {
- return field;
- }
- const newField = new ctor();
- this.Set(key, newField);
- return newField;
- }
-
- GetData<T, U extends Field & { Data: T }>(
- key: Key,
- ctor: { new(): U },
- defaultVal: T
- ): T {
- let val = this.Get(key);
- let vval = val && val instanceof ctor ? val.Data : defaultVal;
- return vval;
- }
-
- GetHtml(key: Key, defaultVal: string): string {
- return this.GetData(key, HtmlField, defaultVal);
- }
-
- GetBoolean(key: Key, defaultVal: boolean): boolean {
- return this.GetData(key, BooleanField, defaultVal);
- }
-
- GetNumber(key: Key, defaultVal: number): number {
- return this.GetData(key, NumberField, defaultVal);
- }
-
- GetText(key: Key, defaultVal: string): string {
- return this.GetData(key, TextField, defaultVal);
- }
-
- GetList<T extends Field>(key: Key, defaultVal: T[]): T[] {
- return this.GetData<T[], ListField<T>>(key, ListField, defaultVal);
- }
-
- @action
- Set(key: Key, field: Field | undefined, setOnPrototype = false): void {
- let old = this.fields.get(key.Id);
- let oldField = old ? old.field : undefined;
- if (setOnPrototype) {
- this.SetOnPrototype(key, field);
- } else {
- if (field) {
- this.fields.set(key.Id, { key, field });
- this._proxies.set(key.Id, field.Id);
- // Server.AddDocumentField(this, key, field);
- } else {
- this.fields.delete(key.Id);
- this._proxies.delete(key.Id);
- // Server.DeleteDocumentField(this, key);
- }
- Server.UpdateField(this);
- }
- if (oldField || field) {
- UndoManager.AddEvent({
- undo: () => this.Set(key, oldField, setOnPrototype),
- redo: () => this.Set(key, field, setOnPrototype)
- });
- }
- }
-
- @action
- SetOnPrototype(key: Key, field: Field | undefined): void {
- this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
- f && f.Set(key, field);
- });
- }
-
- @action
- SetDataOnPrototype<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }, replaceWrongType = true) {
- this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
- f && f.SetData(key, value, ctor, replaceWrongType);
- });
- }
-
- @action
- SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(data: T): U }, replaceWrongType = true) {
- let field = this.Get(key, true);
- if (field instanceof ctor) {
- field.Data = value;
- } else if (!field || replaceWrongType) {
- let newField = new ctor(value);
- // newField.Data = value;
- this.Set(key, newField);
- }
- }
-
- @action
- SetText(key: Key, value: string, replaceWrongType = true) {
- this.SetData(key, value, TextField, replaceWrongType);
- }
- @action
- SetBoolean(key: Key, value: boolean, replaceWrongType = true) {
- this.SetData(key, value, BooleanField, replaceWrongType);
- }
- @action
- SetNumber(key: Key, value: number, replaceWrongType = true) {
- this.SetData(key, value, NumberField, replaceWrongType);
- }
-
- GetPrototype(): FieldValue<Document> {
- return this.GetT(KeyStore.Prototype, Document, true);
- }
-
- GetAllPrototypes(): Document[] {
- let protos: Document[] = [];
- let doc: FieldValue<Document> = this;
- while (doc && doc !== FieldWaiting) {
- protos.push(doc);
- doc = doc.GetPrototype();
- }
- return protos;
- }
-
- CreateAlias(id?: string): Document {
- let alias = new Document(id);
- this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
- f && alias.Set(KeyStore.Prototype, f);
- });
-
- return alias;
- }
-
- @action
- CreateLink(dstTarg: Document) {
- let batch = UndoManager.StartBatch("document view drop");
- let linkDoc: Document = Documents.TextDocument({ width: 100, height: 25, title: "-link-" });
- linkDoc.SetText(KeyStore.LinkDescription, "");
- linkDoc.SetText(KeyStore.LinkTags, "Default");
-
- let srcTarg = this;
- linkDoc.Set(KeyStore.LinkedToDocs, dstTarg);
- linkDoc.Set(KeyStore.LinkedFromDocs, srcTarg);
- const prom1 = new Promise(resolve => dstTarg.GetOrCreateAsync(
- KeyStore.LinkedFromDocs,
- ListField,
- field => {
- (field as ListField<Document>).Data.push(linkDoc);
- resolve();
- }
- ));
- const prom2 = new Promise(resolve => srcTarg.GetOrCreateAsync(
- KeyStore.LinkedToDocs,
- ListField,
- field => {
- (field as ListField<Document>).Data.push(linkDoc);
- resolve();
- }
- ));
- Promise.all([prom1, prom2]).finally(() => batch.end());
- return linkDoc;
- }
-
- MakeDelegate(id?: string): Document {
- let delegate = new Document(id);
-
- delegate.Set(KeyStore.Prototype, this);
-
- return delegate;
- }
-
- ToScriptString(): string {
- return "";
- }
-
- TrySetValue(value: any): boolean {
- throw new Error("Method not implemented.");
- }
- GetValue() {
- return this.Title;
- var title = (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) + "(" + this.Id + ")";
- return title;
- //throw new Error("Method not implemented.");
- }
- Copy(copyProto?: boolean, id?: string): Field {
- let copy = new Document();
- this._proxies.forEach((fieldid, keyid) => { // copy each prototype field
- let key = KeyStore.KeyLookup(keyid);
- if (key) {
- this.GetAsync(key, (field: Opt<Field>) => {
- if (key === KeyStore.Prototype && copyProto) { // handle prototype field specially
- if (field instanceof Document) {
- copy.Set(key, field.Copy(false)); // only copying one level of prototypes for now...
- }
- }
- else
- if (field instanceof Document) { // ... TODO bcz: should we copy documents or reference them
- copy.Set(key!, field);
- }
- else if (field) {
- copy.Set(key!, field.Copy());
- }
- });
- }
- });
- return copy;
- }
-
- ToJson() {
- let fields: { key: string, field: string }[] = [];
- this._proxies.forEach((field, key) =>
- field && fields.push({ key, field }));
-
- return {
- type: Types.Document,
- data: fields,
- id: this.Id
- };
- }
-}
diff --git a/src/fields/DocumentReference.ts b/src/fields/DocumentReference.ts
deleted file mode 100644
index 303754177..000000000
--- a/src/fields/DocumentReference.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Field, Opt, FieldValue, FieldId } from "./Field";
-import { Document } from "./Document";
-import { Key } from "./Key";
-import { Types } from "../server/Message";
-import { ObjectID } from "bson";
-
-export class DocumentReference extends Field {
- get Key(): Key {
- return this.key;
- }
-
- get Document(): Document {
- return this.document;
- }
-
- constructor(private document: Document, private key: Key) {
- super();
- }
-
- UpdateFromServer() {
-
- }
-
- Dereference(): FieldValue<Field> {
- return this.document.Get(this.key);
- }
-
- DereferenceToRoot(): FieldValue<Field> {
- let field: FieldValue<Field> = this;
- while (field instanceof DocumentReference) {
- field = field.Dereference();
- }
- return field;
- }
-
- TrySetValue(value: any): boolean {
- throw new Error("Method not implemented.");
- }
- GetValue() {
- throw new Error("Method not implemented.");
- }
- Copy(): Field {
- throw new Error("Method not implemented.");
- }
-
- ToScriptString(): string {
- return "";
- }
-
- ToJson() {
- return {
- type: Types.DocumentReference,
- data: this.document.Id,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/Field.ts b/src/fields/Field.ts
deleted file mode 100644
index 3b3e95c2b..000000000
--- a/src/fields/Field.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-
-import { Utils } from "../Utils";
-import { Types, Transferable } from "../server/Message";
-import { computed } from "mobx";
-
-export function Cast<T extends Field>(field: FieldValue<Field>, ctor: { new(): T }): Opt<T> {
- if (field) {
- if (ctor && field instanceof ctor) {
- return field;
- }
- }
- return undefined;
-}
-
-export const FieldWaiting: FIELD_WAITING = null;
-export type FIELD_WAITING = null;
-export type FieldId = string;
-export type Opt<T> = T | undefined;
-export type FieldValue<T> = Opt<T> | FIELD_WAITING;
-
-export abstract class Field {
- //FieldUpdated: TypedEvent<Opt<FieldUpdatedArgs>> = new TypedEvent<Opt<FieldUpdatedArgs>>();
-
- init(callback: (res: Field) => any) {
- callback(this);
- }
-
- private id: FieldId;
-
- @computed
- get Id(): FieldId {
- return this.id;
- }
-
- constructor(id: Opt<FieldId> = undefined) {
- this.id = id || Utils.GenerateGuid();
- }
-
- Dereference(): FieldValue<Field> {
- return this;
- }
- DereferenceToRoot(): FieldValue<Field> {
- return this;
- }
-
- DereferenceT<T extends Field = Field>(ctor: { new(): T }): FieldValue<T> {
- return Cast(this.Dereference(), ctor);
- }
-
- DereferenceToRootT<T extends Field = Field>(ctor: { new(): T }): FieldValue<T> {
- return Cast(this.DereferenceToRoot(), ctor);
- }
-
- Equals(other: Field): boolean {
- return this.id === other.id;
- }
-
- abstract UpdateFromServer(serverData: any): void;
-
- abstract ToScriptString(): string;
-
- abstract TrySetValue(value: any): boolean;
-
- abstract GetValue(): any;
-
- abstract Copy(): Field;
-
- abstract ToJson(): Transferable;
-} \ No newline at end of file
diff --git a/src/fields/FieldUpdatedArgs.ts b/src/fields/FieldUpdatedArgs.ts
deleted file mode 100644
index 23ccf2a5a..000000000
--- a/src/fields/FieldUpdatedArgs.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Field, Opt } from "./Field";
-import { Document } from "./Document";
-import { Key } from "./Key";
-
-export enum FieldUpdatedAction {
- Add,
- Remove,
- Replace,
- Update
-}
-
-export interface FieldUpdatedArgs {
- field: Field;
- action: FieldUpdatedAction;
-}
-
-export interface DocumentUpdatedArgs {
- field: Document;
- key: Key;
-
- oldValue: Opt<Field>;
- newValue: Opt<Field>;
-
- fieldArgs?: FieldUpdatedArgs;
-
- action: FieldUpdatedAction;
-}
diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts
deleted file mode 100644
index a1d880070..000000000
--- a/src/fields/HtmlField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-
-export class HtmlField extends BasicField<string> {
- constructor(data: string = "<html></html>", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new HtmlField("${this.Data}")`;
- }
-
- Copy() {
- return new HtmlField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Html,
- data: this.Data,
- id: this.Id,
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/IconFIeld.ts b/src/fields/IconFIeld.ts
deleted file mode 100644
index a6694cc49..000000000
--- a/src/fields/IconFIeld.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class IconField extends BasicField<string> {
- constructor(data: string = "", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new IconField("${this.Data}")`;
- }
-
- Copy() {
- return new IconField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Icon,
- data: this.Data,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/ImageField.ts b/src/fields/ImageField.ts
deleted file mode 100644
index bce20f242..000000000
--- a/src/fields/ImageField.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class ImageField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new ImageField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new ImageField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Image,
- data: this.Data.href,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
deleted file mode 100644
index 2eacd7d0c..000000000
--- a/src/fields/InkField.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-import { observable, ObservableMap } from "mobx";
-
-export enum InkTool {
- None,
- Pen,
- Highlighter,
- Eraser
-}
-export interface StrokeData {
- pathData: Array<{ x: number, y: number }>;
- color: string;
- width: string;
- tool: InkTool;
- page: number;
-}
-export type StrokeMap = Map<string, StrokeData>;
-
-export class InkField extends BasicField<StrokeMap> {
- constructor(data: StrokeMap = new Map, id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new InkField("${this.Data}")`;
- }
-
- Copy() {
- return new InkField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Ink,
- data: this.Data,
- id: this.Id,
- };
- }
-
- UpdateFromServer(data: any) {
- this.data = new ObservableMap(data);
- }
-
- static FromJson(id: string, data: any): InkField {
- let map: StrokeMap = new Map<string, StrokeData>();
- Object.keys(data).forEach(key => {
- map.set(key, data[key]);
- });
- return new InkField(map, id, false);
- }
-} \ No newline at end of file
diff --git a/src/fields/Key.ts b/src/fields/Key.ts
deleted file mode 100644
index 57e2dadf0..000000000
--- a/src/fields/Key.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Field, FieldId } from "./Field";
-import { Utils } from "../Utils";
-import { observable } from "mobx";
-import { Types } from "../server/Message";
-import { Server } from "../client/Server";
-
-export class Key extends Field {
- private name: string;
-
- get Name(): string {
- return this.name;
- }
-
- constructor(name: string, id?: string, save: boolean = true) {
- super(id || Utils.GenerateDeterministicGuid(name));
-
- this.name = name;
- if (save) {
- Server.UpdateField(this);
- }
- }
-
- UpdateFromServer(data: string) {
- this.name = data;
- }
-
- TrySetValue(value: any): boolean {
- throw new Error("Method not implemented.");
- }
-
- GetValue() {
- return this.Name;
- }
-
- Copy(): Field {
- return this;
- }
-
- ToScriptString(): string {
- return name;
- }
-
- ToJson() {
- return {
- type: Types.Key,
- data: this.name,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
deleted file mode 100644
index a7652f1bf..000000000
--- a/src/fields/KeyStore.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { Key } from "./Key";
-
-export namespace KeyStore {
- export const Prototype = new Key("Prototype");
- export const X = new Key("X");
- export const Y = new Key("Y");
- export const Page = new Key("Page");
- export const Title = new Key("Title");
- export const Author = new Key("Author");
- export const PanX = new Key("PanX");
- export const PanY = new Key("PanY");
- export const Scale = new Key("Scale");
- export const NativeWidth = new Key("NativeWidth");
- export const NativeHeight = new Key("NativeHeight");
- export const Width = new Key("Width");
- export const Height = new Key("Height");
- export const ZIndex = new Key("ZIndex");
- export const ZoomBasis = new Key("ZoomBasis");
- export const Data = new Key("Data");
- export const Annotations = new Key("Annotations");
- export const ViewType = new Key("ViewType");
- export const BaseLayout = new Key("BaseLayout");
- export const Layout = new Key("Layout");
- export const Templates = new Key("Templates");
- export const BackgroundColor = new Key("BackgroundColor");
- export const BackgroundLayout = new Key("BackgroundLayout");
- export const OverlayLayout = new Key("OverlayLayout");
- export const LayoutKeys = new Key("LayoutKeys");
- export const LayoutFields = new Key("LayoutFields");
- export const ColumnsKey = new Key("SchemaColumns");
- export const SchemaSplitPercentage = new Key("SchemaSplitPercentage");
- export const Caption = new Key("Caption");
- export const ActiveWorkspace = new Key("ActiveWorkspace");
- export const DocumentText = new Key("DocumentText");
- export const BrushingDocs = new Key("BrushingDocs");
- export const LinkedToDocs = new Key("LinkedToDocs");
- export const LinkedFromDocs = new Key("LinkedFromDocs");
- export const LinkDescription = new Key("LinkDescription");
- export const LinkTags = new Key("LinkTag");
- export const Thumbnail = new Key("Thumbnail");
- export const ThumbnailPage = new Key("ThumbnailPage");
- export const CurPage = new Key("CurPage");
- export const AnnotationOn = new Key("AnnotationOn");
- export const NumPages = new Key("NumPages");
- export const Ink = new Key("Ink");
- export const Cursors = new Key("Cursors");
- export const OptionalRightCollection = new Key("OptionalRightCollection");
- export const Archives = new Key("Archives");
- export const Workspaces = new Key("Workspaces");
- export const IsMinimized = new Key("IsMinimized");
- export const MinimizedDoc = new Key("MinimizedDoc");
- export const MaximizedDoc = new Key("MaximizedDoc");
- export const CopyDraggedItems = new Key("CopyDraggedItems");
- export const BorderRounding = new Key("BorderRounding");
-
- export const KeyList: Key[] = [Prototype, X, Y, Page, Title, Author, PanX, PanY, Scale, NativeWidth, NativeHeight,
- Width, Height, ZIndex, ZoomBasis, Data, Annotations, ViewType, Layout, BackgroundColor, BackgroundLayout, OverlayLayout, LayoutKeys,
- LayoutFields, ColumnsKey, SchemaSplitPercentage, Caption, ActiveWorkspace, DocumentText, BrushingDocs, LinkedToDocs, LinkedFromDocs,
- LinkDescription, LinkTags, Thumbnail, ThumbnailPage, CurPage, AnnotationOn, NumPages, Ink, Cursors, OptionalRightCollection,
- Archives, Workspaces, IsMinimized, MinimizedDoc, MaximizedDoc, CopyDraggedItems, BorderRounding
- ];
- export function KeyLookup(keyid: string) {
- for (const key of KeyList) {
- if (key.Id === keyid) {
- return key;
- }
- }
- return undefined;
- }
-}
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
deleted file mode 100644
index e24099126..000000000
--- a/src/fields/ListField.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-import { Types } from "../server/Message";
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { FieldMap } from "../client/SocketStub";
-import { ScriptField } from "./ScriptField";
-
-export class ListField<T extends Field> extends BasicField<T[]> {
- private _proxies: string[] = [];
- private _scriptIds: string[] = [];
- private scripts: ScriptField[] = [];
-
- constructor(data: T[] = [], scripts: ScriptField[] = [], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- this.scripts = scripts;
- this.updateProxies();
- this._scriptIds = this.scripts.map(script => script.Id);
- if (save) {
- Server.UpdateField(this);
- }
- this.observeList();
- }
-
- private _processingServerUpdate: boolean = false;
-
- private observeDisposer: Lambda | undefined;
- private observeList(): void {
- if (this.observeDisposer) {
- this.observeDisposer();
- }
- this.observeDisposer = observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
- const target = change.object;
- this.updateProxies();
- if (change.type === "splice") {
- this.runScripts(change.removed, false);
- UndoManager.AddEvent({
- undo: () => target.splice(change.index, change.addedCount, ...change.removed),
- redo: () => target.splice(change.index, change.removedCount, ...change.added)
- });
- this.runScripts(change.added, true);
- } else {
- this.runScripts([change.oldValue], false);
- UndoManager.AddEvent({
- undo: () => target[change.index] = change.oldValue,
- redo: () => target[change.index] = change.newValue
- });
- this.runScripts([change.newValue], true);
- }
- if (!this._processingServerUpdate) {
- Server.UpdateField(this);
- }
- });
- }
-
- private runScripts(fields: T[], added: boolean) {
- for (const script of this.scripts) {
- this.runScript(fields, script, added);
- }
- }
-
- private runScript(fields: T[], script: ScriptField, added: boolean) {
- if (!this._processingServerUpdate) {
- for (const field of fields) {
- script.script.run({ field, added });
- }
- }
- }
-
- addScript(script: ScriptField) {
- this.scripts.push(script);
- this._scriptIds.push(script.Id);
-
- this.runScript(this.Data, script, true);
- UndoManager.AddEvent({
- undo: () => this.removeScript(script),
- redo: () => this.addScript(script),
- });
- Server.UpdateField(this);
- }
-
- removeScript(script: ScriptField) {
- const index = this.scripts.indexOf(script);
- if (index === -1) {
- return;
- }
- this.scripts.splice(index, 1);
- this._scriptIds.splice(index, 1);
- UndoManager.AddEvent({
- undo: () => this.addScript(script),
- redo: () => this.removeScript(script),
- });
- this.runScript(this.Data, script, false);
- Server.UpdateField(this);
- }
-
- protected setData(value: T[]) {
- this.runScripts(this.data, false);
-
- this.data = observable(value);
- this.updateProxies();
- this.observeList();
- this.runScripts(this.data, true);
- }
-
- private updateProxies() {
- this._proxies = this.Data.map(field => field.Id);
- }
-
- private arraysEqual(a: any[], b: any[]) {
- if (a === b) return true;
- if (a === null || b === null) return false;
- if (a.length !== b.length) return false;
-
- // If you don't care about the order of the elements inside
- // the array, you should sort both arrays here.
- // Please note that calling sort on an array will modify that array.
- // you might want to clone your array first.
-
- for (var i = 0; i < a.length; ++i) {
- if (a[i] !== b[i]) return false;
- }
- return true;
- }
-
- init(callback: (field: Field) => any) {
- const fieldsPromise = Server.GetFields(this._proxies).then(action((fields: FieldMap) => {
- if (!this.arraysEqual(this._proxies, this.data.map(field => field.Id))) {
- var dataids = this.data.map(d => d.Id);
- var proxies = this._proxies.map(p => p);
- var added = this.data.length < this._proxies.length;
- var deleted = this.data.length > this._proxies.length;
- for (let i = 0; i < dataids.length && added; i++) {
- added = proxies.indexOf(dataids[i]) !== -1;
- }
- for (let i = 0; i < this._proxies.length && deleted; i++) {
- deleted = dataids.indexOf(proxies[i]) !== -1;
- }
-
- this._processingServerUpdate = true;
- for (let i = 0; i < proxies.length && added; i++) {
- if (dataids.indexOf(proxies[i]) === -1) {
- this.Data.splice(i, 0, fields[proxies[i]] as T);
- }
- }
- for (let i = dataids.length - 1; i >= 0 && deleted; i--) {
- if (proxies.indexOf(dataids[i]) === -1) {
- this.Data.splice(i, 1);
- }
- }
- if (!added && !deleted) {// otherwise, just rebuild the whole list
- this.setData(proxies.map(id => fields[id] as T));
- }
- this._processingServerUpdate = false;
- }
- }));
-
- const scriptsPromise = Server.GetFields(this._scriptIds).then((fields: FieldMap) => {
- this.scripts = this._scriptIds.map(id => fields[id] as ScriptField);
- });
-
- Promise.all([fieldsPromise, scriptsPromise]).then(() => callback(this));
- }
-
- ToScriptString(): string {
- return "new ListField([" + this.Data.map(field => field.ToScriptString()).join(", ") + "])";
- }
-
- Copy(): Field {
- return new ListField<T>(this.Data);
- }
-
-
- UpdateFromServer(data: { fields: string[], scripts: string[] }) {
- this._proxies = data.fields;
- this._scriptIds = data.scripts;
- }
- ToJson() {
- return {
- type: Types.List,
- data: {
- fields: this._proxies,
- scripts: this._scriptIds,
- },
- id: this.Id
- };
- }
-
- static FromJson(id: string, data: { fields: string[], scripts: string[] }): ListField<Field> {
- let list = new ListField([], [], id, false);
- list._proxies = data.fields;
- list._scriptIds = data.scripts;
- return list;
- }
-} \ No newline at end of file
diff --git a/src/fields/NumberField.ts b/src/fields/NumberField.ts
deleted file mode 100644
index 7eea360c0..000000000
--- a/src/fields/NumberField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-
-export class NumberField extends BasicField<number> {
- constructor(data: number = 0, id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new NumberField(${this.Data})`;
- }
-
- Copy() {
- return new NumberField(this.Data);
- }
-
- ToJson() {
- return {
- id: this.Id,
- type: Types.Number,
- data: this.Data
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/PDFField.ts b/src/fields/PDFField.ts
deleted file mode 100644
index 718a1a4c0..000000000
--- a/src/fields/PDFField.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { observable } from "mobx";
-import { Types } from "../server/Message";
-
-
-
-export class PDFField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- Copy(): Field {
- return new PDFField(this.Data);
- }
-
- ToScriptString(): string {
- return `new PDFField("${this.Data}")`;
- }
-
- ToJson() {
- return {
- type: Types.PDF,
- data: this.Data.href,
- id: this.Id
- };
- }
-
- @observable
- Page: Number = 1;
-
-} \ No newline at end of file
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
deleted file mode 100644
index f53f48ca6..000000000
--- a/src/fields/RichTextField.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-
-export class RichTextField extends BasicField<string> {
- constructor(data: string = "", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new RichTextField(${this.Data})`;
- }
-
- Copy() {
- return new RichTextField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.RichText,
- data: this.Data,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 7f87be45d..ae532c9e2 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -1,101 +1,101 @@
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting";
-import { Server } from "../client/Server";
-import { Without } from "../Utils";
+// import { Field, FieldId } from "./Field";
+// import { Types } from "../server/Message";
+// import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting";
+// import { Server } from "../client/Server";
+// import { Without } from "../Utils";
-export interface SerializableOptions extends Without<ScriptOptions, "capturedVariables"> {
- capturedIds: { [id: string]: string };
-}
+// export interface SerializableOptions extends Without<ScriptOptions, "capturedVariables"> {
+// capturedIds: { [id: string]: string };
+// }
-export interface ScriptData {
- script: string;
- options: SerializableOptions;
-}
+// export interface ScriptData {
+// script: string;
+// options: SerializableOptions;
+// }
-export class ScriptField extends Field {
- private _script?: CompiledScript;
- get script(): CompiledScript {
- return this._script!;
- }
- private options?: ScriptData;
+// export class ScriptField extends Field {
+// private _script?: CompiledScript;
+// get script(): CompiledScript {
+// return this._script!;
+// }
+// private options?: ScriptData;
- constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) {
- super(id);
+// constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) {
+// super(id);
- this._script = script;
+// this._script = script;
- if (save) {
- Server.UpdateField(this);
- }
- }
+// if (save) {
+// Server.UpdateField(this);
+// }
+// }
- ToScriptString() {
- return "new ScriptField(...)";
- }
+// ToScriptString() {
+// return "new ScriptField(...)";
+// }
- GetValue() {
- return this.script;
- }
+// GetValue() {
+// return this.script;
+// }
- TrySetValue(): boolean {
- throw new Error("Script fields currently can't be modified");
- }
+// TrySetValue(): boolean {
+// throw new Error("Script fields currently can't be modified");
+// }
- UpdateFromServer() {
- throw new Error("Script fields currently can't be updated");
- }
+// UpdateFromServer() {
+// throw new Error("Script fields currently can't be updated");
+// }
- static FromJson(id: string, data: ScriptData): ScriptField {
- let field = new ScriptField(undefined, id, false);
- field.options = data;
- return field;
- }
+// static FromJson(id: string, data: ScriptData): ScriptField {
+// let field = new ScriptField(undefined, id, false);
+// field.options = data;
+// return field;
+// }
- init(callback: (res: Field) => any) {
- const options = this.options!;
- const keys = Object.keys(options.options.capturedIds);
- Server.GetFields(keys).then(fields => {
- let captured: { [name: string]: Field } = {};
- keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
- const opts: ScriptOptions = {
- addReturn: options.options.addReturn,
- params: options.options.params,
- requiredType: options.options.requiredType,
- capturedVariables: captured
- };
- const script = CompileScript(options.script, opts);
- if (!script.compiled) {
- throw new Error("Can't compile script");
- }
- this._script = script;
- callback(this);
- });
- }
+// init(callback: (res: Field) => any) {
+// const options = this.options!;
+// const keys = Object.keys(options.options.capturedIds);
+// Server.GetFields(keys).then(fields => {
+// let captured: { [name: string]: Field } = {};
+// keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
+// const opts: ScriptOptions = {
+// addReturn: options.options.addReturn,
+// params: options.options.params,
+// requiredType: options.options.requiredType,
+// capturedVariables: captured
+// };
+// const script = CompileScript(options.script, opts);
+// if (!script.compiled) {
+// throw new Error("Can't compile script");
+// }
+// this._script = script;
+// callback(this);
+// });
+// }
- ToJson() {
- const { options, originalScript } = this.script;
- let capturedIds: { [id: string]: string } = {};
- for (const capt in options.capturedVariables) {
- capturedIds[options.capturedVariables[capt].Id] = capt;
- }
- const opts: SerializableOptions = {
- ...options,
- capturedIds
- };
- delete (opts as any).capturedVariables;
- return {
- id: this.Id,
- type: Types.Script,
- data: {
- script: originalScript,
- options: opts,
- },
- };
- }
+// ToJson() {
+// const { options, originalScript } = this.script;
+// let capturedIds: { [id: string]: string } = {};
+// for (const capt in options.capturedVariables) {
+// capturedIds[options.capturedVariables[capt].Id] = capt;
+// }
+// const opts: SerializableOptions = {
+// ...options,
+// capturedIds
+// };
+// delete (opts as any).capturedVariables;
+// return {
+// id: this.Id,
+// type: Types.Script,
+// data: {
+// script: originalScript,
+// options: opts,
+// },
+// };
+// }
- Copy(): Field {
- //Script fields are currently immutable, so we can fake copy them
- return this;
- }
-} \ No newline at end of file
+// Copy(): Field {
+// //Script fields are currently immutable, so we can fake copy them
+// return this;
+// }
+// } \ No newline at end of file
diff --git a/src/fields/TextField.ts b/src/fields/TextField.ts
deleted file mode 100644
index ddedec9b1..000000000
--- a/src/fields/TextField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class TextField extends BasicField<string> {
- constructor(data: string = "", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new TextField("${this.Data}")`;
- }
-
- Copy() {
- return new TextField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Text,
- data: this.Data,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts
deleted file mode 100644
index 347f1fa05..000000000
--- a/src/fields/TupleField.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-import { Types } from "../server/Message";
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-
-export class TupleField<T, U> extends BasicField<[T, U]> {
- constructor(data: [T, U], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- if (save) {
- Server.UpdateField(this);
- }
- this.observeTuple();
- }
-
- private observeDisposer: Lambda | undefined;
- private observeTuple(): void {
- this.observeDisposer = observe(this.Data as (T | U)[] as IObservableArray<T | U>, (change: IArrayChange<T | U> | IArraySplice<T | U>) => {
- if (change.type === "update") {
- UndoManager.AddEvent({
- undo: () => this.Data[change.index] = change.oldValue,
- redo: () => this.Data[change.index] = change.newValue
- });
- Server.UpdateField(this);
- } else {
- throw new Error("Why are you messing with the length of a tuple, huh?");
- }
- });
- }
-
- protected setData(value: [T, U]) {
- if (this.observeDisposer) {
- this.observeDisposer();
- }
- this.data = observable(value) as (T | U)[] as [T, U];
- this.observeTuple();
- }
-
- UpdateFromServer(values: [T, U]) {
- this.setData(values);
- }
-
- ToScriptString(): string {
- return `new TupleField([${this.Data[0], this.Data[1]}])`;
- }
-
- Copy(): Field {
- return new TupleField<T, U>(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Tuple,
- data: this.Data,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/VideoField.ts b/src/fields/VideoField.ts
deleted file mode 100644
index 838b811b1..000000000
--- a/src/fields/VideoField.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class VideoField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://techslides.com/demos/sample-videos/small.mp4") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new VideoField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new VideoField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Video,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/fields/WebField.ts b/src/fields/WebField.ts
deleted file mode 100644
index 8b276a552..000000000
--- a/src/fields/WebField.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class WebField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("https://crossorigin.me/" + "https://cs.brown.edu/") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new WebField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new WebField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Web,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index ec89a1194..1f9e160ce 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -1,15 +1,14 @@
import * as ReactDOM from 'react-dom';
import * as rp from 'request-promise';
-import { Documents } from '../client/documents/Documents';
-import { Server } from '../client/Server';
-import { Document } from '../fields/Document';
-import { KeyStore } from '../fields/KeyStore';
-import { ListField } from '../fields/ListField';
+import { Docs } from '../client/documents/Documents';
import { RouteStore } from '../server/RouteStore';
-import { ServerUtils } from '../server/ServerUtil';
import "./ImageUpload.scss";
import React = require('react');
-import { Opt } from '../fields/Field';
+import { DocServer } from '../client/DocServer';
+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';
@@ -38,21 +37,24 @@ const onFileLoad = async (file: any) => {
const json = await res.json();
json.map(async (file: any) => {
let path = window.location.origin + file;
- var doc: Document = Documents.ImageDocument(path, { nativeWidth: 200, width: 200 });
+ var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200 });
- const res = await rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId));
+ const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
if (!res) {
throw new Error("No user id returned");
}
- const field = await Server.GetField(res);
- let pending: Opt<Document>;
- if (field instanceof Document) {
- pending = await field.GetTAsync(KeyStore.OptionalRightCollection, Document);
+ const field = await DocServer.GetRefField(res);
+ let pending: Opt<Doc>;
+ if (field instanceof Doc) {
+ pending = await Cast(field.optionalRightCollection, Doc);
}
if (pending) {
- pending.GetOrCreateAsync(KeyStore.Data, ListField, list => {
- list.Data.push(doc);
- });
+ const data = await Cast(pending.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ pending.data = new List([doc]);
+ }
}
});
diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts
new file mode 100644
index 000000000..c0a79f267
--- /dev/null
+++ b/src/new_fields/DateField.ts
@@ -0,0 +1,18 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, date } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+@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);
+ }
+}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
new file mode 100644
index 000000000..afcf71fc9
--- /dev/null
+++ b/src/new_fields/Doc.ts
@@ -0,0 +1,229 @@
+import { observable, action } from "mobx";
+import { serializable, primitive, map, alias, list } from "serializr";
+import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
+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);
+}
+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");
+export const WidthSym = Symbol("Width");
+export const HeightSym = Symbol("Height");
+
+export function DocListCast(field: FieldResult): Promise<Doc[] | undefined>;
+export function DocListCast(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>;
+export function DocListCast(field: FieldResult, defaultValue?: Doc[]) {
+ const list = Cast(field, listSpec(Doc));
+ return list ? Promise.all(list) : Promise.resolve(defaultValue);
+}
+
+@Deserializable("doc").withFields(["id"])
+export class Doc extends RefField {
+ constructor(id?: FieldId, forceSave?: boolean) {
+ super(id);
+ const doc = new Proxy<this>(this, {
+ set: setter,
+ get: getter,
+ has: (target, key) => key in target.__fields,
+ 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"); },
+ });
+ this[SelfProxy] = doc;
+ if (!id || forceSave) {
+ DocServer.CreateField(doc);
+ }
+ return doc;
+ }
+
+ proto: Opt<Doc>;
+ [key: string]: FieldResult;
+
+ @serializable(alias("fields", map(autoObject())))
+ private get __fields() {
+ return this.___fields;
+ }
+
+ private set __fields(value) {
+ this.___fields = value;
+ for (const key in value) {
+ const field = value[key];
+ if (!(field instanceof ObjectField)) continue;
+ field[Parent] = this[Self];
+ field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ }
+ }
+
+ @observable
+ //{ [key: string]: Field | FieldWaiting | undefined }
+ private ___fields: any = {};
+
+ private [Update] = (diff: any) => {
+ DocServer.UpdateField(this[Id], diff);
+ }
+
+ 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 [HandleUpdate](diff: any) {
+ console.log(diff);
+ const set = diff.$set;
+ if (set) {
+ for (const key in set) {
+ if (!key.startsWith("fields.")) {
+ continue;
+ }
+ const value = SerializationHelper.Deserialize(set[key]);
+ const fKey = key.substring(7);
+ this[fKey] = value;
+ }
+ }
+ }
+}
+
+export namespace Doc {
+ // export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise<Field | undefined> {
+ // const self = doc[Self];
+ // return new Promise(res => getField(self, key, ignoreProto, res));
+ // }
+ // export function GetTAsync<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): Promise<T | undefined> {
+ // return new Promise(async res => {
+ // const field = await GetAsync(doc, key, ignoreProto);
+ // return Cast(field, ctor);
+ // });
+ // }
+ export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
+ 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 async function SetOnPrototype(doc: Doc, key: string, value: Field) {
+ const proto = doc.proto;
+ if (proto) {
+ proto[key] = value;
+ }
+ }
+ export function GetAllPrototypes(doc: Doc): Doc[] {
+ const protos: Doc[] = [];
+ let d: Opt<Doc> = doc;
+ while (d) {
+ protos.push(d);
+ d = FieldValue(d.proto);
+ }
+ return protos;
+ }
+ export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
+ for (const key in fields) {
+ if (fields.hasOwnProperty(key)) {
+ const value = fields[key];
+ if (value !== undefined) {
+ doc[key] = value;
+ }
+ }
+ }
+ return doc;
+ }
+
+ export function MakeAlias(doc: Doc) {
+ const alias = new Doc;
+
+ PromiseValue(Cast(doc.proto, Doc)).then(proto => {
+ if (proto) {
+ alias.proto = proto;
+ }
+ });
+
+ return alias;
+ }
+
+ export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
+ const copy = new Doc;
+ Object.keys(doc).forEach(key => {
+ const field = doc[key];
+ if (key === "proto" && copyProto) {
+ if (field instanceof Doc) {
+ copy[key] = Doc.MakeCopy(field);
+ }
+ } else {
+ if (field instanceof RefField) {
+ copy[key] = field;
+ } else if (field instanceof ObjectField) {
+ copy[key] = ObjectField.MakeCopy(field);
+ } else {
+ copy[key] = field;
+ }
+ }
+ });
+ 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.proto!.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> {
+ if (!doc) {
+ return undefined;
+ }
+ const delegate = new Doc();
+ delegate.proto = doc;
+ return delegate;
+ }
+ export const Prototype = Symbol("Prototype");
+} \ No newline at end of file
diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts
new file mode 100644
index 000000000..d998746bb
--- /dev/null
+++ b/src/new_fields/HtmlField.ts
@@ -0,0 +1,18 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("html")
+export class HtmlField extends ObjectField {
+ @serializable(primitive())
+ readonly html: string;
+
+ constructor(html: string) {
+ super();
+ this.html = html;
+ }
+
+ [Copy]() {
+ return new HtmlField(this.html);
+ }
+}
diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts
new file mode 100644
index 000000000..1a928389d
--- /dev/null
+++ b/src/new_fields/IconField.ts
@@ -0,0 +1,18 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("icon")
+export class IconField extends ObjectField {
+ @serializable(primitive())
+ readonly icon: string;
+
+ constructor(icon: string) {
+ super();
+ this.icon = icon;
+ }
+
+ [Copy]() {
+ return new IconField(this.icon);
+ }
+}
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
new file mode 100644
index 000000000..86a8bd18a
--- /dev/null
+++ b/src/new_fields/InkField.ts
@@ -0,0 +1,44 @@
+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 { deepCopy } from "../Utils";
+
+export enum InkTool {
+ None,
+ Pen,
+ Highlighter,
+ Eraser
+}
+export interface StrokeData {
+ pathData: Array<{ x: number, y: number }>;
+ color: string;
+ width: string;
+ tool: InkTool;
+ page: number;
+}
+
+const pointSchema = createSimpleSchema({
+ x: true, y: true
+});
+
+const strokeDataSchema = createSimpleSchema({
+ pathData: list(object(pointSchema)),
+ "*": true
+});
+
+@Deserializable("ink")
+export class InkField extends ObjectField {
+ @serializable(map(object(strokeDataSchema)))
+ readonly inkData: Map<string, StrokeData>;
+
+ constructor(data?: Map<string, StrokeData>) {
+ super();
+ this.inkData = data || new Map;
+ }
+
+ [Copy]() {
+ return new InkField(deepCopy(this.inkData))
+ }
+}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
new file mode 100644
index 000000000..5aba64406
--- /dev/null
+++ b/src/new_fields/List.ts
@@ -0,0 +1,240 @@
+import { Deserializable, autoObject } from "../client/util/SerializationHelper";
+import { Field, Update, Self, FieldResult } from "./Doc";
+import { setter, getter, deleteProperty } from "./util";
+import { serializable, alias, list } from "serializr";
+import { observable, action } from "mobx";
+import { ObjectField, OnUpdate, Copy } from "./ObjectField";
+import { RefField } from "./RefField";
+import { ProxyField } from "./Proxy";
+
+const listHandlers: any = {
+ /// Mutator methods
+ copyWithin() {
+ throw new Error("copyWithin not supported yet");
+ },
+ fill(value: any, start?: number, end?: number) {
+ if (value instanceof RefField) {
+ throw new Error("fill with RefFields not supported yet");
+ }
+ const res = this[Self].__fields.fill(value, start, end);
+ this[Update]();
+ return res;
+ },
+ pop(): any {
+ const field = toRealField(this[Self].__fields.pop());
+ this[Update]();
+ return field;
+ },
+ push: action(function (this: any, ...items: any[]) {
+ items = items.map(toObjectField);
+ const res = this[Self].__fields.push(...items);
+ this[Update]();
+ return res;
+ }),
+ reverse() {
+ const res = this[Self].__fields.reverse();
+ this[Update]();
+ return res;
+ },
+ shift() {
+ const res = toRealField(this[Self].__fields.shift());
+ this[Update]();
+ return res;
+ },
+ sort(cmpFunc: any) {
+ const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
+ this[Update]();
+ return res;
+ },
+ splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
+ items = items.map(toObjectField);
+ const res = this[Self].__fields.splice(start, deleteCount, ...items);
+ this[Update]();
+ return res.map(toRealField);
+ }),
+ unshift(...items: any[]) {
+ items = items.map(toObjectField);
+ const res = this[Self].__fields.unshift(...items);
+ this[Update]();
+ return res;
+
+ },
+ /// Accessor methods
+ concat: action(function (this: any, ...items: any[]) {
+ return this[Self].__fields.map(toRealField).concat(...items);
+ }),
+ includes(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).includes(valueToFind, fromIndex);
+ } else {
+ return fields.includes(valueToFind, fromIndex);
+ }
+ },
+ indexOf(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).indexOf(valueToFind, fromIndex);
+ } else {
+ return fields.indexOf(valueToFind, fromIndex);
+ }
+ },
+ join(separator: any) {
+ return this[Self].__fields.map(toRealField).join(separator);
+ },
+ lastIndexOf(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).lastIndexOf(valueToFind, fromIndex);
+ } else {
+ return fields.lastIndexOf(valueToFind, fromIndex);
+ }
+ },
+ slice(begin: number, end: number) {
+ return this[Self].__fields.slice(begin, end).map(toRealField);
+ },
+
+ /// Iteration methods
+ entries() {
+ return this[Self].__fields.map(toRealField).entries();
+ },
+ every(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).every(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ filter(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).filter(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ find(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).find(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ findIndex(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).findIndex(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ forEach(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).forEach(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ map(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).map(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ reduce(callback: any, initialValue: any) {
+ return this[Self].__fields.map(toRealField).reduce(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ reduceRight(callback: any, initialValue: any) {
+ return this[Self].__fields.map(toRealField).reduceRight(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ some(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).some(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ values() {
+ return this[Self].__fields.map(toRealField).values();
+ },
+ [Symbol.iterator]() {
+ return this[Self].__fields.map(toRealField).values();
+ }
+};
+
+function toObjectField(field: Field) {
+ return field instanceof RefField ? new ProxyField(field) : field;
+}
+
+function toRealField(field: Field) {
+ return field instanceof ProxyField ? field.value() : field;
+}
+
+function listGetter(target: any, prop: string | number | symbol, receiver: any): any {
+ if (listHandlers.hasOwnProperty(prop)) {
+ return listHandlers[prop];
+ }
+ return getter(target, prop, receiver);
+}
+
+interface ListSpliceUpdate<T> {
+ type: "splice";
+ index: number;
+ added: T[];
+ removedCount: number;
+}
+
+interface ListIndexUpdate<T> {
+ type: "update";
+ index: number;
+ newValue: T;
+}
+
+type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>;
+
+type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
+
+@Deserializable("list")
+class ListImpl<T extends Field> extends ObjectField {
+ constructor(fields: T[] = []) {
+ super();
+ const list = new Proxy<this>(this, {
+ set: setter,
+ get: listGetter,
+ deleteProperty: deleteProperty,
+ defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+ });
+ (list as any).push(...fields);
+ return list;
+ }
+
+ [key: number]: T | (T extends RefField ? Promise<T> : never);
+
+ @serializable(alias("fields", list(autoObject())))
+ private get __fields() {
+ return this.___fields;
+ }
+
+ private set __fields(value) {
+ this.___fields = value;
+ }
+
+ [Copy]() {
+ let copiedData = this[Self].__fields.map(f => f instanceof ObjectField ? f[Copy]() : f);
+ let deepCopy = new ListImpl<T>(copiedData as any);
+ return deepCopy;
+ }
+
+ // @serializable(alias("fields", list(autoObject())))
+ @observable
+ private ___fields: StoredType<T>[] = [];
+
+ private [Update] = (diff: any) => {
+ // console.log(diff);
+ const update = this[OnUpdate];
+ // update && update(diff);
+ update && update();
+ }
+
+ private [Self] = this;
+}
+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
new file mode 100644
index 000000000..0f3777af6
--- /dev/null
+++ b/src/new_fields/ObjectField.ts
@@ -0,0 +1,17 @@
+import { Doc } from "./Doc";
+
+export const OnUpdate = Symbol("OnUpdate");
+export const Parent = Symbol("Parent");
+export const Copy = Symbol("Copy");
+
+export abstract class ObjectField {
+ protected [OnUpdate]?: (diff?: any) => void;
+ private [Parent]?: Doc;
+ abstract [Copy](): ObjectField;
+}
+
+export namespace ObjectField {
+ export function MakeCopy<T extends ObjectField>(field: T) {
+ return field[Copy]();
+ }
+}
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
new file mode 100644
index 000000000..fd99ae1c0
--- /dev/null
+++ b/src/new_fields/Proxy.ts
@@ -0,0 +1,65 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+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";
+
+@Deserializable("proxy")
+export class ProxyField<T extends RefField> extends ObjectField {
+ constructor();
+ constructor(value: T);
+ constructor(fieldId: string);
+ constructor(value?: T | string) {
+ super();
+ if (typeof value === "string") {
+ this.fieldId = value;
+ } else if (value) {
+ this.cache = value;
+ this.fieldId = value[Id];
+ }
+ }
+
+ [Copy]() {
+ if (this.cache) return new ProxyField<T>(this.cache);
+ return new ProxyField<T>(this.fieldId);
+ }
+
+ @serializable(primitive())
+ readonly fieldId: string = "";
+
+ // This getter/setter and nested object thing is
+ // because mobx doesn't play well with observable proxies
+ @observable.ref
+ private _cache: { readonly field: T | undefined } = { field: undefined };
+ private get cache(): T | undefined {
+ return this._cache.field;
+ }
+ private set cache(field: T | undefined) {
+ this._cache = { field };
+ }
+
+ private failed = false;
+ private promise?: Promise<any>;
+
+ value(callback?: ((field: T | undefined) => void)): T | undefined | FieldWaiting {
+ if (this.cache) {
+ callback && callback(this.cache);
+ return this.cache;
+ }
+ if (this.failed) {
+ return undefined;
+ }
+ if (!this.promise) {
+ this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => {
+ this.promise = undefined;
+ this.cache = field;
+ if (field === undefined) this.failed = true;
+ return field;
+ }));
+ }
+ callback && this.promise.then(callback);
+ return this.promise;
+ }
+}
diff --git a/src/new_fields/RefField.ts b/src/new_fields/RefField.ts
new file mode 100644
index 000000000..202c65f21
--- /dev/null
+++ b/src/new_fields/RefField.ts
@@ -0,0 +1,18 @@
+import { serializable, primitive, alias } from "serializr";
+import { Utils } from "../Utils";
+
+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;
+ readonly [Id]: FieldId;
+
+ constructor(id?: FieldId) {
+ this.__id = id || Utils.GenerateGuid();
+ this[Id] = this.__id;
+ }
+
+ protected [HandleUpdate]?(diff: any): void;
+}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
new file mode 100644
index 000000000..eb30e76de
--- /dev/null
+++ b/src/new_fields/RichTextField.ts
@@ -0,0 +1,18 @@
+import { ObjectField, Copy } from "./ObjectField";
+import { serializable } from "serializr";
+import { Deserializable } from "../client/util/SerializationHelper";
+
+@Deserializable("RichTextField")
+export class RichTextField extends ObjectField {
+ @serializable(true)
+ readonly Data: string;
+
+ constructor(data: string) {
+ super();
+ this.Data = data;
+ }
+
+ [Copy]() {
+ return new RichTextField(this.Data);
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
new file mode 100644
index 000000000..b821baec9
--- /dev/null
+++ b/src/new_fields/Schema.ts
@@ -0,0 +1,82 @@
+import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType } from "./Types";
+import { Doc, Field } from "./Doc";
+
+type AllToInterface<T extends Interface[]> = {
+ 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>,
+ 0: ToInterface<Head<T>>
+}[HasTail<T> extends true ? 1 : 0];
+
+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 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> {
+ let schema: Interface = {};
+ for (const s of schemas) {
+ for (const key in s) {
+ schema[key] = s[key];
+ }
+ }
+ const proto = new Proxy({}, {
+ get(target: any, prop, receiver) {
+ const field = receiver.doc[prop];
+ if (prop in schema) {
+ return Cast(field, (schema as any)[prop]);
+ }
+ return field;
+ },
+ set(target: any, prop, value, receiver) {
+ receiver.doc[prop] = value;
+ return true;
+ }
+ });
+ return function (doc?: Doc) {
+ doc = doc || new Doc;
+ if (!(doc instanceof Doc)) {
+ throw new Error("Currently wrapping a schema in another schema isn't supported");
+ }
+ const obj = Object.create(proto, { doc: { value: doc, writable: false } });
+ return obj;
+ };
+}
+
+export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>;
+export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) => makeStrictInterface<T> {
+ const proto = {};
+ for (const key in schema) {
+ const type = schema[key];
+ Object.defineProperty(proto, key, {
+ get() {
+ return Cast(this.__doc[key], type as any);
+ },
+ set(value) {
+ value = Cast(value, type as any);
+ if (value !== undefined) {
+ this.__doc[key] = value;
+ return;
+ }
+ throw new TypeError("Expected type " + type);
+ }
+ });
+ }
+ return function (doc: any) {
+ if (!(doc instanceof Doc)) {
+ throw new Error("Currently wrapping a schema in another schema isn't supported");
+ }
+ const obj = Object.create(proto);
+ obj.__doc = doc;
+ return obj;
+ };
+}
+
+export function createSchema<T extends Interface>(schema: T): T & { proto: ToConstructor<Doc> } {
+ schema.proto = Doc;
+ return schema as any;
+}
+
+export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> {
+ return { List: type as any };//TODO Types
+} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
new file mode 100644
index 000000000..4b4c58eb8
--- /dev/null
+++ b/src/new_fields/Types.ts
@@ -0,0 +1,88 @@
+import { Field, Opt, FieldResult, Doc } from "./Doc";
+import { List } from "./List";
+import { RefField } from "./RefField";
+
+export type ToType<T extends ToConstructor<Field> | ListSpec<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 { new(...args: any[]): List<Field> } ? never :
+ T extends { new(...args: any[]): infer R } ? R : never;
+
+export type ToConstructor<T extends Field> =
+ T extends string ? "string" :
+ T extends number ? "number" :
+ T extends boolean ? "boolean" :
+ T extends List<infer U> ? ListSpec<U> :
+ new (...args: any[]) => T;
+
+export type ToInterface<T extends Interface> = {
+ [P in Exclude<keyof T, "proto">]: 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> };
+
+// 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;
+export type Tail<T extends any[]> =
+ ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
+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[]>;
+}
+
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T): FieldResult<ToType<T>>;
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal: WithoutList<ToType<T>> | null): WithoutList<ToType<T>>;
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined {
+ if (field instanceof Promise) {
+ return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal;
+ }
+ if (field !== undefined && !(field instanceof Promise)) {
+ if (typeof ctor === "string") {
+ if (typeof field === ctor) {
+ return field as ToType<T>;
+ }
+ } else if (typeof ctor === "object") {
+ if (field instanceof List) {
+ return field as any;
+ }
+ } else if (field instanceof (ctor as any)) {
+ return field as ToType<T>;
+ }
+ }
+ return defaultVal === null ? undefined : defaultVal;
+}
+
+export function NumCast(field: FieldResult, defaultVal: number | null = 0) {
+ return Cast(field, "number", defaultVal);
+}
+
+export function StrCast(field: FieldResult, defaultVal: string | null = "") {
+ return Cast(field, "string", defaultVal);
+}
+
+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 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>;
+export function FieldValue<T extends Field>(field: FieldResult<T>, defaultValue?: T): Opt<T> {
+ return (field instanceof Promise || field === undefined) ? defaultValue : field;
+}
+
+export interface PromiseLike<T> {
+ then(callback: (field: Opt<T>) => void): void;
+}
+export function PromiseValue<T extends Field>(field: FieldResult<T>): PromiseLike<Opt<T>> {
+ return field instanceof Promise ? field : { then(cb: ((field: Opt<T>) => void)) { return cb(field); } };
+} \ No newline at end of file
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
new file mode 100644
index 000000000..d00a95a16
--- /dev/null
+++ b/src/new_fields/URLField.ts
@@ -0,0 +1,34 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, custom } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+function url() {
+ return custom(
+ function (value: URL) {
+ return value.href;
+ },
+ function (jsonValue: string) {
+ return new URL(jsonValue);
+ }
+ );
+}
+
+export class URLField extends ObjectField {
+ @serializable(url())
+ readonly url: URL;
+
+ constructor(url: URL) {
+ super();
+ this.url = url;
+ }
+
+ [Copy](): this {
+ return new (this.constructor as any)(this.url);
+ }
+}
+
+@Deserializable("audio") export class AudioField extends URLField { }
+@Deserializable("image") export class ImageField extends URLField { }
+@Deserializable("video") export class VideoField extends URLField { }
+@Deserializable("pdf") export class PdfField extends URLField { }
+@Deserializable("web") export class WebField extends URLField { } \ No newline at end of file
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
new file mode 100644
index 000000000..bbd8157f6
--- /dev/null
+++ b/src/new_fields/util.ts
@@ -0,0 +1,104 @@
+import { UndoManager } from "../client/util/UndoManager";
+import { Update, 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 { action } from "mobx";
+
+export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ if (SerializationHelper.IsSerializing()) {
+ target[prop] = value;
+ return true;
+ }
+ if (typeof prop === "symbol") {
+ target[prop] = value;
+ return true;
+ }
+ const curValue = target.__fields[prop];
+ if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
+ // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
+ // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
+ return true;
+ }
+ if (value instanceof RefField) {
+ value = new ProxyField(value);
+ }
+ if (value instanceof ObjectField) {
+ //TODO Instead of target, maybe use target[Self]
+ if (value[Parent] && value[Parent] !== target) {
+ throw new Error("Can't put the same object in multiple documents at the same time");
+ }
+ value[Parent] = target;
+ value[OnUpdate] = updateFunction(target, prop, value, receiver);
+ }
+ if (curValue instanceof ObjectField) {
+ delete curValue[Parent];
+ delete curValue[OnUpdate];
+ }
+ 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,
+ undo: () => receiver[prop] = curValue
+ });
+ return true;
+});
+
+export function getter(target: any, prop: string | symbol | number, receiver: any): any {
+ if (typeof prop === "symbol") {
+ return target.__fields[prop] || target[prop];
+ }
+ if (SerializationHelper.IsSerializing()) {
+ return target[prop];
+ }
+ return getField(target, prop);
+}
+
+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) {
+ 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;
+ }
+ }
+ }
+ callback && callback(field);
+ return field;
+}
+
+export function deleteProperty(target: any, prop: string | number | symbol) {
+ if (typeof prop === "symbol") {
+ delete target[prop];
+ return true;
+ }
+ throw new Error("Currently properties can't be deleted from documents, assign to undefined instead");
+}
+
+export function updateFunction(target: any, prop: any, value: any, receiver: any) {
+ let current = ObjectField.MakeCopy(value);
+ return (diff?: any) => {
+ if (true || !diff) {
+ diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+ const oldValue = current;
+ const newValue = ObjectField.MakeCopy(value);
+ current = newValue;
+ UndoManager.AddEvent({
+ redo() { receiver[prop] = newValue; },
+ undo() { receiver[prop] = oldValue; }
+ });
+ }
+ target[Update](diff);
+ };
+} \ No newline at end of file
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 854ae0168..e9a8b0f0c 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -24,6 +24,14 @@ export interface Transferable {
readonly data?: any;
}
+export interface Reference {
+ readonly id: string;
+}
+
+export interface Diff extends Reference {
+ readonly diff: any;
+}
+
export namespace MessageStore {
export const Foo = new Message<string>("Foo");
export const Bar = new Message<string>("Bar");
@@ -32,4 +40,9 @@ export namespace MessageStore {
export const GetFields = new Message<string[]>("Get Fields"); // send string[] of 'id' get Transferable[] back
export const GetDocument = new Message<string>("Get Document");
export const DeleteAll = new Message<any>("Delete All");
+
+ export const GetRefField = new Message<string>("Get Ref Field");
+ export const GetRefFields = new Message<string[]>("Get Ref Fields");
+ export const UpdateField = new Message<Diff>("Update Ref Field");
+ export const CreateField = new Message<Reference>("Create Ref Field");
}
diff --git a/src/server/Search.ts b/src/server/Search.ts
index 8ae996e9e..6e7883bc9 100644
--- a/src/server/Search.ts
+++ b/src/server/Search.ts
@@ -1,14 +1,41 @@
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/';
+ private client: any;
- public updateDocument(document: any): rp.RequestPromise {
- return rp.post(this.url + "dash/update/json/docs", {
+ constructor() {
+ console.log("Search Instantiated!");
+ var SolrNode = require('solr-node');
+ this.client = new SolrNode({
+ host: 'localhost',
+ port: '8983',
+ core: 'dash',
+ protocol: 'http'
+ });
+ var strQuery = this.client.query().q('text:test');
+
+ console.log(strQuery);
+
+ // Search documents using strQuery
+ // client.search(strQuery, (err: any, result: any) => {
+ // if (err) {
+ // console.log(err);
+ // return;
+ // }
+ // console.log('Response:', result.response);
+ // });
+ }
+
+
+ public async updateDocument(document: any) {
+ console.log("UPDATE: ", JSON.stringify(document));
+ return rp.post(this.url + "dash/update", {
headers: { 'content-type': 'application/json' },
- body: JSON.stringify(document)
+ body: JSON.stringify([document])
});
}
@@ -22,9 +49,7 @@ export class Search {
}));
const fields = searchResults.response.docs;
const ids = fields.map((field: any) => field.id);
- const docs = await Database.Instance.searchQuery(ids);
- const docIds = docs.map((doc: any) => doc._id);
- return docIds;
+ return ids;
}
public async clear() {
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
deleted file mode 100644
index eb5749dff..000000000
--- a/src/server/ServerUtil.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { HistogramField } from "../client/northstar/dash-fields/HistogramField";
-import { AudioField } from "../fields/AudioField";
-import { BooleanField } from "../fields/BooleanField";
-import { HtmlField } from "../fields/HtmlField";
-import { InkField } from "../fields/InkField";
-import { PDFField } from "../fields/PDFField";
-import { ScriptField } from "../fields/ScriptField";
-import { TupleField } from "../fields/TupleField";
-import { VideoField } from "../fields/VideoField";
-import { WebField } from "../fields/WebField";
-import { Utils } from "../Utils";
-import { Document } from "./../fields/Document";
-import { Field } from "./../fields/Field";
-import { ImageField } from "./../fields/ImageField";
-import { Key } from "./../fields/Key";
-import { ListField } from "./../fields/ListField";
-import { NumberField } from "./../fields/NumberField";
-import { RichTextField } from "./../fields/RichTextField";
-import { TextField } from "./../fields/TextField";
-import { Transferable, Types } from "./Message";
-import { TemplateField } from "../fields/TemplateField";
-import { IconField } from "../fields/IconFIeld";
-
-export class ServerUtils {
- public static prepend(extension: string): string {
- return window.location.origin + extension;
- }
-
- public static FromJson(json: Transferable): Field {
-
- if (!(json.data !== undefined && json.id && json.type !== undefined)) {
- console.log(
- "how did you manage to get an object that doesn't have a data or an id?"
- );
- return new TextField("Something to fill the space", Utils.GenerateGuid());
- }
-
- switch (json.type) {
- case Types.Boolean: return new BooleanField(json.data, json.id, false);
- case Types.Number: return new NumberField(json.data, json.id, false);
- case Types.Text: return new TextField(json.data, json.id, false);
- case Types.Icon: return new IconField(json.data, json.id, false);
- case Types.Html: return new HtmlField(json.data, json.id, false);
- case Types.Web: return new WebField(new URL(json.data), json.id, false);
- case Types.RichText: return new RichTextField(json.data, json.id, false);
- case Types.Key: return new Key(json.data, json.id, false);
- case Types.Image: return new ImageField(new URL(json.data), json.id, false);
- case Types.HistogramOp: return HistogramField.FromJson(json.id, json.data);
- case Types.PDF: return new PDFField(new URL(json.data), json.id, false);
- case Types.List: return ListField.FromJson(json.id, json.data);
- case Types.Script: return ScriptField.FromJson(json.id, json.data);
- case Types.Audio: return new AudioField(new URL(json.data), json.id, false);
- case Types.Video: return new VideoField(new URL(json.data), json.id, false);
- case Types.Tuple: return new TupleField(json.data, json.id, false);
- case Types.Ink: return InkField.FromJson(json.id, json.data);
- case Types.Templates: return TemplateField.FromJson(json.id, json.data);
- case Types.Document: return Document.FromJson(json.data, json.id, false);
- default:
- throw Error(
- "Error, unrecognized field type received from server. If you just created a new field type, be sure to add it here"
- );
- }
- }
-}
diff --git a/src/server/authentication/controllers/WorkspacesMenu.css b/src/server/authentication/controllers/WorkspacesMenu.css
deleted file mode 100644
index b89039965..000000000
--- a/src/server/authentication/controllers/WorkspacesMenu.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.ids:hover {
- color: darkblue;
-} \ No newline at end of file
diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx
deleted file mode 100644
index b08c1aebe..000000000
--- a/src/server/authentication/controllers/WorkspacesMenu.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import * as React from 'react';
-import { observable, action, configure, reaction, computed, ObservableMap, runInAction } from 'mobx';
-import { observer } from "mobx-react";
-import './WorkspacesMenu.css';
-import { Document } from '../../../fields/Document';
-import { EditableView } from '../../../client/views/EditableView';
-import { KeyStore } from '../../../fields/KeyStore';
-
-export interface WorkspaceMenuProps {
- active: Document | undefined;
- open: (workspace: Document) => void;
- new: () => void;
- allWorkspaces: Document[];
- isShown: () => boolean;
- toggle: () => void;
-}
-
-@observer
-export class WorkspacesMenu extends React.Component<WorkspaceMenuProps> {
- constructor(props: WorkspaceMenuProps) {
- super(props);
- this.addNewWorkspace = this.addNewWorkspace.bind(this);
- }
-
- @action
- addNewWorkspace() {
- this.props.new();
- this.props.toggle();
- }
-
- render() {
- return (
- <div
- style={{
- width: "auto",
- maxHeight: '200px',
- overflow: 'scroll',
- borderRadius: 5,
- position: "absolute",
- top: 78,
- left: this.props.isShown() ? 11 : -500,
- background: "white",
- border: "black solid 2px",
- transition: "all 1s ease",
- zIndex: 15,
- padding: 10,
- paddingRight: 12,
- }}>
- <img
- src="https://bit.ly/2IBBkxk"
- style={{
- width: 20,
- height: 20,
- marginTop: 3,
- marginLeft: 3,
- marginBottom: 3,
- cursor: "grab"
- }}
- onClick={this.addNewWorkspace}
- />
- {this.props.allWorkspaces.map((s, i) =>
- <div
- key={s.Id}
- onContextMenu={(e) => {
- e.preventDefault();
- this.props.open(s);
- }}
- style={{
- marginTop: 10,
- color: s === this.props.active ? "red" : "black"
- }}
- >
- <span>{i + 1} - </span>
- <EditableView
- display={"inline"}
- GetValue={() => s.Title}
- SetValue={(title: string): boolean => {
- s.SetText(KeyStore.Title, title);
- return true;
- }}
- contents={s.Title}
- height={20}
- />
- </div>
- )}
- </div>
- );
- }
-} \ 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 5d4479c88..5f45d7bcc 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,19 +1,20 @@
import { computed, observable, action, runInAction } from "mobx";
import * as rp from 'request-promise';
-import { Documents } from "../../../client/documents/Documents";
+import { Docs } from "../../../client/documents/Documents";
import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea";
import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil";
-import { Server } from "../../../client/Server";
-import { Document } from "../../../fields/Document";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
import { RouteStore } from "../../RouteStore";
-import { ServerUtils } from "../../ServerUtil";
+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";
export class CurrentUserUtils {
private static curr_email: string;
private static curr_id: string;
- @observable private static user_document: Document;
+ @observable private static user_document: Doc;
//TODO tfs: these should be temporary...
private static mainDocId: string | undefined;
@@ -23,15 +24,21 @@ export class CurrentUserUtils {
public static get MainDocId() { return this.mainDocId; }
public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
- private static createUserDocument(id: string): Document {
- let doc = new Document(id);
- doc.Set(KeyStore.Workspaces, new ListField<Document>());
- doc.Set(KeyStore.OptionalRightCollection, Documents.SchemaDocument([], { title: "Pending documents" }));
+ private static createUserDocument(id: string): Doc {
+ let doc = new Doc(id, true);
+ doc.viewType = CollectionViewType.Tree;
+ doc.layout = CollectionView.LayoutString();
+ doc.title = this.email;
+ doc.data = new List<Doc>();
+ doc.excludeFromLibrary = true;
+ doc.optionalRightCollection = Docs.SchemaDocument([], { title: "Pending documents" });
+ // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
+ // (doc.library as Doc).excludeFromLibrary = true;
return doc;
}
public static loadCurrentUser(): Promise<any> {
- let userPromise = rp.get(ServerUtils.prepend(RouteStore.getCurrUser)).then(response => {
+ let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
if (response) {
let obj = JSON.parse(response);
CurrentUserUtils.curr_id = obj.id as string;
@@ -40,10 +47,10 @@ export class CurrentUserUtils {
throw new Error("There should be a user! Why does Dash think there isn't one?");
}
});
- let userDocPromise = rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId)).then(id => {
+ let userDocPromise = rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
- return Server.GetField(id).then(field =>
- runInAction(() => this.user_document = field instanceof Document ? field : this.createUserDocument(id)));
+ return DocServer.GetRefField(id).then(field =>
+ runInAction(() => this.user_document = field instanceof Doc ? field : this.createUserDocument(id)));
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
diff --git a/src/server/database.ts b/src/server/database.ts
index d5905c7b3..69005d2d3 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -13,22 +13,15 @@ export class Database {
this.MongoClient.connect(this.url, (err, client) => this.db = client.db());
}
- public update(id: string, value: any, callback: () => void) {
+ public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) {
if (this.db) {
- let collection = this.db.collection('documents');
+ let collection = this.db.collection(collectionName);
const prom = this.currentWrites[id];
let newProm: Promise<void>;
const run = (): Promise<void> => {
return new Promise<void>(resolve => {
- collection.updateOne({ _id: id }, { $set: value }, { upsert: true }
+ 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];
}
@@ -51,36 +44,53 @@ export class Database {
this.db && this.db.collection(collectionName).deleteMany({}, res));
}
- public insert(kvpairs: any, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).insertOne(kvpairs, (err, res) =>
- err // && console.log(err)
- );
+ public insert(value: any, collectionName = Database.DocumentsCollection) {
+ if (!this.db) { return; }
+ 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;
}
public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).findOne({ id: id }, (err, result) =>
- fn(result ? ({ id: result._id, type: result.type, data: result.data }) : undefined));
+ 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);
+ }
+ });
}
public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).find({ id: { "$in": ids } }).toArray((err, docs) => {
+ 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 => ({ id: doc._id, type: doc.type, data: doc.data })));
- });
- }
-
- public searchQuery(ids: string[], collectionName = Database.DocumentsCollection): Promise<any> {
- return new Promise<any>(resolve => {
- this.db && this.db.collection(collectionName).find({ "data.field": { "$in": ids } }).toArray((err, docs) => {
- if (err) {
- console.log(err.message);
- console.log(err.errmsg);
- }
- resolve(docs);
- });
+ fn(docs.map(doc => {
+ doc.id = doc._id;
+ delete doc._id;
+ return doc;
+ }));
});
}
diff --git a/src/server/index.ts b/src/server/index.ts
index cb4268a2d..2381f9840 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -17,13 +17,12 @@ import { Socket } from 'socket.io';
import * as webpack from 'webpack';
import * as wdm from 'webpack-dev-middleware';
import * as whm from 'webpack-hot-middleware';
-import { Field, FieldId } from '../fields/Field';
import { Utils } from '../Utils';
import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller';
import { DashUserModel } from './authentication/models/user_model';
import { Client } from './Client';
import { Database } from './database';
-import { MessageStore, Transferable, Types } from "./Message";
+import { MessageStore, Transferable, Types, Diff } from "./Message";
import { RouteStore } from './RouteStore';
const app = express();
const config = require('../../webpack.config');
@@ -34,6 +33,8 @@ 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');
@@ -120,11 +121,14 @@ 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);
+ console.log(results);
res.send(results);
});
@@ -240,15 +244,22 @@ server.on("connection", function (socket: Socket) {
Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+
+ Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
+ Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
});
async function deleteFields() {
await Database.Instance.deleteAll();
await Search.Instance.clear();
+ await Database.Instance.deleteAll('newDocuments');
}
async function deleteAll() {
await Database.Instance.deleteAll();
+ await Database.Instance.deleteAll('newDocuments');
await Database.Instance.deleteAll('sessions');
await Database.Instance.deleteAll('users');
await Search.Instance.clear();
@@ -272,7 +283,50 @@ function setField(socket: Socket, newValue: Transferable) {
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");
+ }
+}
+
+function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
+ Database.Instance.getDocument(id, callback, "newDocuments");
+}
+
+function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) {
+ Database.Instance.getDocuments(ids, callback, "newDocuments");
+}
+
+
+const suffixMap: { [type: string]: string } = {
+ "number": "_n",
+ "string": "_t"
+};
+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;
+ const update: any = { id: diff.id };
+ console.log("FIELD: ", docfield);
+ let dynfield = false;
+ for (let key in docfield) {
+ if (!key.startsWith("fields.")) continue;
+ const val = docfield[key];
+ const suffix = suffixMap[typeof val];
+ if (suffix !== undefined) {
+ key = key.substring(7);
+ Object.values(suffixMap).forEach(suf => update[key + suf] = null);
+ update[key + suffix] = { set: val };
+ dynfield = true;
+ }
}
+ if (dynfield) {
+ console.log("dynamic field detected!");
+ Search.Instance.updateDocument(update);
+ }
+}
+
+function CreateField(newValue: any) {
+ Database.Instance.insert(newValue, "newDocuments");
+ console.log("created field");
}
server.listen(serverPort);
diff --git a/test/test.ts b/test/test.ts
index 16cace026..91dc43379 100644
--- a/test/test.ts
+++ b/test/test.ts
@@ -1,171 +1,35 @@
-import { NumberField } from "../src/fields/NumberField";
import { expect } from 'chai';
import 'mocha';
-import { Key } from "../src/fields/Key";
-import { Document } from "../src/fields/Document";
import { autorun, reaction } from "mobx";
-import { DocumentReference } from "../src/fields/DocumentReference";
-import { TextField } from "../src/fields/TextField";
-import { Field, FieldWaiting } from "../src/fields/Field";
-
-describe('Number Controller', () => {
- it('Should be constructable', () => {
- const numController = new NumberField(15);
- expect(numController.Data).to.equal(15);
- });
-
- it('Should update', () => {
- const numController = new NumberField(15);
- let ran = false;
- reaction(() => numController.Data, (data) => { ran = true; });
- expect(ran).to.equal(false);
- numController.Data = 5;
- expect(ran).to.equal(true);
- });
-});
+import { Doc } from '../src/new_fields/Doc';
+import { Cast } from '../src/new_fields/Types';
describe("Document", () => {
it('should hold fields', () => {
- let key = new Key("Test");
- let key2 = new Key("Test2");
- let field = new NumberField(15);
- let doc = new Document();
- doc.Set(key, field);
- let getField = doc.GetT(key, NumberField);
- let getField2 = doc.GetT(key2, NumberField);
+ let key = "Test";
+ let key2 = "Test2";
+ let field = 15;
+ let doc = new Doc();
+ doc[key] = field;
+ let getField = Cast(doc[key], "number");
+ let getField2 = Cast(doc[key2], "number");
expect(getField).to.equal(field);
expect(getField2).to.equal(undefined);
});
it('should update', () => {
- let doc = new Document();
- let key = new Key("Test");
- let key2 = new Key("Test2");
+ let doc = new Doc();
+ let key = "Test";
+ let key2 = "Test2";
let ran = false;
- reaction(() => doc.Get(key), (field) => { ran = true; });
+ reaction(() => doc[key], (field) => { ran = true; });
expect(ran).to.equal(false);
- doc.Set(key2, new NumberField(4));
+ doc[key2] = 4;
expect(ran).to.equal(false);
- doc.Set(key, new NumberField(5));
+ doc[key] = 5;
expect(ran).to.equal(true);
});
});
-
-describe("Reference", () => {
- it('should dereference', () => {
- let doc = new Document();
- let doc2 = new Document();
- const key = new Key("test");
- const key2 = new Key("test2");
-
- const numCont = new NumberField(55);
- doc.Set(key, numCont);
- let ref = new DocumentReference(doc, key);
- let ref2 = new DocumentReference(doc, key2);
- doc2.Set(key2, ref);
-
- let ref3 = new DocumentReference(doc2, key2);
- let ref4 = new DocumentReference(doc2, key);
-
- expect(ref.Dereference()).to.equal(numCont);
- expect(ref.DereferenceToRoot()).to.equal(numCont);
- expect(ref2.Dereference()).to.equal(undefined);
- expect(ref2.DereferenceToRoot()).to.equal(undefined);
- expect(ref3.Dereference()).to.equal(ref);
- expect(ref3.DereferenceToRoot()).to.equal(numCont);
- expect(ref4.Dereference()).to.equal(undefined);
- expect(ref4.DereferenceToRoot()).to.equal(undefined);
- });
-
- it('should work with prototypes', () => {
- let doc = new Document;
- let doc2 = doc.MakeDelegate();
- let key = new Key("test");
- expect(doc.Get(key)).to.equal(undefined);
- expect(doc2.Get(key)).to.equal(undefined);
- let num = new NumberField(55);
- let num2 = new NumberField(56);
-
- doc.Set(key, num);
- expect(doc.Get(key)).to.equal(num);
- expect(doc2.Get(key)).to.equal(num);
-
- doc2.Set(key, num2);
- expect(doc.Get(key)).to.equal(num);
- expect(doc2.Get(key)).to.equal(num2);
- });
-
- it('should update through layers', () => {
- let doc = new Document();
- let doc2 = new Document();
- let doc3 = new Document();
- const key = new Key("test");
- const key2 = new Key("test2");
- const key3 = new Key("test3");
-
- const numCont = new NumberField(55);
- doc.Set(key, numCont);
- const ref = new DocumentReference(doc, key);
- doc2.Set(key2, ref);
- const ref3 = new DocumentReference(doc2, key2);
- doc3.Set(key3, ref3);
-
- let ran = false;
- reaction(() => {
- let field = (<Field>(<Field>doc3.Get(key3)).DereferenceToRoot()).GetValue();
- return field;
- }, (field) => {
- ran = true;
- });
- expect(ran).to.equal(false);
-
- numCont.Data = 44;
- expect(ran).to.equal(true);
- ran = false;
-
- doc.Set(key, new NumberField(33));
- expect(ran).to.equal(true);
- ran = false;
-
- doc.Set(key2, new NumberField(4));
- expect(ran).to.equal(false);
-
- doc2.Set(key2, new TextField("hello"));
- expect(ran).to.equal(true);
- ran = false;
-
- doc3.Set(key3, new TextField("world"));
- expect(ran).to.equal(true);
- ran = false;
- });
-
- it('should update with prototypes', () => {
- let doc = new Document();
- let doc2 = doc.MakeDelegate();
- const key = new Key("test");
-
- const numCont = new NumberField(55);
-
- let ran = false;
- reaction(() => {
- let field = doc2.GetT(key, NumberField);
- if (field && field !== FieldWaiting) {
- return field.Data;
- }
- return undefined;
- }, (field) => {
- ran = true;
- });
- expect(ran).to.equal(false);
-
- doc.Set(key, numCont);
- expect(ran).to.equal(true);
-
- ran = false;
- numCont.Data = 1;
- expect(ran).to.equal(true);
- });
-}); \ No newline at end of file