id覆蓋檔案匯入

This commit is contained in:
User 2025-06-30 14:30:13 +08:00
parent b1d9692b82
commit e9375e6449
140 changed files with 10470 additions and 10390 deletions

16
.gitignore vendored Normal file → Executable file
View File

@ -1,8 +1,8 @@
.bundle/
log/*.log
pkg/
test/dummy/db/*.sqlite3
test/dummy/db/*.sqlite3-journal
test/dummy/log/*.log
test/dummy/tmp/
test/dummy/.sass-cache
.bundle/
log/*.log
pkg/
test/dummy/db/*.sqlite3
test/dummy/db/*.sqlite3-journal
test/dummy/log/*.log
test/dummy/tmp/
test/dummy/.sass-cache

28
Gemfile Normal file → Executable file
View File

@ -1,14 +1,14 @@
source "https://rubygems.org"
# Declare your gem's dependencies in universal_table.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec
# Declare any dependencies that are still in development here instead of in
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.
# To use debugger
# gem 'debugger'
source "https://rubygems.org"
# Declare your gem's dependencies in universal_table.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec
# Declare any dependencies that are still in development here instead of in
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.
# To use debugger
# gem 'debugger'

40
MIT-LICENSE Normal file → Executable file
View File

@ -1,20 +1,20 @@
Copyright 2015 YOURNAME
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copyright 2015 YOURNAME
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

4
README.rdoc Normal file → Executable file
View File

@ -1,3 +1,3 @@
= UniversalTable
= UniversalTable
This project rocks and uses MIT-LICENSE.

68
Rakefile Normal file → Executable file
View File

@ -1,34 +1,34 @@
begin
require 'bundler/setup'
rescue LoadError
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end
require 'rdoc/task'
RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'UniversalTable'
rdoc.options << '--line-numbers'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
load 'rails/tasks/engine.rake'
Bundler::GemHelper.install_tasks
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = false
end
task default: :test
begin
require 'bundler/setup'
rescue LoadError
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end
require 'rdoc/task'
RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'UniversalTable'
rdoc.options << '--line-numbers'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
load 'rails/tasks/engine.rake'
Bundler::GemHelper.install_tasks
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = false
end
task default: :test

0
app/assets/images/universal_table/.keep Normal file → Executable file
View File

156
app/assets/javascripts/mind_map/jsmind/jsmind.common.js Normal file → Executable file
View File

@ -1,78 +1,78 @@
export const __version__ = '1.0.0'
export const __author__ = 'author'
if (typeof String.prototype.startsWith != 'function') {
String.prototype.startsWith = function (p) {
return this.slice(0, p.length) === p
}
}
export const Direction = {
left: -1,
center: 0,
right: 1,
of: function (dir) {
if (!dir || dir === -1 || dir === 0 || dir === 1) {
return dir
}
if (dir === '-1' || dir === '0' || dir === '1') {
return parseInt(dir)
}
if (dir.toLowerCase() === 'left') {
return this.left
}
if (dir.toLowerCase() === 'right') {
return this.right
}
if (dir.toLowerCase() === 'center') {
return this.center
}
},
}
export const EventType = { show: 1, resize: 2, edit: 3, select: 4 }
export const Key = { meta: 1 << 13, ctrl: 1 << 12, alt: 1 << 11, shift: 1 << 10 }
export const LogLevel = { debug: 1, info: 2, warn: 3, error: 4, disable: 9 }
// an noop function define
var _noop = function () {}
export let logger =
typeof console === 'undefined'
? {
level: _noop,
log: _noop,
debug: _noop,
info: _noop,
warn: _noop,
error: _noop,
}
: {
level: setup_logger_level,
log: console.log,
debug: console.debug,
info: console.info,
warn: console.warn,
error: console.error,
}
function setup_logger_level(log_level) {
if (log_level > LogLevel.debug) {
logger.debug = _noop
} else {
logger.debug = console.debug
}
if (log_level > LogLevel.info) {
logger.info = _noop
} else {
logger.info = console.info
}
if (log_level > LogLevel.warn) {
logger.warn = _noop
} else {
logger.warn = console.warn
}
if (log_level > LogLevel.error) {
logger.error = _noop
} else {
logger.error = console.error
}
}
export const __version__ = '1.0.0'
export const __author__ = 'author'
if (typeof String.prototype.startsWith != 'function') {
String.prototype.startsWith = function (p) {
return this.slice(0, p.length) === p
}
}
export const Direction = {
left: -1,
center: 0,
right: 1,
of: function (dir) {
if (!dir || dir === -1 || dir === 0 || dir === 1) {
return dir
}
if (dir === '-1' || dir === '0' || dir === '1') {
return parseInt(dir)
}
if (dir.toLowerCase() === 'left') {
return this.left
}
if (dir.toLowerCase() === 'right') {
return this.right
}
if (dir.toLowerCase() === 'center') {
return this.center
}
},
}
export const EventType = { show: 1, resize: 2, edit: 3, select: 4 }
export const Key = { meta: 1 << 13, ctrl: 1 << 12, alt: 1 << 11, shift: 1 << 10 }
export const LogLevel = { debug: 1, info: 2, warn: 3, error: 4, disable: 9 }
// an noop function define
var _noop = function () {}
export let logger =
typeof console === 'undefined'
? {
level: _noop,
log: _noop,
debug: _noop,
info: _noop,
warn: _noop,
error: _noop,
}
: {
level: setup_logger_level,
log: console.log,
debug: console.debug,
info: console.info,
warn: console.warn,
error: console.error,
}
function setup_logger_level(log_level) {
if (log_level > LogLevel.debug) {
logger.debug = _noop
} else {
logger.debug = console.debug
}
if (log_level > LogLevel.info) {
logger.info = _noop
} else {
logger.info = console.info
}
if (log_level > LogLevel.warn) {
logger.warn = _noop
} else {
logger.warn = console.warn
}
if (log_level > LogLevel.error) {
logger.error = _noop
} else {
logger.error = console.error
}
}

800
app/assets/javascripts/mind_map/jsmind/jsmind.css Normal file → Executable file
View File

@ -1,400 +1,400 @@
/* important section */
.jsmind-inner {
position: relative;
overflow: auto;
width: 100%;
height: 100%;
outline: none;
} /*box-shadow:0 0 2px #000;*/
.jsmind-inner {
moz-user-select: -moz-none;
-moz-user-select: none;
-o-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.jsmind-inner canvas {
position: absolute;
}
/* z-index:1 */
svg.jsmind {
position: absolute;
z-index: 1;
}
canvas.jsmind {
position: absolute;
z-index: 1;
}
/* z-index:2 */
jmnodes {
position: absolute;
z-index: 2;
background-color: rgba(0, 0, 0, 0);
} /*background color is necessary*/
jmnode {
position: absolute;
cursor: default;
max-width: 400px;
}
jmexpander {
position: absolute;
width: 11px;
height: 11px;
display: block;
overflow: hidden;
line-height: 12px;
font-size: 10px;
text-align: center;
border-radius: 6px;
border-width: 1px;
border-style: solid;
cursor: pointer;
}
.jmnode-overflow-wrap jmnodes {
min-width: 420px;
}
.jmnode-overflow-hidden jmnode {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* default theme */
jmnode {
padding: 10px;
background-color: #fff;
color: #333;
border-radius: 5px;
box-shadow: 1px 1px 1px #666;
font: 16px/1.125 Verdana, Arial, Helvetica, sans-serif;
}
jmnode:hover {
box-shadow: 2px 2px 8px #000;
background-color: #ebebeb;
color: #333;
}
jmnode.selected {
background-color: #11f;
color: #fff;
box-shadow: 2px 2px 8px #000;
}
jmnode.root {
font-size: 24px;
}
jmexpander {
border-color: gray;
}
jmexpander:hover {
border-color: #000;
}
@media screen and (max-device-width: 1024px) {
jmnode {
padding: 5px;
border-radius: 3px;
font-size: 14px;
}
jmnode.root {
font-size: 21px;
}
}
/* primary theme */
jmnodes.theme-primary jmnode {
background-color: #428bca;
color: #fff;
border-color: #357ebd;
}
jmnodes.theme-primary jmnode:hover {
background-color: #3276b1;
border-color: #285e8e;
}
jmnodes.theme-primary jmnode.selected {
background-color: #f1c40f;
color: #fff;
}
jmnodes.theme-primary jmnode.root {
}
jmnodes.theme-primary jmexpander {
}
jmnodes.theme-primary jmexpander:hover {
}
/* warning theme */
jmnodes.theme-warning jmnode {
background-color: #f0ad4e;
border-color: #eea236;
color: #fff;
}
jmnodes.theme-warning jmnode:hover {
background-color: #ed9c28;
border-color: #d58512;
}
jmnodes.theme-warning jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-warning jmnode.root {
}
jmnodes.theme-warning jmexpander {
}
jmnodes.theme-warning jmexpander:hover {
}
/* danger theme */
jmnodes.theme-danger jmnode {
background-color: #d9534f;
border-color: #d43f3a;
color: #fff;
}
jmnodes.theme-danger jmnode:hover {
background-color: #d2322d;
border-color: #ac2925;
}
jmnodes.theme-danger jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-danger jmnode.root {
}
jmnodes.theme-danger jmexpander {
}
jmnodes.theme-danger jmexpander:hover {
}
/* success theme */
jmnodes.theme-success jmnode {
background-color: #5cb85c;
border-color: #4cae4c;
color: #fff;
}
jmnodes.theme-success jmnode:hover {
background-color: #47a447;
border-color: #398439;
}
jmnodes.theme-success jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-success jmnode.root {
}
jmnodes.theme-success jmexpander {
}
jmnodes.theme-success jmexpander:hover {
}
/* info theme */
jmnodes.theme-info jmnode {
background-color: #5dc0de;
border-color: #46b8da;
color: #fff;
}
jmnodes.theme-info jmnode:hover {
background-color: #39b3d7;
border-color: #269abc;
}
jmnodes.theme-info jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-info jmnode.root {
}
jmnodes.theme-info jmexpander {
}
jmnodes.theme-info jmexpander:hover {
}
/* greensea theme */
jmnodes.theme-greensea jmnode {
background-color: #1abc9c;
color: #fff;
}
jmnodes.theme-greensea jmnode:hover {
background-color: #16a085;
}
jmnodes.theme-greensea jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-greensea jmnode.root {
}
jmnodes.theme-greensea jmexpander {
}
jmnodes.theme-greensea jmexpander:hover {
}
/* nephrite theme */
jmnodes.theme-nephrite jmnode {
background-color: #2ecc71;
color: #fff;
}
jmnodes.theme-nephrite jmnode:hover {
background-color: #27ae60;
}
jmnodes.theme-nephrite jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-nephrite jmnode.root {
}
jmnodes.theme-nephrite jmexpander {
}
jmnodes.theme-nephrite jmexpander:hover {
}
/* belizehole theme */
jmnodes.theme-belizehole jmnode {
background-color: #3498db;
color: #fff;
}
jmnodes.theme-belizehole jmnode:hover {
background-color: #2980b9;
}
jmnodes.theme-belizehole jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-belizehole jmnode.root {
}
jmnodes.theme-belizehole jmexpander {
}
jmnodes.theme-belizehole jmexpander:hover {
}
/* wisteria theme */
jmnodes.theme-wisteria jmnode {
background-color: #9b59b6;
color: #fff;
}
jmnodes.theme-wisteria jmnode:hover {
background-color: #8e44ad;
}
jmnodes.theme-wisteria jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-wisteria jmnode.root {
}
jmnodes.theme-wisteria jmexpander {
}
jmnodes.theme-wisteria jmexpander:hover {
}
/* asphalt theme */
jmnodes.theme-asphalt jmnode {
background-color: #34495e;
color: #fff;
}
jmnodes.theme-asphalt jmnode:hover {
background-color: #2c3e50;
}
jmnodes.theme-asphalt jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-asphalt jmnode.root {
}
jmnodes.theme-asphalt jmexpander {
}
jmnodes.theme-asphalt jmexpander:hover {
}
/* orange theme */
jmnodes.theme-orange jmnode {
background-color: #f1c40f;
color: #fff;
}
jmnodes.theme-orange jmnode:hover {
background-color: #f39c12;
}
jmnodes.theme-orange jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-orange jmnode.root {
}
jmnodes.theme-orange jmexpander {
}
jmnodes.theme-orange jmexpander:hover {
}
/* pumpkin theme */
jmnodes.theme-pumpkin jmnode {
background-color: #e67e22;
color: #fff;
}
jmnodes.theme-pumpkin jmnode:hover {
background-color: #d35400;
}
jmnodes.theme-pumpkin jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-pumpkin jmnode.root {
}
jmnodes.theme-pumpkin jmexpander {
}
jmnodes.theme-pumpkin jmexpander:hover {
}
/* pomegranate theme */
jmnodes.theme-pomegranate jmnode {
background-color: #e74c3c;
color: #fff;
}
jmnodes.theme-pomegranate jmnode:hover {
background-color: #c0392b;
}
jmnodes.theme-pomegranate jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-pomegranate jmnode.root {
}
jmnodes.theme-pomegranate jmexpander {
}
jmnodes.theme-pomegranate jmexpander:hover {
}
/* clouds theme */
jmnodes.theme-clouds jmnode {
background-color: #ecf0f1;
color: #333;
}
jmnodes.theme-clouds jmnode:hover {
background-color: #bdc3c7;
}
jmnodes.theme-clouds jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-clouds jmnode.root {
}
jmnodes.theme-clouds jmexpander {
}
jmnodes.theme-clouds jmexpander:hover {
}
/* asbestos theme */
jmnodes.theme-asbestos jmnode {
background-color: #95a5a6;
color: #fff;
}
jmnodes.theme-asbestos jmnode:hover {
background-color: #7f8c8d;
}
jmnodes.theme-asbestos jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-asbestos jmnode.root {
}
jmnodes.theme-asbestos jmexpander {
}
jmnodes.theme-asbestos jmexpander:hover {
}
/* important section */
.jsmind-inner {
position: relative;
overflow: auto;
width: 100%;
height: 100%;
outline: none;
} /*box-shadow:0 0 2px #000;*/
.jsmind-inner {
moz-user-select: -moz-none;
-moz-user-select: none;
-o-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.jsmind-inner canvas {
position: absolute;
}
/* z-index:1 */
svg.jsmind {
position: absolute;
z-index: 1;
}
canvas.jsmind {
position: absolute;
z-index: 1;
}
/* z-index:2 */
jmnodes {
position: absolute;
z-index: 2;
background-color: rgba(0, 0, 0, 0);
} /*background color is necessary*/
jmnode {
position: absolute;
cursor: default;
max-width: 400px;
}
jmexpander {
position: absolute;
width: 11px;
height: 11px;
display: block;
overflow: hidden;
line-height: 12px;
font-size: 10px;
text-align: center;
border-radius: 6px;
border-width: 1px;
border-style: solid;
cursor: pointer;
}
.jmnode-overflow-wrap jmnodes {
min-width: 420px;
}
.jmnode-overflow-hidden jmnode {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* default theme */
jmnode {
padding: 10px;
background-color: #fff;
color: #333;
border-radius: 5px;
box-shadow: 1px 1px 1px #666;
font: 16px/1.125 Verdana, Arial, Helvetica, sans-serif;
}
jmnode:hover {
box-shadow: 2px 2px 8px #000;
background-color: #ebebeb;
color: #333;
}
jmnode.selected {
background-color: #11f;
color: #fff;
box-shadow: 2px 2px 8px #000;
}
jmnode.root {
font-size: 24px;
}
jmexpander {
border-color: gray;
}
jmexpander:hover {
border-color: #000;
}
@media screen and (max-device-width: 1024px) {
jmnode {
padding: 5px;
border-radius: 3px;
font-size: 14px;
}
jmnode.root {
font-size: 21px;
}
}
/* primary theme */
jmnodes.theme-primary jmnode {
background-color: #428bca;
color: #fff;
border-color: #357ebd;
}
jmnodes.theme-primary jmnode:hover {
background-color: #3276b1;
border-color: #285e8e;
}
jmnodes.theme-primary jmnode.selected {
background-color: #f1c40f;
color: #fff;
}
jmnodes.theme-primary jmnode.root {
}
jmnodes.theme-primary jmexpander {
}
jmnodes.theme-primary jmexpander:hover {
}
/* warning theme */
jmnodes.theme-warning jmnode {
background-color: #f0ad4e;
border-color: #eea236;
color: #fff;
}
jmnodes.theme-warning jmnode:hover {
background-color: #ed9c28;
border-color: #d58512;
}
jmnodes.theme-warning jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-warning jmnode.root {
}
jmnodes.theme-warning jmexpander {
}
jmnodes.theme-warning jmexpander:hover {
}
/* danger theme */
jmnodes.theme-danger jmnode {
background-color: #d9534f;
border-color: #d43f3a;
color: #fff;
}
jmnodes.theme-danger jmnode:hover {
background-color: #d2322d;
border-color: #ac2925;
}
jmnodes.theme-danger jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-danger jmnode.root {
}
jmnodes.theme-danger jmexpander {
}
jmnodes.theme-danger jmexpander:hover {
}
/* success theme */
jmnodes.theme-success jmnode {
background-color: #5cb85c;
border-color: #4cae4c;
color: #fff;
}
jmnodes.theme-success jmnode:hover {
background-color: #47a447;
border-color: #398439;
}
jmnodes.theme-success jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-success jmnode.root {
}
jmnodes.theme-success jmexpander {
}
jmnodes.theme-success jmexpander:hover {
}
/* info theme */
jmnodes.theme-info jmnode {
background-color: #5dc0de;
border-color: #46b8da;
color: #fff;
}
jmnodes.theme-info jmnode:hover {
background-color: #39b3d7;
border-color: #269abc;
}
jmnodes.theme-info jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-info jmnode.root {
}
jmnodes.theme-info jmexpander {
}
jmnodes.theme-info jmexpander:hover {
}
/* greensea theme */
jmnodes.theme-greensea jmnode {
background-color: #1abc9c;
color: #fff;
}
jmnodes.theme-greensea jmnode:hover {
background-color: #16a085;
}
jmnodes.theme-greensea jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-greensea jmnode.root {
}
jmnodes.theme-greensea jmexpander {
}
jmnodes.theme-greensea jmexpander:hover {
}
/* nephrite theme */
jmnodes.theme-nephrite jmnode {
background-color: #2ecc71;
color: #fff;
}
jmnodes.theme-nephrite jmnode:hover {
background-color: #27ae60;
}
jmnodes.theme-nephrite jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-nephrite jmnode.root {
}
jmnodes.theme-nephrite jmexpander {
}
jmnodes.theme-nephrite jmexpander:hover {
}
/* belizehole theme */
jmnodes.theme-belizehole jmnode {
background-color: #3498db;
color: #fff;
}
jmnodes.theme-belizehole jmnode:hover {
background-color: #2980b9;
}
jmnodes.theme-belizehole jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-belizehole jmnode.root {
}
jmnodes.theme-belizehole jmexpander {
}
jmnodes.theme-belizehole jmexpander:hover {
}
/* wisteria theme */
jmnodes.theme-wisteria jmnode {
background-color: #9b59b6;
color: #fff;
}
jmnodes.theme-wisteria jmnode:hover {
background-color: #8e44ad;
}
jmnodes.theme-wisteria jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-wisteria jmnode.root {
}
jmnodes.theme-wisteria jmexpander {
}
jmnodes.theme-wisteria jmexpander:hover {
}
/* asphalt theme */
jmnodes.theme-asphalt jmnode {
background-color: #34495e;
color: #fff;
}
jmnodes.theme-asphalt jmnode:hover {
background-color: #2c3e50;
}
jmnodes.theme-asphalt jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-asphalt jmnode.root {
}
jmnodes.theme-asphalt jmexpander {
}
jmnodes.theme-asphalt jmexpander:hover {
}
/* orange theme */
jmnodes.theme-orange jmnode {
background-color: #f1c40f;
color: #fff;
}
jmnodes.theme-orange jmnode:hover {
background-color: #f39c12;
}
jmnodes.theme-orange jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-orange jmnode.root {
}
jmnodes.theme-orange jmexpander {
}
jmnodes.theme-orange jmexpander:hover {
}
/* pumpkin theme */
jmnodes.theme-pumpkin jmnode {
background-color: #e67e22;
color: #fff;
}
jmnodes.theme-pumpkin jmnode:hover {
background-color: #d35400;
}
jmnodes.theme-pumpkin jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-pumpkin jmnode.root {
}
jmnodes.theme-pumpkin jmexpander {
}
jmnodes.theme-pumpkin jmexpander:hover {
}
/* pomegranate theme */
jmnodes.theme-pomegranate jmnode {
background-color: #e74c3c;
color: #fff;
}
jmnodes.theme-pomegranate jmnode:hover {
background-color: #c0392b;
}
jmnodes.theme-pomegranate jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-pomegranate jmnode.root {
}
jmnodes.theme-pomegranate jmexpander {
}
jmnodes.theme-pomegranate jmexpander:hover {
}
/* clouds theme */
jmnodes.theme-clouds jmnode {
background-color: #ecf0f1;
color: #333;
}
jmnodes.theme-clouds jmnode:hover {
background-color: #bdc3c7;
}
jmnodes.theme-clouds jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-clouds jmnode.root {
}
jmnodes.theme-clouds jmexpander {
}
jmnodes.theme-clouds jmexpander:hover {
}
/* asbestos theme */
jmnodes.theme-asbestos jmnode {
background-color: #95a5a6;
color: #fff;
}
jmnodes.theme-asbestos jmnode:hover {
background-color: #7f8c8d;
}
jmnodes.theme-asbestos jmnode.selected {
background-color: #11f;
color: #fff;
}
jmnodes.theme-asbestos jmnode.root {
}
jmnodes.theme-asbestos jmexpander {
}
jmnodes.theme-asbestos jmexpander:hover {
}

View File

@ -1,56 +1,56 @@
import { logger } from './jsmind.common.js'
import { format } from './jsmind.format.js'
export class DataProvider {
constructor(jm) {
this.jm = jm
}
init() {
logger.debug('data.init')
}
reset() {
logger.debug('data.reset')
}
load(mind_data) {
var df = null
var mind = null
if (typeof mind_data === 'object') {
if (!!mind_data.format) {
df = mind_data.format
} else {
df = 'node_tree'
}
} else {
df = 'freemind'
}
if (df == 'node_array') {
mind = format.node_array.get_mind(mind_data)
} else if (df == 'node_tree') {
mind = format.node_tree.get_mind(mind_data)
} else if (df == 'freemind') {
mind = format.freemind.get_mind(mind_data)
} else if (df == 'text') {
mind = format.text.get_mind(mind_data)
} else {
logger.warn('unsupported format')
}
return mind
}
get_data(data_format) {
var data = null
if (data_format == 'node_array') {
data = format.node_array.get_data(this.jm.mind)
} else if (data_format == 'node_tree') {
data = format.node_tree.get_data(this.jm.mind)
} else if (data_format == 'freemind') {
data = format.freemind.get_data(this.jm.mind)
} else if (data_format == 'text') {
data = format.text.get_data(this.jm.mind)
} else {
logger.error('unsupported ' + data_format + ' format')
}
return data
}
}
import { logger } from './jsmind.common.js'
import { format } from './jsmind.format.js'
export class DataProvider {
constructor(jm) {
this.jm = jm
}
init() {
logger.debug('data.init')
}
reset() {
logger.debug('data.reset')
}
load(mind_data) {
var df = null
var mind = null
if (typeof mind_data === 'object') {
if (!!mind_data.format) {
df = mind_data.format
} else {
df = 'node_tree'
}
} else {
df = 'freemind'
}
if (df == 'node_array') {
mind = format.node_array.get_mind(mind_data)
} else if (df == 'node_tree') {
mind = format.node_tree.get_mind(mind_data)
} else if (df == 'freemind') {
mind = format.freemind.get_mind(mind_data)
} else if (df == 'text') {
mind = format.text.get_mind(mind_data)
} else {
logger.warn('unsupported format')
}
return mind
}
get_data(data_format) {
var data = null
if (data_format == 'node_array') {
data = format.node_array.get_data(this.jm.mind)
} else if (data_format == 'node_tree') {
data = format.node_tree.get_data(this.jm.mind)
} else if (data_format == 'freemind') {
data = format.freemind.get_data(this.jm.mind)
} else if (data_format == 'text') {
data = format.text.get_data(this.jm.mind)
} else {
logger.error('unsupported ' + data_format + ' format')
}
return data
}
}

98
app/assets/javascripts/mind_map/jsmind/jsmind.dom.js Normal file → Executable file
View File

@ -1,49 +1,49 @@
class Dom {
constructor(w) {
this.w = w
this.d = w.document
this.g = function (id) {
return this.d.getElementById(id)
}
this.c = function (tag) {
return this.d.createElement(tag)
}
this.t = function (n, t) {
if (n.hasChildNodes()) {
n.firstChild.nodeValue = t
} else {
n.appendChild(this.d.createTextNode(t))
}
}
this.h = function (n, t) {
if (t instanceof HTMLElement) {
n.innerHTML = ''
n.appendChild(t)
} else {
n.innerHTML = t
}
}
// detect isElement
this.i = function (el) {
return (
!!el &&
typeof el === 'object' &&
el.nodeType === 1 &&
typeof el.style === 'object' &&
typeof el.ownerDocument === 'object'
)
}
//target,eventType,handler
this.on = function (t, e, h) {
if (!!t.addEventListener) {
t.addEventListener(e, h, false)
} else {
t.attachEvent('on' + e, h)
}
}
}
}
export const $ = new Dom(window)
class Dom {
constructor(w) {
this.w = w
this.d = w.document
this.g = function (id) {
return this.d.getElementById(id)
}
this.c = function (tag) {
return this.d.createElement(tag)
}
this.t = function (n, t) {
if (n.hasChildNodes()) {
n.firstChild.nodeValue = t
} else {
n.appendChild(this.d.createTextNode(t))
}
}
this.h = function (n, t) {
if (t instanceof HTMLElement) {
n.innerHTML = ''
n.appendChild(t)
} else {
n.innerHTML = t
}
}
// detect isElement
this.i = function (el) {
return (
!!el &&
typeof el === 'object' &&
el.nodeType === 1 &&
typeof el.style === 'object' &&
typeof el.ownerDocument === 'object'
)
}
//target,eventType,handler
this.on = function (t, e, h) {
if (!!t.addEventListener) {
t.addEventListener(e, h, false)
} else {
t.attachEvent('on' + e, h)
}
}
}
}
export const $ = new Dom(window)

1066
app/assets/javascripts/mind_map/jsmind/jsmind.format.js Normal file → Executable file

File diff suppressed because it is too large Load Diff

358
app/assets/javascripts/mind_map/jsmind/jsmind.graph.js Normal file → Executable file
View File

@ -1,179 +1,179 @@
import { $ } from './jsmind.dom.js'
import { logger } from './jsmind.common.js'
class SvgGraph {
constructor(view) {
this.view = view
this.opts = view.opts
this.e_svg = SvgGraph.c('svg')
this.e_svg.setAttribute('class', 'jsmind')
this.size = { w: 0, h: 0 }
this.lines = []
this.line_drawing = {
straight: this._line_to,
curved: this._bezier_to,
}
this.init_line_render()
}
static c(tag) {
return $.d.createElementNS('http://www.w3.org/2000/svg', tag)
}
init_line_render() {
if (typeof this.opts.custom_line_render === 'function') {
this.drawing = (path, x1, y1, x2, y2) => {
try {
this.opts.custom_line_render.call(this, {
ctx: path,
start_point: { x: x1, y: y1 },
end_point: { x: x2, y: y2 },
})
} catch (e) {
logger.error('custom line renderer error: ', e)
}
}
} else {
this.drawing = this.line_drawing[this.opts.line_style] || this.line_drawing.curved
}
}
element() {
return this.e_svg
}
set_size(w, h) {
this.size.w = w
this.size.h = h
this.e_svg.setAttribute('width', w)
this.e_svg.setAttribute('height', h)
}
clear() {
var len = this.lines.length
while (len--) {
this.e_svg.removeChild(this.lines[len])
}
this.lines.length = 0
}
draw_line(pout, pin, offset, color) {
var line = SvgGraph.c('path')
line.setAttribute('stroke', color || this.opts.line_color)
line.setAttribute('stroke-width', this.opts.line_width)
line.setAttribute('fill', 'transparent')
this.lines.push(line)
this.e_svg.appendChild(line)
this.drawing(line, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y)
}
copy_to(dest_canvas_ctx, callback) {
var img = new Image()
img.onload = function () {
dest_canvas_ctx.drawImage(img, 0, 0)
!!callback && callback()
}
img.src =
'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(this.e_svg))
}
_bezier_to(path, x1, y1, x2, y2) {
path.setAttribute(
'd',
'M ' +
x1 +
' ' +
y1 +
' C ' +
(x1 + ((x2 - x1) * 2) / 3) +
' ' +
y1 +
', ' +
x1 +
' ' +
y2 +
', ' +
x2 +
' ' +
y2
)
}
_line_to(path, x1, y1, x2, y2) {
path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
}
}
class CanvasGraph {
constructor(view) {
this.opts = view.opts
this.e_canvas = $.c('canvas')
this.e_canvas.className = 'jsmind'
this.canvas_ctx = this.e_canvas.getContext('2d')
this.size = { w: 0, h: 0 }
this.line_drawing = {
straight: this._line_to,
curved: this._bezier_to,
}
this.dpr = view.device_pixel_ratio
this.init_line_render()
}
init_line_render() {
if (typeof this.opts.custom_line_render === 'function') {
this.drawing = (ctx, x1, y1, x2, y2) => {
try {
this.opts.custom_line_render.call(this, {
ctx,
start_point: { x: x1, y: y1 },
end_point: { x: x2, y: y2 },
})
} catch (e) {
logger.error('custom line render error: ', e)
}
}
} else {
this.drawing = this.line_drawing[this.opts.line_style] || this.line_drawing.curved
}
}
element() {
return this.e_canvas
}
set_size(w, h) {
this.size.w = w
this.size.h = h
if (this.e_canvas.width && this.e_canvas.height && this.canvas_ctx.scale) {
this.e_canvas.width = w * this.dpr
this.e_canvas.height = h * this.dpr
this.e_canvas.style.width = w + 'px'
this.e_canvas.style.height = h + 'px'
this.canvas_ctx.scale(this.dpr, this.dpr)
} else {
this.e_canvas.width = w
this.e_canvas.height = h
}
}
clear() {
this.canvas_ctx.clearRect(0, 0, this.size.w, this.size.h)
}
draw_line(pout, pin, offset, color) {
var ctx = this.canvas_ctx
ctx.strokeStyle = color || this.opts.line_color
ctx.lineWidth = this.opts.line_width
ctx.lineCap = 'round'
this.drawing(ctx, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y)
}
copy_to(dest_canvas_ctx, callback) {
dest_canvas_ctx.drawImage(this.e_canvas, 0, 0, this.size.w, this.size.h)
!!callback && callback()
}
_bezier_to(ctx, x1, y1, x2, y2) {
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.bezierCurveTo(x1 + ((x2 - x1) * 2) / 3, y1, x1, y2, x2, y2)
ctx.stroke()
}
_line_to(ctx, x1, y1, x2, y2) {
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
}
}
export function init_graph(view, engine) {
return engine.toLowerCase() === 'svg' ? new SvgGraph(view) : new CanvasGraph(view)
}
import { $ } from './jsmind.dom.js'
import { logger } from './jsmind.common.js'
class SvgGraph {
constructor(view) {
this.view = view
this.opts = view.opts
this.e_svg = SvgGraph.c('svg')
this.e_svg.setAttribute('class', 'jsmind')
this.size = { w: 0, h: 0 }
this.lines = []
this.line_drawing = {
straight: this._line_to,
curved: this._bezier_to,
}
this.init_line_render()
}
static c(tag) {
return $.d.createElementNS('http://www.w3.org/2000/svg', tag)
}
init_line_render() {
if (typeof this.opts.custom_line_render === 'function') {
this.drawing = (path, x1, y1, x2, y2) => {
try {
this.opts.custom_line_render.call(this, {
ctx: path,
start_point: { x: x1, y: y1 },
end_point: { x: x2, y: y2 },
})
} catch (e) {
logger.error('custom line renderer error: ', e)
}
}
} else {
this.drawing = this.line_drawing[this.opts.line_style] || this.line_drawing.curved
}
}
element() {
return this.e_svg
}
set_size(w, h) {
this.size.w = w
this.size.h = h
this.e_svg.setAttribute('width', w)
this.e_svg.setAttribute('height', h)
}
clear() {
var len = this.lines.length
while (len--) {
this.e_svg.removeChild(this.lines[len])
}
this.lines.length = 0
}
draw_line(pout, pin, offset, color) {
var line = SvgGraph.c('path')
line.setAttribute('stroke', color || this.opts.line_color)
line.setAttribute('stroke-width', this.opts.line_width)
line.setAttribute('fill', 'transparent')
this.lines.push(line)
this.e_svg.appendChild(line)
this.drawing(line, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y)
}
copy_to(dest_canvas_ctx, callback) {
var img = new Image()
img.onload = function () {
dest_canvas_ctx.drawImage(img, 0, 0)
!!callback && callback()
}
img.src =
'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(this.e_svg))
}
_bezier_to(path, x1, y1, x2, y2) {
path.setAttribute(
'd',
'M ' +
x1 +
' ' +
y1 +
' C ' +
(x1 + ((x2 - x1) * 2) / 3) +
' ' +
y1 +
', ' +
x1 +
' ' +
y2 +
', ' +
x2 +
' ' +
y2
)
}
_line_to(path, x1, y1, x2, y2) {
path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
}
}
class CanvasGraph {
constructor(view) {
this.opts = view.opts
this.e_canvas = $.c('canvas')
this.e_canvas.className = 'jsmind'
this.canvas_ctx = this.e_canvas.getContext('2d')
this.size = { w: 0, h: 0 }
this.line_drawing = {
straight: this._line_to,
curved: this._bezier_to,
}
this.dpr = view.device_pixel_ratio
this.init_line_render()
}
init_line_render() {
if (typeof this.opts.custom_line_render === 'function') {
this.drawing = (ctx, x1, y1, x2, y2) => {
try {
this.opts.custom_line_render.call(this, {
ctx,
start_point: { x: x1, y: y1 },
end_point: { x: x2, y: y2 },
})
} catch (e) {
logger.error('custom line render error: ', e)
}
}
} else {
this.drawing = this.line_drawing[this.opts.line_style] || this.line_drawing.curved
}
}
element() {
return this.e_canvas
}
set_size(w, h) {
this.size.w = w
this.size.h = h
if (this.e_canvas.width && this.e_canvas.height && this.canvas_ctx.scale) {
this.e_canvas.width = w * this.dpr
this.e_canvas.height = h * this.dpr
this.e_canvas.style.width = w + 'px'
this.e_canvas.style.height = h + 'px'
this.canvas_ctx.scale(this.dpr, this.dpr)
} else {
this.e_canvas.width = w
this.e_canvas.height = h
}
}
clear() {
this.canvas_ctx.clearRect(0, 0, this.size.w, this.size.h)
}
draw_line(pout, pin, offset, color) {
var ctx = this.canvas_ctx
ctx.strokeStyle = color || this.opts.line_color
ctx.lineWidth = this.opts.line_width
ctx.lineCap = 'round'
this.drawing(ctx, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y)
}
copy_to(dest_canvas_ctx, callback) {
dest_canvas_ctx.drawImage(this.e_canvas, 0, 0, this.size.w, this.size.h)
!!callback && callback()
}
_bezier_to(ctx, x1, y1, x2, y2) {
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.bezierCurveTo(x1 + ((x2 - x1) * 2) / 3, y1, x1, y2, x2, y2)
ctx.stroke()
}
_line_to(ctx, x1, y1, x2, y2) {
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()
}
}
export function init_graph(view, engine) {
return engine.toLowerCase() === 'svg' ? new SvgGraph(view) : new CanvasGraph(view)
}

1510
app/assets/javascripts/mind_map/jsmind/jsmind.js Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,445 +1,445 @@
import { logger, Direction, EventType } from './jsmind.common.js'
export class LayoutProvider {
constructor(jm, options) {
this.opts = options
this.jm = jm
this.isside = this.opts.mode == 'side'
this.bounds = null
this.cache_valid = false
}
init() {
logger.debug('layout.init')
}
reset() {
logger.debug('layout.reset')
this.bounds = { n: 0, s: 0, w: 0, e: 0 }
}
calculate_next_child_direction(node) {
if (this.isside) {
return Direction.right
}
var children = node.children || []
var children_len = children.length
var r = 0
for (var i = 0; i < children_len; i++) {
if (children[i].direction === Direction.left) {
r--
} else {
r++
}
}
return children_len > 1 && r > 0 ? Direction.left : Direction.right
}
layout() {
logger.debug('layout.layout')
this.layout_direction()
this.layout_offset()
}
layout_direction() {
this._layout_direction_root()
}
_layout_direction_root() {
var node = this.jm.mind.root
var layout_data = null
if ('layout' in node._data) {
layout_data = node._data.layout
} else {
layout_data = {}
node._data.layout = layout_data
}
var children = node.children
var children_count = children.length
layout_data.direction = Direction.center
layout_data.side_index = 0
if (this.isside) {
var i = children_count
while (i--) {
this._layout_direction_side(children[i], Direction.right, i)
}
} else {
var i = children_count
var subnode = null
while (i--) {
subnode = children[i]
if (subnode.direction == Direction.left) {
this._layout_direction_side(subnode, Direction.left, i)
} else {
this._layout_direction_side(subnode, Direction.right, i)
}
}
}
}
_layout_direction_side(node, direction, side_index) {
var layout_data = null
if ('layout' in node._data) {
layout_data = node._data.layout
} else {
layout_data = {}
node._data.layout = layout_data
}
var children = node.children
var children_count = children.length
layout_data.direction = direction
layout_data.side_index = side_index
var i = children_count
while (i--) {
this._layout_direction_side(children[i], direction, i)
}
}
layout_offset() {
var node = this.jm.mind.root
var layout_data = node._data.layout
layout_data.offset_x = 0
layout_data.offset_y = 0
layout_data.outer_height = 0
var children = node.children
var i = children.length
var left_nodes = []
var right_nodes = []
var subnode = null
while (i--) {
subnode = children[i]
if (subnode._data.layout.direction == Direction.right) {
right_nodes.unshift(subnode)
} else {
left_nodes.unshift(subnode)
}
}
layout_data.left_nodes = left_nodes
layout_data.right_nodes = right_nodes
layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes)
layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes)
this.bounds.e = node._data.view.width / 2
this.bounds.w = 0 - this.bounds.e
this.bounds.n = 0
this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right)
}
// layout both the x and y axis
_layout_offset_subnodes(nodes) {
var total_height = 0
var nodes_count = nodes.length
var i = nodes_count
var node = null
var node_outer_height = 0
var layout_data = null
var base_y = 0
var pd = null // parent._data
while (i--) {
node = nodes[i]
layout_data = node._data.layout
if (pd == null) {
pd = node.parent._data
}
node_outer_height = this._layout_offset_subnodes(node.children)
if (!node.expanded) {
node_outer_height = 0
this.set_visible(node.children, false)
}
node_outer_height = Math.max(node._data.view.height, node_outer_height)
if (node.children.length > 1) {
node_outer_height += this.opts.cousin_space
}
layout_data.outer_height = node_outer_height
layout_data.offset_y = base_y - node_outer_height / 2
layout_data.offset_x =
this.opts.hspace * layout_data.direction +
(pd.view.width * (pd.layout.direction + layout_data.direction)) / 2
if (!node.parent.isroot) {
layout_data.offset_x += this.opts.pspace * layout_data.direction
}
base_y = base_y - node_outer_height - this.opts.vspace
total_height += node_outer_height
}
if (nodes_count > 1) {
total_height += this.opts.vspace * (nodes_count - 1)
}
i = nodes_count
var middle_height = total_height / 2
while (i--) {
node = nodes[i]
node._data.layout.offset_y += middle_height
}
return total_height
}
// layout the y axis only, for collapse/expand a node
_layout_offset_subnodes_height(nodes) {
var total_height = 0
var nodes_count = nodes.length
var i = nodes_count
var node = null
var node_outer_height = 0
var layout_data = null
var base_y = 0
var pd = null // parent._data
while (i--) {
node = nodes[i]
layout_data = node._data.layout
if (pd == null) {
pd = node.parent._data
}
node_outer_height = this._layout_offset_subnodes_height(node.children)
if (!node.expanded) {
node_outer_height = 0
}
node_outer_height = Math.max(node._data.view.height, node_outer_height)
if (node.children.length > 1) {
node_outer_height += this.opts.cousin_space
}
layout_data.outer_height = node_outer_height
layout_data.offset_y = base_y - node_outer_height / 2
base_y = base_y - node_outer_height - this.opts.vspace
total_height += node_outer_height
}
if (nodes_count > 1) {
total_height += this.opts.vspace * (nodes_count - 1)
}
i = nodes_count
var middle_height = total_height / 2
while (i--) {
node = nodes[i]
node._data.layout.offset_y += middle_height
}
return total_height
}
get_node_offset(node) {
var layout_data = node._data.layout
var offset_cache = null
if ('_offset_' in layout_data && this.cache_valid) {
offset_cache = layout_data._offset_
} else {
offset_cache = { x: -1, y: -1 }
layout_data._offset_ = offset_cache
}
if (offset_cache.x == -1 || offset_cache.y == -1) {
var x = layout_data.offset_x
var y = layout_data.offset_y
if (!node.isroot) {
var offset_p = this.get_node_offset(node.parent)
x += offset_p.x
y += offset_p.y
}
offset_cache.x = x
offset_cache.y = y
}
return offset_cache
}
get_node_point(node) {
var view_data = node._data.view
var offset_p = this.get_node_offset(node)
var p = {}
p.x = offset_p.x + (view_data.width * (node._data.layout.direction - 1)) / 2
p.y = offset_p.y - view_data.height / 2
return p
}
get_node_point_in(node) {
var p = this.get_node_offset(node)
return p
}
get_node_point_out(node) {
var layout_data = node._data.layout
var pout_cache = null
if ('_pout_' in layout_data && this.cache_valid) {
pout_cache = layout_data._pout_
} else {
pout_cache = { x: -1, y: -1 }
layout_data._pout_ = pout_cache
}
if (pout_cache.x == -1 || pout_cache.y == -1) {
if (node.isroot) {
pout_cache.x = 0
pout_cache.y = 0
} else {
var view_data = node._data.view
var offset_p = this.get_node_offset(node)
pout_cache.x =
offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction
pout_cache.y = offset_p.y
}
}
return pout_cache
}
get_expander_point(node) {
var p = this.get_node_point_out(node)
var ex_p = {}
if (node._data.layout.direction == Direction.right) {
ex_p.x = p.x - this.opts.pspace
} else {
ex_p.x = p.x
}
ex_p.y = p.y - Math.ceil(this.opts.pspace / 2)
return ex_p
}
get_min_size() {
var nodes = this.jm.mind.nodes
var node = null
var pout = null
for (var node_id in nodes) {
node = nodes[node_id]
pout = this.get_node_point_out(node)
if (pout.x > this.bounds.e) {
this.bounds.e = pout.x
}
if (pout.x < this.bounds.w) {
this.bounds.w = pout.x
}
}
return {
w: this.bounds.e - this.bounds.w,
h: this.bounds.s - this.bounds.n,
}
}
toggle_node(node) {
if (node.isroot) {
return
}
if (node.expanded) {
this.collapse_node(node)
} else {
this.expand_node(node)
}
}
expand_node(node) {
node.expanded = true
this.part_layout(node)
this.set_visible(node.children, true)
this.jm.invoke_event_handle(EventType.show, {
evt: 'expand_node',
data: [],
node: node.id,
})
}
collapse_node(node) {
node.expanded = false
this.part_layout(node)
this.set_visible(node.children, false)
this.jm.invoke_event_handle(EventType.show, {
evt: 'collapse_node',
data: [],
node: node.id,
})
}
expand_all() {
var nodes = this.jm.mind.nodes
var c = 0
var node
for (var node_id in nodes) {
node = nodes[node_id]
if (!node.expanded) {
node.expanded = true
c++
}
}
if (c > 0) {
var root = this.jm.mind.root
this.part_layout(root)
this.set_visible(root.children, true)
}
}
collapse_all() {
var nodes = this.jm.mind.nodes
var c = 0
var node
for (var node_id in nodes) {
node = nodes[node_id]
if (node.expanded && !node.isroot) {
node.expanded = false
c++
}
}
if (c > 0) {
var root = this.jm.mind.root
this.part_layout(root)
this.set_visible(root.children, true)
}
}
expand_to_depth(target_depth, curr_nodes, curr_depth) {
if (target_depth < 1) {
return
}
var nodes = curr_nodes || this.jm.mind.root.children
var depth = curr_depth || 1
var i = nodes.length
var node = null
while (i--) {
node = nodes[i]
if (depth < target_depth) {
if (!node.expanded) {
this.expand_node(node)
}
this.expand_to_depth(target_depth, node.children, depth + 1)
}
if (depth == target_depth) {
if (node.expanded) {
this.collapse_node(node)
}
}
}
}
part_layout(node) {
var root = this.jm.mind.root
if (!!root) {
var root_layout_data = root._data.layout
if (node.isroot) {
root_layout_data.outer_height_right = this._layout_offset_subnodes_height(
root_layout_data.right_nodes
)
root_layout_data.outer_height_left = this._layout_offset_subnodes_height(
root_layout_data.left_nodes
)
} else {
if (node._data.layout.direction == Direction.right) {
root_layout_data.outer_height_right = this._layout_offset_subnodes_height(
root_layout_data.right_nodes
)
} else {
root_layout_data.outer_height_left = this._layout_offset_subnodes_height(
root_layout_data.left_nodes
)
}
}
this.bounds.s = Math.max(
root_layout_data.outer_height_left,
root_layout_data.outer_height_right
)
this.cache_valid = false
} else {
logger.warn('can not found root node')
}
}
set_visible(nodes, visible) {
var i = nodes.length
var node = null
var layout_data = null
while (i--) {
node = nodes[i]
layout_data = node._data.layout
if (node.expanded) {
this.set_visible(node.children, visible)
} else {
this.set_visible(node.children, false)
}
if (!node.isroot) {
node._data.layout.visible = visible
}
}
}
is_expand(node) {
return node.expanded
}
is_visible(node) {
var layout_data = node._data.layout
if ('visible' in layout_data && !layout_data.visible) {
return false
} else {
return true
}
}
}
import { logger, Direction, EventType } from './jsmind.common.js'
export class LayoutProvider {
constructor(jm, options) {
this.opts = options
this.jm = jm
this.isside = this.opts.mode == 'side'
this.bounds = null
this.cache_valid = false
}
init() {
logger.debug('layout.init')
}
reset() {
logger.debug('layout.reset')
this.bounds = { n: 0, s: 0, w: 0, e: 0 }
}
calculate_next_child_direction(node) {
if (this.isside) {
return Direction.right
}
var children = node.children || []
var children_len = children.length
var r = 0
for (var i = 0; i < children_len; i++) {
if (children[i].direction === Direction.left) {
r--
} else {
r++
}
}
return children_len > 1 && r > 0 ? Direction.left : Direction.right
}
layout() {
logger.debug('layout.layout')
this.layout_direction()
this.layout_offset()
}
layout_direction() {
this._layout_direction_root()
}
_layout_direction_root() {
var node = this.jm.mind.root
var layout_data = null
if ('layout' in node._data) {
layout_data = node._data.layout
} else {
layout_data = {}
node._data.layout = layout_data
}
var children = node.children
var children_count = children.length
layout_data.direction = Direction.center
layout_data.side_index = 0
if (this.isside) {
var i = children_count
while (i--) {
this._layout_direction_side(children[i], Direction.right, i)
}
} else {
var i = children_count
var subnode = null
while (i--) {
subnode = children[i]
if (subnode.direction == Direction.left) {
this._layout_direction_side(subnode, Direction.left, i)
} else {
this._layout_direction_side(subnode, Direction.right, i)
}
}
}
}
_layout_direction_side(node, direction, side_index) {
var layout_data = null
if ('layout' in node._data) {
layout_data = node._data.layout
} else {
layout_data = {}
node._data.layout = layout_data
}
var children = node.children
var children_count = children.length
layout_data.direction = direction
layout_data.side_index = side_index
var i = children_count
while (i--) {
this._layout_direction_side(children[i], direction, i)
}
}
layout_offset() {
var node = this.jm.mind.root
var layout_data = node._data.layout
layout_data.offset_x = 0
layout_data.offset_y = 0
layout_data.outer_height = 0
var children = node.children
var i = children.length
var left_nodes = []
var right_nodes = []
var subnode = null
while (i--) {
subnode = children[i]
if (subnode._data.layout.direction == Direction.right) {
right_nodes.unshift(subnode)
} else {
left_nodes.unshift(subnode)
}
}
layout_data.left_nodes = left_nodes
layout_data.right_nodes = right_nodes
layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes)
layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes)
this.bounds.e = node._data.view.width / 2
this.bounds.w = 0 - this.bounds.e
this.bounds.n = 0
this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right)
}
// layout both the x and y axis
_layout_offset_subnodes(nodes) {
var total_height = 0
var nodes_count = nodes.length
var i = nodes_count
var node = null
var node_outer_height = 0
var layout_data = null
var base_y = 0
var pd = null // parent._data
while (i--) {
node = nodes[i]
layout_data = node._data.layout
if (pd == null) {
pd = node.parent._data
}
node_outer_height = this._layout_offset_subnodes(node.children)
if (!node.expanded) {
node_outer_height = 0
this.set_visible(node.children, false)
}
node_outer_height = Math.max(node._data.view.height, node_outer_height)
if (node.children.length > 1) {
node_outer_height += this.opts.cousin_space
}
layout_data.outer_height = node_outer_height
layout_data.offset_y = base_y - node_outer_height / 2
layout_data.offset_x =
this.opts.hspace * layout_data.direction +
(pd.view.width * (pd.layout.direction + layout_data.direction)) / 2
if (!node.parent.isroot) {
layout_data.offset_x += this.opts.pspace * layout_data.direction
}
base_y = base_y - node_outer_height - this.opts.vspace
total_height += node_outer_height
}
if (nodes_count > 1) {
total_height += this.opts.vspace * (nodes_count - 1)
}
i = nodes_count
var middle_height = total_height / 2
while (i--) {
node = nodes[i]
node._data.layout.offset_y += middle_height
}
return total_height
}
// layout the y axis only, for collapse/expand a node
_layout_offset_subnodes_height(nodes) {
var total_height = 0
var nodes_count = nodes.length
var i = nodes_count
var node = null
var node_outer_height = 0
var layout_data = null
var base_y = 0
var pd = null // parent._data
while (i--) {
node = nodes[i]
layout_data = node._data.layout
if (pd == null) {
pd = node.parent._data
}
node_outer_height = this._layout_offset_subnodes_height(node.children)
if (!node.expanded) {
node_outer_height = 0
}
node_outer_height = Math.max(node._data.view.height, node_outer_height)
if (node.children.length > 1) {
node_outer_height += this.opts.cousin_space
}
layout_data.outer_height = node_outer_height
layout_data.offset_y = base_y - node_outer_height / 2
base_y = base_y - node_outer_height - this.opts.vspace
total_height += node_outer_height
}
if (nodes_count > 1) {
total_height += this.opts.vspace * (nodes_count - 1)
}
i = nodes_count
var middle_height = total_height / 2
while (i--) {
node = nodes[i]
node._data.layout.offset_y += middle_height
}
return total_height
}
get_node_offset(node) {
var layout_data = node._data.layout
var offset_cache = null
if ('_offset_' in layout_data && this.cache_valid) {
offset_cache = layout_data._offset_
} else {
offset_cache = { x: -1, y: -1 }
layout_data._offset_ = offset_cache
}
if (offset_cache.x == -1 || offset_cache.y == -1) {
var x = layout_data.offset_x
var y = layout_data.offset_y
if (!node.isroot) {
var offset_p = this.get_node_offset(node.parent)
x += offset_p.x
y += offset_p.y
}
offset_cache.x = x
offset_cache.y = y
}
return offset_cache
}
get_node_point(node) {
var view_data = node._data.view
var offset_p = this.get_node_offset(node)
var p = {}
p.x = offset_p.x + (view_data.width * (node._data.layout.direction - 1)) / 2
p.y = offset_p.y - view_data.height / 2
return p
}
get_node_point_in(node) {
var p = this.get_node_offset(node)
return p
}
get_node_point_out(node) {
var layout_data = node._data.layout
var pout_cache = null
if ('_pout_' in layout_data && this.cache_valid) {
pout_cache = layout_data._pout_
} else {
pout_cache = { x: -1, y: -1 }
layout_data._pout_ = pout_cache
}
if (pout_cache.x == -1 || pout_cache.y == -1) {
if (node.isroot) {
pout_cache.x = 0
pout_cache.y = 0
} else {
var view_data = node._data.view
var offset_p = this.get_node_offset(node)
pout_cache.x =
offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction
pout_cache.y = offset_p.y
}
}
return pout_cache
}
get_expander_point(node) {
var p = this.get_node_point_out(node)
var ex_p = {}
if (node._data.layout.direction == Direction.right) {
ex_p.x = p.x - this.opts.pspace
} else {
ex_p.x = p.x
}
ex_p.y = p.y - Math.ceil(this.opts.pspace / 2)
return ex_p
}
get_min_size() {
var nodes = this.jm.mind.nodes
var node = null
var pout = null
for (var node_id in nodes) {
node = nodes[node_id]
pout = this.get_node_point_out(node)
if (pout.x > this.bounds.e) {
this.bounds.e = pout.x
}
if (pout.x < this.bounds.w) {
this.bounds.w = pout.x
}
}
return {
w: this.bounds.e - this.bounds.w,
h: this.bounds.s - this.bounds.n,
}
}
toggle_node(node) {
if (node.isroot) {
return
}
if (node.expanded) {
this.collapse_node(node)
} else {
this.expand_node(node)
}
}
expand_node(node) {
node.expanded = true
this.part_layout(node)
this.set_visible(node.children, true)
this.jm.invoke_event_handle(EventType.show, {
evt: 'expand_node',
data: [],
node: node.id,
})
}
collapse_node(node) {
node.expanded = false
this.part_layout(node)
this.set_visible(node.children, false)
this.jm.invoke_event_handle(EventType.show, {
evt: 'collapse_node',
data: [],
node: node.id,
})
}
expand_all() {
var nodes = this.jm.mind.nodes
var c = 0
var node
for (var node_id in nodes) {
node = nodes[node_id]
if (!node.expanded) {
node.expanded = true
c++
}
}
if (c > 0) {
var root = this.jm.mind.root
this.part_layout(root)
this.set_visible(root.children, true)
}
}
collapse_all() {
var nodes = this.jm.mind.nodes
var c = 0
var node
for (var node_id in nodes) {
node = nodes[node_id]
if (node.expanded && !node.isroot) {
node.expanded = false
c++
}
}
if (c > 0) {
var root = this.jm.mind.root
this.part_layout(root)
this.set_visible(root.children, true)
}
}
expand_to_depth(target_depth, curr_nodes, curr_depth) {
if (target_depth < 1) {
return
}
var nodes = curr_nodes || this.jm.mind.root.children
var depth = curr_depth || 1
var i = nodes.length
var node = null
while (i--) {
node = nodes[i]
if (depth < target_depth) {
if (!node.expanded) {
this.expand_node(node)
}
this.expand_to_depth(target_depth, node.children, depth + 1)
}
if (depth == target_depth) {
if (node.expanded) {
this.collapse_node(node)
}
}
}
}
part_layout(node) {
var root = this.jm.mind.root
if (!!root) {
var root_layout_data = root._data.layout
if (node.isroot) {
root_layout_data.outer_height_right = this._layout_offset_subnodes_height(
root_layout_data.right_nodes
)
root_layout_data.outer_height_left = this._layout_offset_subnodes_height(
root_layout_data.left_nodes
)
} else {
if (node._data.layout.direction == Direction.right) {
root_layout_data.outer_height_right = this._layout_offset_subnodes_height(
root_layout_data.right_nodes
)
} else {
root_layout_data.outer_height_left = this._layout_offset_subnodes_height(
root_layout_data.left_nodes
)
}
}
this.bounds.s = Math.max(
root_layout_data.outer_height_left,
root_layout_data.outer_height_right
)
this.cache_valid = false
} else {
logger.warn('can not found root node')
}
}
set_visible(nodes, visible) {
var i = nodes.length
var node = null
var layout_data = null
while (i--) {
node = nodes[i]
layout_data = node._data.layout
if (node.expanded) {
this.set_visible(node.children, visible)
} else {
this.set_visible(node.children, false)
}
if (!node.isroot) {
node._data.layout.visible = visible
}
}
}
is_expand(node) {
return node.expanded
}
is_visible(node) {
var layout_data = node._data.layout
if ('visible' in layout_data && !layout_data.visible) {
return false
} else {
return true
}
}
}

508
app/assets/javascripts/mind_map/jsmind/jsmind.mind.js Normal file → Executable file
View File

@ -1,254 +1,254 @@
import { Node } from './jsmind.node.js'
import { logger, Direction } from './jsmind.common.js'
export class Mind {
constructor() {
this.name = null
this.author = null
this.version = null
this.root = null
this.selected = null
this.nodes = {}
}
get_node(node_id) {
if (node_id in this.nodes) {
return this.nodes[node_id]
} else {
logger.warn('the node[id=' + node_id + '] can not be found')
return null
}
}
set_root(node_id, topic, data) {
if (this.root == null) {
this.root = new Node(node_id, 0, topic, data, true)
this._put_node(this.root)
return this.root
} else {
logger.error('root node is already exist')
return null
}
}
add_node(parent_node, node_id, topic, data, direction, expanded, idx) {
if (!Node.is_node(parent_node)) {
logger.error('the parent_node ' + parent_node + ' is not a node.')
return null
}
var node_index = idx || -1
var node = new Node(
node_id,
node_index,
topic,
data,
false,
parent_node,
parent_node.direction,
expanded
)
if (parent_node.isroot) {
node.direction = direction || Direction.right
}
if (this._put_node(node)) {
parent_node.children.push(node)
this._update_index(parent_node)
} else {
logger.error("fail, the node id '" + node.id + "' has been already exist.")
node = null
}
return node
}
insert_node_before(node_before, node_id, topic, data, direction) {
if (!Node.is_node(node_before)) {
logger.error('the node_before ' + node_before + ' is not a node.')
return null
}
var node_index = node_before.index - 0.5
return this.add_node(node_before.parent, node_id, topic, data, direction, true, node_index)
}
get_node_before(node) {
if (!Node.is_node(node)) {
var the_node = this.get_node(node)
if (!the_node) {
logger.error('the node[id=' + node + '] can not be found.')
return null
} else {
return this.get_node_before(the_node)
}
}
if (node.isroot) {
return null
}
var idx = node.index - 2
if (idx >= 0) {
return node.parent.children[idx]
} else {
return null
}
}
insert_node_after(node_after, node_id, topic, data, direction) {
if (!Node.is_node(node_after)) {
logger.error('the node_after ' + node_after + ' is not a node.')
return null
}
var node_index = node_after.index + 0.5
return this.add_node(node_after.parent, node_id, topic, data, direction, true, node_index)
}
get_node_after(node) {
if (!Node.is_node(node)) {
var the_node = this.get_node(node)
if (!the_node) {
logger.error('the node[id=' + node + '] can not be found.')
return null
} else {
return this.get_node_after(the_node)
}
}
if (node.isroot) {
return null
}
var idx = node.index
var brothers = node.parent.children
if (brothers.length > idx) {
return node.parent.children[idx]
} else {
return null
}
}
move_node(node, before_id, parent_id, direction) {
if (!Node.is_node(node)) {
logger.error('the parameter node ' + node + ' is not a node.')
return null
}
if (!parent_id) {
parent_id = node.parent.id
}
return this._move_node(node, before_id, parent_id, direction)
}
_flow_node_direction(node, direction) {
if (typeof direction === 'undefined') {
direction = node.direction
} else {
node.direction = direction
}
var len = node.children.length
while (len--) {
this._flow_node_direction(node.children[len], direction)
}
}
_move_node_internal(node, before_id) {
if (!!node && !!before_id) {
if (before_id == '_last_') {
node.index = -1
this._update_index(node.parent)
} else if (before_id == '_first_') {
node.index = 0
this._update_index(node.parent)
} else {
var node_before = !!before_id ? this.get_node(before_id) : null
if (
node_before != null &&
node_before.parent != null &&
node_before.parent.id == node.parent.id
) {
node.index = node_before.index - 0.5
this._update_index(node.parent)
}
}
}
return node
}
_move_node(node, before_id, parent_id, direction) {
if (!!node && !!parent_id) {
var parent_node = this.get_node(parent_id)
if (Node.inherited(node, parent_node)) {
logger.error('can not move a node to its children')
return null
}
if (node.parent.id != parent_id) {
// remove from parent's children
var sibling = node.parent.children
var si = sibling.length
while (si--) {
if (sibling[si].id == node.id) {
sibling.splice(si, 1)
break
}
}
let origin_parent = node.parent
node.parent = parent_node
parent_node.children.push(node)
this._update_index(origin_parent)
}
if (node.parent.isroot) {
if (direction == Direction.left) {
node.direction = direction
} else {
node.direction = Direction.right
}
} else {
node.direction = node.parent.direction
}
this._move_node_internal(node, before_id)
this._flow_node_direction(node)
}
return node
}
remove_node(node) {
if (!Node.is_node(node)) {
logger.error('the parameter node ' + node + ' is not a node.')
return false
}
if (node.isroot) {
logger.error('fail, can not remove root node')
return false
}
if (this.selected != null && this.selected.id == node.id) {
this.selected = null
}
// clean all subordinate nodes
var children = node.children
var ci = children.length
while (ci--) {
this.remove_node(children[ci])
}
// clean all children
children.length = 0
var node_parent = node.parent
// remove from parent's children
var sibling = node_parent.children
var si = sibling.length
while (si--) {
if (sibling[si].id == node.id) {
sibling.splice(si, 1)
break
}
}
// remove from global nodes
delete this.nodes[node.id]
// clean all properties
for (var k in node) {
delete node[k]
}
// remove it's self
node = null
this._update_index(node_parent)
return true
}
_put_node(node) {
if (node.id in this.nodes) {
logger.warn("the node_id '" + node.id + "' has been already exist.")
return false
} else {
this.nodes[node.id] = node
return true
}
}
_update_index(node) {
if (node instanceof Node) {
node.children.sort(Node.compare)
for (var i = 0; i < node.children.length; i++) {
node.children[i].index = i + 1
}
}
}
}
import { Node } from './jsmind.node.js'
import { logger, Direction } from './jsmind.common.js'
export class Mind {
constructor() {
this.name = null
this.author = null
this.version = null
this.root = null
this.selected = null
this.nodes = {}
}
get_node(node_id) {
if (node_id in this.nodes) {
return this.nodes[node_id]
} else {
logger.warn('the node[id=' + node_id + '] can not be found')
return null
}
}
set_root(node_id, topic, data) {
if (this.root == null) {
this.root = new Node(node_id, 0, topic, data, true)
this._put_node(this.root)
return this.root
} else {
logger.error('root node is already exist')
return null
}
}
add_node(parent_node, node_id, topic, data, direction, expanded, idx) {
if (!Node.is_node(parent_node)) {
logger.error('the parent_node ' + parent_node + ' is not a node.')
return null
}
var node_index = idx || -1
var node = new Node(
node_id,
node_index,
topic,
data,
false,
parent_node,
parent_node.direction,
expanded
)
if (parent_node.isroot) {
node.direction = direction || Direction.right
}
if (this._put_node(node)) {
parent_node.children.push(node)
this._update_index(parent_node)
} else {
logger.error("fail, the node id '" + node.id + "' has been already exist.")
node = null
}
return node
}
insert_node_before(node_before, node_id, topic, data, direction) {
if (!Node.is_node(node_before)) {
logger.error('the node_before ' + node_before + ' is not a node.')
return null
}
var node_index = node_before.index - 0.5
return this.add_node(node_before.parent, node_id, topic, data, direction, true, node_index)
}
get_node_before(node) {
if (!Node.is_node(node)) {
var the_node = this.get_node(node)
if (!the_node) {
logger.error('the node[id=' + node + '] can not be found.')
return null
} else {
return this.get_node_before(the_node)
}
}
if (node.isroot) {
return null
}
var idx = node.index - 2
if (idx >= 0) {
return node.parent.children[idx]
} else {
return null
}
}
insert_node_after(node_after, node_id, topic, data, direction) {
if (!Node.is_node(node_after)) {
logger.error('the node_after ' + node_after + ' is not a node.')
return null
}
var node_index = node_after.index + 0.5
return this.add_node(node_after.parent, node_id, topic, data, direction, true, node_index)
}
get_node_after(node) {
if (!Node.is_node(node)) {
var the_node = this.get_node(node)
if (!the_node) {
logger.error('the node[id=' + node + '] can not be found.')
return null
} else {
return this.get_node_after(the_node)
}
}
if (node.isroot) {
return null
}
var idx = node.index
var brothers = node.parent.children
if (brothers.length > idx) {
return node.parent.children[idx]
} else {
return null
}
}
move_node(node, before_id, parent_id, direction) {
if (!Node.is_node(node)) {
logger.error('the parameter node ' + node + ' is not a node.')
return null
}
if (!parent_id) {
parent_id = node.parent.id
}
return this._move_node(node, before_id, parent_id, direction)
}
_flow_node_direction(node, direction) {
if (typeof direction === 'undefined') {
direction = node.direction
} else {
node.direction = direction
}
var len = node.children.length
while (len--) {
this._flow_node_direction(node.children[len], direction)
}
}
_move_node_internal(node, before_id) {
if (!!node && !!before_id) {
if (before_id == '_last_') {
node.index = -1
this._update_index(node.parent)
} else if (before_id == '_first_') {
node.index = 0
this._update_index(node.parent)
} else {
var node_before = !!before_id ? this.get_node(before_id) : null
if (
node_before != null &&
node_before.parent != null &&
node_before.parent.id == node.parent.id
) {
node.index = node_before.index - 0.5
this._update_index(node.parent)
}
}
}
return node
}
_move_node(node, before_id, parent_id, direction) {
if (!!node && !!parent_id) {
var parent_node = this.get_node(parent_id)
if (Node.inherited(node, parent_node)) {
logger.error('can not move a node to its children')
return null
}
if (node.parent.id != parent_id) {
// remove from parent's children
var sibling = node.parent.children
var si = sibling.length
while (si--) {
if (sibling[si].id == node.id) {
sibling.splice(si, 1)
break
}
}
let origin_parent = node.parent
node.parent = parent_node
parent_node.children.push(node)
this._update_index(origin_parent)
}
if (node.parent.isroot) {
if (direction == Direction.left) {
node.direction = direction
} else {
node.direction = Direction.right
}
} else {
node.direction = node.parent.direction
}
this._move_node_internal(node, before_id)
this._flow_node_direction(node)
}
return node
}
remove_node(node) {
if (!Node.is_node(node)) {
logger.error('the parameter node ' + node + ' is not a node.')
return false
}
if (node.isroot) {
logger.error('fail, can not remove root node')
return false
}
if (this.selected != null && this.selected.id == node.id) {
this.selected = null
}
// clean all subordinate nodes
var children = node.children
var ci = children.length
while (ci--) {
this.remove_node(children[ci])
}
// clean all children
children.length = 0
var node_parent = node.parent
// remove from parent's children
var sibling = node_parent.children
var si = sibling.length
while (si--) {
if (sibling[si].id == node.id) {
sibling.splice(si, 1)
break
}
}
// remove from global nodes
delete this.nodes[node.id]
// clean all properties
for (var k in node) {
delete node[k]
}
// remove it's self
node = null
this._update_index(node_parent)
return true
}
_put_node(node) {
if (node.id in this.nodes) {
logger.warn("the node_id '" + node.id + "' has been already exist.")
return false
} else {
this.nodes[node.id] = node
return true
}
}
_update_index(node) {
if (node instanceof Node) {
node.children.sort(Node.compare)
for (var i = 0; i < node.children.length; i++) {
node.children[i].index = i + 1
}
}
}
}

164
app/assets/javascripts/mind_map/jsmind/jsmind.node.js Normal file → Executable file
View File

@ -1,82 +1,82 @@
import { logger } from './jsmind.common.js'
export class Node {
constructor(sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) {
if (!sId) {
logger.error('invalid node id')
return
}
if (typeof iIndex != 'number') {
logger.error('invalid node index')
return
}
if (typeof bExpanded === 'undefined') {
bExpanded = true
}
this.id = sId
this.index = iIndex
this.topic = sTopic
this.data = oData || {}
this.isroot = bIsRoot
this.parent = oParent
this.direction = eDirection
this.expanded = !!bExpanded
this.children = []
this._data = {}
}
get_location() {
var vd = this._data.view
return {
x: vd.abs_x,
y: vd.abs_y,
}
}
get_size() {
var vd = this._data.view
return {
w: vd.width,
h: vd.height,
}
}
static compare(node1, node2) {
// '-1' is always the latest
var r = 0
var i1 = node1.index
var i2 = node2.index
if (i1 >= 0 && i2 >= 0) {
r = i1 - i2
} else if (i1 == -1 && i2 == -1) {
r = 0
} else if (i1 == -1) {
r = 1
} else if (i2 == -1) {
r = -1
} else {
r = 0
}
return r
}
static inherited(parent_node, node) {
if (!!parent_node && !!node) {
if (parent_node.id === node.id) {
return true
}
if (parent_node.isroot) {
return true
}
var pid = parent_node.id
var p = node
while (!p.isroot) {
p = p.parent
if (p.id === pid) {
return true
}
}
}
return false
}
static is_node(n) {
return !!n && n instanceof Node
}
}
import { logger } from './jsmind.common.js'
export class Node {
constructor(sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) {
if (!sId) {
logger.error('invalid node id')
return
}
if (typeof iIndex != 'number') {
logger.error('invalid node index')
return
}
if (typeof bExpanded === 'undefined') {
bExpanded = true
}
this.id = sId
this.index = iIndex
this.topic = sTopic
this.data = oData || {}
this.isroot = bIsRoot
this.parent = oParent
this.direction = eDirection
this.expanded = !!bExpanded
this.children = []
this._data = {}
}
get_location() {
var vd = this._data.view
return {
x: vd.abs_x,
y: vd.abs_y,
}
}
get_size() {
var vd = this._data.view
return {
w: vd.width,
h: vd.height,
}
}
static compare(node1, node2) {
// '-1' is always the latest
var r = 0
var i1 = node1.index
var i2 = node2.index
if (i1 >= 0 && i2 >= 0) {
r = i1 - i2
} else if (i1 == -1 && i2 == -1) {
r = 0
} else if (i1 == -1) {
r = 1
} else if (i2 == -1) {
r = -1
} else {
r = 0
}
return r
}
static inherited(parent_node, node) {
if (!!parent_node && !!node) {
if (parent_node.id === node.id) {
return true
}
if (parent_node.isroot) {
return true
}
var pid = parent_node.id
var p = node
while (!p.isroot) {
p = p.parent
if (p.id === pid) {
return true
}
}
}
return false
}
static is_node(n) {
return !!n && n instanceof Node
}
}

138
app/assets/javascripts/mind_map/jsmind/jsmind.option.js Normal file → Executable file
View File

@ -1,69 +1,69 @@
import { util } from './jsmind.util.js'
const default_options = {
container: '', // id of the container
editable: false, // you can change it in your options
theme: null,
mode: 'full', // full or side
support_html: true,
log_level: 'info',
view: {
engine: 'canvas',
enable_device_pixel_ratio: false,
hmargin: 100,
vmargin: 50,
line_width: 2,
line_color: '#555',
line_style: 'curved', // [straight | curved]
draggable: false, // drag the mind map with your mouse, when it's larger that the container
hide_scrollbars_when_draggable: false, // hide container scrollbars, when mind map is larger than container and draggable option is true.
node_overflow: 'hidden', // [hidden | wrap]
zoom: {
min: 0.5,
max: 2.1,
step: 0.1,
},
custom_node_render: null,
expander_style: 'char', // [char | number]
},
layout: {
hspace: 30,
vspace: 20,
pspace: 13,
cousin_space: 0,
},
default_event_handle: {
enable_mousedown_handle: true,
enable_click_handle: true,
enable_dblclick_handle: true,
enable_mousewheel_handle: true,
},
shortcut: {
enable: true,
handles: {},
mapping: {
addchild: [45, 4096 + 13], // Insert, Ctrl+Enter
addbrother: 13, // Enter
editnode: 113, // F2
delnode: 46, // Delete
toggle: 32, // Space
left: 37, // Left
up: 38, // Up
right: 39, // Right
down: 40, // Down
},
},
plugin: {},
}
export function merge_option(options) {
var opts = {}
util.json.merge(opts, default_options)
util.json.merge(opts, options)
if (!opts.container) {
throw new Error('the options.container should not be null or empty.')
}
return opts
}
import { util } from './jsmind.util.js'
const default_options = {
container: '', // id of the container
editable: false, // you can change it in your options
theme: null,
mode: 'full', // full or side
support_html: true,
log_level: 'info',
view: {
engine: 'canvas',
enable_device_pixel_ratio: false,
hmargin: 100,
vmargin: 50,
line_width: 2,
line_color: '#555',
line_style: 'curved', // [straight | curved]
draggable: false, // drag the mind map with your mouse, when it's larger that the container
hide_scrollbars_when_draggable: false, // hide container scrollbars, when mind map is larger than container and draggable option is true.
node_overflow: 'hidden', // [hidden | wrap]
zoom: {
min: 0.5,
max: 2.1,
step: 0.1,
},
custom_node_render: null,
expander_style: 'char', // [char | number]
},
layout: {
hspace: 30,
vspace: 20,
pspace: 13,
cousin_space: 0,
},
default_event_handle: {
enable_mousedown_handle: true,
enable_click_handle: true,
enable_dblclick_handle: true,
enable_mousewheel_handle: true,
},
shortcut: {
enable: true,
handles: {},
mapping: {
addchild: [45, 4096 + 13], // Insert, Ctrl+Enter
addbrother: 13, // Enter
editnode: 113, // F2
delnode: 46, // Delete
toggle: 32, // Space
left: 37, // Left
up: 38, // Up
right: 39, // Right
down: 40, // Down
},
},
plugin: {},
}
export function merge_option(options) {
var opts = {}
util.json.merge(opts, default_options)
util.json.merge(opts, options)
if (!opts.container) {
throw new Error('the options.container should not be null or empty.')
}
return opts
}

78
app/assets/javascripts/mind_map/jsmind/jsmind.plugin.js Normal file → Executable file
View File

@ -1,39 +1,39 @@
import { $ } from './jsmind.dom.js'
const plugin_data = {
plugins: [],
}
export function register(plugin) {
if (!(plugin instanceof Plugin)) {
throw new Error('can not register plugin, it is not an instance of Plugin')
}
if (plugin_data.plugins.map((p) => p.name).includes(plugin.name)) {
throw new Error('can not register plugin ' + plugin.name + ': plugin name already exist')
}
plugin_data.plugins.push(plugin)
}
export function apply(jm, options) {
$.w.setTimeout(function () {
_apply(jm, options)
}, 0)
}
function _apply(jm, options) {
plugin_data.plugins.forEach((p) => p.fn_init(jm, options[p.name]))
}
export class Plugin {
// function fn_init(jm, options){ }
constructor(name, fn_init) {
if (!name) {
throw new Error('plugin must has a name')
}
if (!fn_init || typeof fn_init !== 'function') {
throw new Error('plugin must has an init function')
}
this.name = name
this.fn_init = fn_init
}
}
import { $ } from './jsmind.dom.js'
const plugin_data = {
plugins: [],
}
export function register(plugin) {
if (!(plugin instanceof Plugin)) {
throw new Error('can not register plugin, it is not an instance of Plugin')
}
if (plugin_data.plugins.map((p) => p.name).includes(plugin.name)) {
throw new Error('can not register plugin ' + plugin.name + ': plugin name already exist')
}
plugin_data.plugins.push(plugin)
}
export function apply(jm, options) {
$.w.setTimeout(function () {
_apply(jm, options)
}, 0)
}
function _apply(jm, options) {
plugin_data.plugins.forEach((p) => p.fn_init(jm, options[p.name]))
}
export class Plugin {
// function fn_init(jm, options){ }
constructor(name, fn_init) {
if (!name) {
throw new Error('plugin must has a name')
}
if (!fn_init || typeof fn_init !== 'function') {
throw new Error('plugin must has an init function')
}
this.name = name
this.fn_init = fn_init
}
}

View File

@ -1,188 +1,188 @@
import { $ } from './jsmind.dom.js'
import { util } from './jsmind.util.js'
import { Direction } from './jsmind.common.js'
export class ShortcutProvider {
constructor(jm, options) {
this.jm = jm
this.opts = options
this.mapping = options.mapping
this.handles = options.handles
this._newid = null
this._mapping = {}
}
init() {
$.on(this.jm.view.e_panel, 'keydown', this.handler.bind(this))
this.handles['addchild'] = this.handle_addchild
this.handles['addbrother'] = this.handle_addbrother
this.handles['editnode'] = this.handle_editnode
this.handles['delnode'] = this.handle_delnode
this.handles['toggle'] = this.handle_toggle
this.handles['up'] = this.handle_up
this.handles['down'] = this.handle_down
this.handles['left'] = this.handle_left
this.handles['right'] = this.handle_right
for (var handle in this.mapping) {
if (!!this.mapping[handle] && handle in this.handles) {
let keys = this.mapping[handle]
if (!Array.isArray(keys)) {
keys = [keys]
}
for (let key of keys) {
this._mapping[key] = this.handles[handle]
}
}
}
if (typeof this.opts.id_generator === 'function') {
this._newid = this.opts.id_generator
} else {
this._newid = util.uuid.newid
}
}
enable_shortcut() {
this.opts.enable = true
}
disable_shortcut() {
this.opts.enable = false
}
handler(e) {
if (e.which == 9) {
e.preventDefault()
} //prevent tab to change focus in browser
if (this.jm.view.is_editing()) {
return
}
var evt = e || event
if (!this.opts.enable) {
return true
}
var kc =
evt.keyCode +
(evt.metaKey << 13) +
(evt.ctrlKey << 12) +
(evt.altKey << 11) +
(evt.shiftKey << 10)
if (kc in this._mapping) {
this._mapping[kc].call(this, this.jm, e)
}
}
handle_addchild(_jm, e) {
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
var node_id = this._newid()
var node = _jm.add_node(selected_node, node_id, 'New Node')
if (!!node) {
_jm.select_node(node_id)
_jm.begin_edit(node_id)
}
}
}
handle_addbrother(_jm, e) {
var selected_node = _jm.get_selected_node()
if (!!selected_node && !selected_node.isroot) {
var node_id = this._newid()
var node = _jm.insert_node_after(selected_node, node_id, 'New Node')
if (!!node) {
_jm.select_node(node_id)
_jm.begin_edit(node_id)
}
}
}
handle_editnode(_jm, e) {
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
_jm.begin_edit(selected_node)
}
}
handle_delnode(_jm, e) {
var selected_node = _jm.get_selected_node()
if (!!selected_node && !selected_node.isroot) {
_jm.select_node(selected_node.parent)
_jm.remove_node(selected_node)
}
}
handle_toggle(_jm, e) {
var evt = e || event
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
_jm.toggle_node(selected_node.id)
evt.stopPropagation()
evt.preventDefault()
}
}
handle_up(_jm, e) {
var evt = e || event
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
var up_node = _jm.find_node_before(selected_node)
if (!up_node) {
var np = _jm.find_node_before(selected_node.parent)
if (!!np && np.children.length > 0) {
up_node = np.children[np.children.length - 1]
}
}
if (!!up_node) {
_jm.select_node(up_node)
}
evt.stopPropagation()
evt.preventDefault()
}
}
handle_down(_jm, e) {
var evt = e || event
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
var down_node = _jm.find_node_after(selected_node)
if (!down_node) {
var np = _jm.find_node_after(selected_node.parent)
if (!!np && np.children.length > 0) {
down_node = np.children[0]
}
}
if (!!down_node) {
_jm.select_node(down_node)
}
evt.stopPropagation()
evt.preventDefault()
}
}
handle_left(_jm, e) {
this._handle_direction(_jm, e, Direction.left)
}
handle_right(_jm, e) {
this._handle_direction(_jm, e, Direction.right)
}
_handle_direction(_jm, e, d) {
var evt = e || event
var selected_node = _jm.get_selected_node()
var node = null
if (!!selected_node) {
if (selected_node.isroot) {
var c = selected_node.children
var children = []
for (var i = 0; i < c.length; i++) {
if (c[i].direction === d) {
children.push(i)
}
}
node = c[children[Math.floor((children.length - 1) / 2)]]
} else if (selected_node.direction === d) {
var children = selected_node.children
var children_count = children.length
if (children_count > 0) {
node = children[Math.floor((children_count - 1) / 2)]
}
} else {
node = selected_node.parent
}
if (!!node) {
_jm.select_node(node)
}
evt.stopPropagation()
evt.preventDefault()
}
}
}
import { $ } from './jsmind.dom.js'
import { util } from './jsmind.util.js'
import { Direction } from './jsmind.common.js'
export class ShortcutProvider {
constructor(jm, options) {
this.jm = jm
this.opts = options
this.mapping = options.mapping
this.handles = options.handles
this._newid = null
this._mapping = {}
}
init() {
$.on(this.jm.view.e_panel, 'keydown', this.handler.bind(this))
this.handles['addchild'] = this.handle_addchild
this.handles['addbrother'] = this.handle_addbrother
this.handles['editnode'] = this.handle_editnode
this.handles['delnode'] = this.handle_delnode
this.handles['toggle'] = this.handle_toggle
this.handles['up'] = this.handle_up
this.handles['down'] = this.handle_down
this.handles['left'] = this.handle_left
this.handles['right'] = this.handle_right
for (var handle in this.mapping) {
if (!!this.mapping[handle] && handle in this.handles) {
let keys = this.mapping[handle]
if (!Array.isArray(keys)) {
keys = [keys]
}
for (let key of keys) {
this._mapping[key] = this.handles[handle]
}
}
}
if (typeof this.opts.id_generator === 'function') {
this._newid = this.opts.id_generator
} else {
this._newid = util.uuid.newid
}
}
enable_shortcut() {
this.opts.enable = true
}
disable_shortcut() {
this.opts.enable = false
}
handler(e) {
if (e.which == 9) {
e.preventDefault()
} //prevent tab to change focus in browser
if (this.jm.view.is_editing()) {
return
}
var evt = e || event
if (!this.opts.enable) {
return true
}
var kc =
evt.keyCode +
(evt.metaKey << 13) +
(evt.ctrlKey << 12) +
(evt.altKey << 11) +
(evt.shiftKey << 10)
if (kc in this._mapping) {
this._mapping[kc].call(this, this.jm, e)
}
}
handle_addchild(_jm, e) {
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
var node_id = this._newid()
var node = _jm.add_node(selected_node, node_id, 'New Node')
if (!!node) {
_jm.select_node(node_id)
_jm.begin_edit(node_id)
}
}
}
handle_addbrother(_jm, e) {
var selected_node = _jm.get_selected_node()
if (!!selected_node && !selected_node.isroot) {
var node_id = this._newid()
var node = _jm.insert_node_after(selected_node, node_id, 'New Node')
if (!!node) {
_jm.select_node(node_id)
_jm.begin_edit(node_id)
}
}
}
handle_editnode(_jm, e) {
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
_jm.begin_edit(selected_node)
}
}
handle_delnode(_jm, e) {
var selected_node = _jm.get_selected_node()
if (!!selected_node && !selected_node.isroot) {
_jm.select_node(selected_node.parent)
_jm.remove_node(selected_node)
}
}
handle_toggle(_jm, e) {
var evt = e || event
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
_jm.toggle_node(selected_node.id)
evt.stopPropagation()
evt.preventDefault()
}
}
handle_up(_jm, e) {
var evt = e || event
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
var up_node = _jm.find_node_before(selected_node)
if (!up_node) {
var np = _jm.find_node_before(selected_node.parent)
if (!!np && np.children.length > 0) {
up_node = np.children[np.children.length - 1]
}
}
if (!!up_node) {
_jm.select_node(up_node)
}
evt.stopPropagation()
evt.preventDefault()
}
}
handle_down(_jm, e) {
var evt = e || event
var selected_node = _jm.get_selected_node()
if (!!selected_node) {
var down_node = _jm.find_node_after(selected_node)
if (!down_node) {
var np = _jm.find_node_after(selected_node.parent)
if (!!np && np.children.length > 0) {
down_node = np.children[0]
}
}
if (!!down_node) {
_jm.select_node(down_node)
}
evt.stopPropagation()
evt.preventDefault()
}
}
handle_left(_jm, e) {
this._handle_direction(_jm, e, Direction.left)
}
handle_right(_jm, e) {
this._handle_direction(_jm, e, Direction.right)
}
_handle_direction(_jm, e, d) {
var evt = e || event
var selected_node = _jm.get_selected_node()
var node = null
if (!!selected_node) {
if (selected_node.isroot) {
var c = selected_node.children
var children = []
for (var i = 0; i < c.length; i++) {
if (c[i].direction === d) {
children.push(i)
}
}
node = c[children[Math.floor((children.length - 1) / 2)]]
} else if (selected_node.direction === d) {
var children = selected_node.children
var children_count = children.length
if (children_count > 0) {
node = children[Math.floor((children_count - 1) / 2)]
}
} else {
node = selected_node.parent
}
if (!!node) {
_jm.select_node(node)
}
evt.stopPropagation()
evt.preventDefault()
}
}
}

188
app/assets/javascripts/mind_map/jsmind/jsmind.util.js Normal file → Executable file
View File

@ -1,94 +1,94 @@
import { $ } from './jsmind.dom.js'
export const util = {
file: {
read: function (file_data, fn_callback) {
var reader = new FileReader()
reader.onload = function () {
if (typeof fn_callback === 'function') {
fn_callback(this.result, file_data.name)
}
}
reader.readAsText(file_data)
},
save: function (file_data, type, name) {
var blob
if (typeof $.w.Blob === 'function') {
blob = new Blob([file_data], { type: type })
} else {
var BlobBuilder =
$.w.BlobBuilder ||
$.w.MozBlobBuilder ||
$.w.WebKitBlobBuilder ||
$.w.MSBlobBuilder
var bb = new BlobBuilder()
bb.append(file_data)
blob = bb.getBlob(type)
}
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, name)
} else {
var URL = $.w.URL || $.w.webkitURL
var blob_url = URL.createObjectURL(blob)
var anchor = $.c('a')
if ('download' in anchor) {
anchor.style.visibility = 'hidden'
anchor.href = blob_url
anchor.download = name
$.d.body.appendChild(anchor)
var evt = $.d.createEvent('MouseEvents')
evt.initEvent('click', true, true)
anchor.dispatchEvent(evt)
$.d.body.removeChild(anchor)
} else {
location.href = blob_url
}
}
},
},
json: {
json2string: function (json) {
return JSON.stringify(json)
},
string2json: function (json_str) {
return JSON.parse(json_str)
},
merge: function (b, a) {
for (var o in a) {
if (o in b) {
if (
typeof b[o] === 'object' &&
Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' &&
!b[o].length
) {
util.json.merge(b[o], a[o])
} else {
b[o] = a[o]
}
} else {
b[o] = a[o]
}
}
return b
},
},
uuid: {
newid: function () {
return (
new Date().getTime().toString(16) + Math.random().toString(16).substring(2)
).substring(2, 18)
},
},
text: {
is_empty: function (s) {
if (!s) {
return true
}
return s.replace(/\s*/, '').length == 0
},
},
}
import { $ } from './jsmind.dom.js'
export const util = {
file: {
read: function (file_data, fn_callback) {
var reader = new FileReader()
reader.onload = function () {
if (typeof fn_callback === 'function') {
fn_callback(this.result, file_data.name)
}
}
reader.readAsText(file_data)
},
save: function (file_data, type, name) {
var blob
if (typeof $.w.Blob === 'function') {
blob = new Blob([file_data], { type: type })
} else {
var BlobBuilder =
$.w.BlobBuilder ||
$.w.MozBlobBuilder ||
$.w.WebKitBlobBuilder ||
$.w.MSBlobBuilder
var bb = new BlobBuilder()
bb.append(file_data)
blob = bb.getBlob(type)
}
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, name)
} else {
var URL = $.w.URL || $.w.webkitURL
var blob_url = URL.createObjectURL(blob)
var anchor = $.c('a')
if ('download' in anchor) {
anchor.style.visibility = 'hidden'
anchor.href = blob_url
anchor.download = name
$.d.body.appendChild(anchor)
var evt = $.d.createEvent('MouseEvents')
evt.initEvent('click', true, true)
anchor.dispatchEvent(evt)
$.d.body.removeChild(anchor)
} else {
location.href = blob_url
}
}
},
},
json: {
json2string: function (json) {
return JSON.stringify(json)
},
string2json: function (json_str) {
return JSON.parse(json_str)
},
merge: function (b, a) {
for (var o in a) {
if (o in b) {
if (
typeof b[o] === 'object' &&
Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' &&
!b[o].length
) {
util.json.merge(b[o], a[o])
} else {
b[o] = a[o]
}
} else {
b[o] = a[o]
}
}
return b
},
},
uuid: {
newid: function () {
return (
new Date().getTime().toString(16) + Math.random().toString(16).substring(2)
).substring(2, 18)
},
},
text: {
is_empty: function (s) {
if (!s) {
return true
}
return s.replace(/\s*/, '').length == 0
},
},
}

File diff suppressed because it is too large Load Diff

View File

@ -1,466 +1,466 @@
import jsMind from '../jsmind.js'
if (!jsMind) {
throw new Error('jsMind is not defined')
}
const $ = jsMind.$
const clear_selection =
'getSelection' in $.w
? function () {
$.w.getSelection().removeAllRanges()
}
: function () {
$.d.selection.empty()
}
const DEFAULT_OPTIONS = {
line_width: 5,
line_color: 'rgba(0,0,0,0.3)',
line_color_invalid: 'rgba(255,51,51,0.6)',
lookup_delay: 200,
lookup_interval: 100,
scrolling_trigger_width: 20,
scrolling_step_length: 10,
shadow_node_class_name: 'jsmind-draggable-shadow-node',
}
class DraggableNode {
constructor(jm, options) {
var opts = {}
jsMind.util.json.merge(opts, DEFAULT_OPTIONS)
jsMind.util.json.merge(opts, options)
this.version = '0.4.0'
this.jm = jm
this.options = opts
this.e_canvas = null
this.canvas_ctx = null
this.shadow = null
this.shadow_p_x = 0
this.shadow_p_y = 0
this.shadow_w = 0
this.shadow_h = 0
this.active_node = null
this.target_node = null
this.target_direct = null
this.client_w = 0
this.client_h = 0
this.offset_x = 0
this.offset_y = 0
this.hlookup_delay = 0
this.hlookup_timer = 0
this.capture = false
this.moved = false
this.canvas_draggable = jm.get_view_draggable()
this.view_panel = jm.view.e_panel
this.view_panel_rect = null
}
init() {
this.create_canvas()
this.create_shadow()
this.event_bind()
}
resize() {
this.jm.view.e_nodes.appendChild(this.shadow)
this.e_canvas.width = this.jm.view.size.w
this.e_canvas.height = this.jm.view.size.h
}
create_canvas() {
var c = $.c('canvas')
this.jm.view.e_panel.appendChild(c)
var ctx = c.getContext('2d')
this.e_canvas = c
this.canvas_ctx = ctx
}
create_shadow() {
var s = $.c('jmnode')
s.style.visibility = 'hidden'
s.style.zIndex = '3'
s.style.cursor = 'move'
s.style.opacity = '0.7'
s.className = this.options.shadow_node_class_name
this.shadow = s
}
reset_shadow(el) {
var s = this.shadow.style
this.shadow.innerHTML = el.innerHTML
s.left = el.style.left
s.top = el.style.top
s.width = el.style.width
s.height = el.style.height
s.backgroundImage = el.style.backgroundImage
s.backgroundSize = el.style.backgroundSize
s.transform = el.style.transform
this.shadow_w = this.shadow.clientWidth
this.shadow_h = this.shadow.clientHeight
}
show_shadow() {
if (!this.moved) {
this.shadow.style.visibility = 'visible'
}
}
hide_shadow() {
this.shadow.style.visibility = 'hidden'
}
magnet_shadow(shadow_p, node_p, invalid) {
this.canvas_ctx.lineWidth = this.options.line_width
this.canvas_ctx.strokeStyle = invalid
? this.options.line_color_invalid
: this.options.line_color
this.canvas_ctx.lineCap = 'round'
this.clear_lines()
this.canvas_lineto(shadow_p.x, shadow_p.y, node_p.x, node_p.y)
}
clear_lines() {
this.canvas_ctx.clearRect(0, 0, this.jm.view.size.w, this.jm.view.size.h)
}
canvas_lineto(x1, y1, x2, y2) {
this.canvas_ctx.beginPath()
this.canvas_ctx.moveTo(x1, y1)
this.canvas_ctx.lineTo(x2, y2)
this.canvas_ctx.stroke()
}
event_bind() {
var jd = this
var container = this.jm.view.container
$.on(container, 'mousedown', function (e) {
if (e.button === 0) {
jd.dragstart.call(jd, e)
}
})
$.on(container, 'mousemove', function (e) {
if (e.movementX !== 0 || e.movementY !== 0) {
jd.drag.call(jd, e)
}
})
$.on(container, 'mouseup', function (e) {
jd.dragend.call(jd, e)
})
$.on(container, 'touchstart', function (e) {
jd.dragstart.call(jd, e)
})
$.on(container, 'touchmove', function (e) {
jd.drag.call(jd, e)
})
$.on(container, 'touchend', function (e) {
jd.dragend.call(jd, e)
})
}
dragstart(e) {
if (!this.jm.get_editable()) {
return
}
if (this.capture) {
return
}
var jview = this.jm.view
if (jview.is_editing()) {
return
}
this.active_node = null
this.view_draggable = this.jm.get_view_draggable()
var el = this.find_node_element(e.target)
if (!el) {
return
}
if (this.view_draggable) {
this.jm.disable_view_draggable()
}
var nodeid = jview.get_binded_nodeid(el)
if (!!nodeid) {
var node = this.jm.get_node(nodeid)
if (!node.isroot) {
this.reset_shadow(el)
this.view_panel_rect = this.view_panel.getBoundingClientRect()
this.active_node = node
this.offset_x =
(e.clientX || e.touches[0].clientX) / jview.zoom_current - el.offsetLeft
this.offset_y =
(e.clientY || e.touches[0].clientY) / jview.zoom_current - el.offsetTop
this.client_hw = Math.floor(el.clientWidth / 2)
this.client_hh = Math.floor(el.clientHeight / 2)
if (this.hlookup_delay != 0) {
$.w.clearTimeout(this.hlookup_delay)
}
if (this.hlookup_timer != 0) {
$.w.clearInterval(this.hlookup_timer)
}
var jd = this
this.hlookup_delay = $.w.setTimeout(function () {
jd.hlookup_delay = 0
jd.hlookup_timer = $.w.setInterval(function () {
jd.lookup_target_node.call(jd)
}, jd.options.lookup_interval)
}, this.options.lookup_delay)
jd.capture = true
}
}
}
drag(e) {
if (!this.jm.get_editable()) {
return
}
if (this.capture) {
e.preventDefault()
this.show_shadow()
this.moved = true
clear_selection()
var jview = this.jm.view
var px = (e.clientX || e.touches[0].clientX) / jview.zoom_current - this.offset_x
var py = (e.clientY || e.touches[0].clientY) / jview.zoom_current - this.offset_y
// scrolling container axisY if drag nodes exceeding container
if (
e.clientY - this.view_panel_rect.top < this.options.scrolling_trigger_width &&
this.view_panel.scrollTop > this.options.scrolling_step_length
) {
this.view_panel.scrollBy(0, -this.options.scrolling_step_length)
this.offset_y += this.options.scrolling_step_length / jview.zoom_current
} else if (
this.view_panel_rect.bottom - e.clientY < this.options.scrolling_trigger_width &&
this.view_panel.scrollTop <
this.view_panel.scrollHeight -
this.view_panel_rect.height -
this.options.scrolling_step_length
) {
this.view_panel.scrollBy(0, this.options.scrolling_step_length)
this.offset_y -= this.options.scrolling_step_length / jview.zoom_current
}
// scrolling container axisX if drag nodes exceeding container
if (
e.clientX - this.view_panel_rect.left < this.options.scrolling_trigger_width &&
this.view_panel.scrollLeft > this.options.scrolling_step_length
) {
this.view_panel.scrollBy(-this.options.scrolling_step_length, 0)
this.offset_x += this.options.scrolling_step_length / jview.zoom_current
} else if (
this.view_panel_rect.right - e.clientX < this.options.scrolling_trigger_width &&
this.view_panel.scrollLeft <
this.view_panel.scrollWidth -
this.view_panel_rect.width -
this.options.scrolling_step_length
) {
this.view_panel.scrollBy(this.options.scrolling_step_length, 0)
this.offset_x -= this.options.scrolling_step_length / jview.zoom_current
}
this.shadow.style.left = px + 'px'
this.shadow.style.top = py + 'px'
clear_selection()
}
}
dragend(e) {
if (!this.jm.get_editable()) {
return
}
if (this.view_draggable) {
this.jm.enable_view_draggable()
}
if (this.capture) {
if (this.hlookup_delay != 0) {
$.w.clearTimeout(this.hlookup_delay)
this.hlookup_delay = 0
this.clear_lines()
}
if (this.hlookup_timer != 0) {
$.w.clearInterval(this.hlookup_timer)
this.hlookup_timer = 0
this.clear_lines()
}
if (this.moved) {
var src_node = this.active_node
var target_node = this.target_node
var target_direct = this.target_direct
this.move_node(src_node, target_node, target_direct)
}
this.hide_shadow()
}
this.view_panel_rect = null
this.moved = false
this.capture = false
}
find_node_element(el) {
if (
!el ||
el === this.jm.view.e_nodes ||
el === this.jm.view.e_panel ||
el === this.jm.view.container
) {
return null
}
if (el.tagName.toLowerCase() === 'jmnode') {
return el
}
return this.find_node_element(el.parentNode)
}
lookup_target_node() {
let sx = this.shadow.offsetLeft
let sy = this.shadow.offsetTop
if (sx === this.shadow_p_x && sy === this.shadow_p_y) {
return
}
this.shadow_p_x = sx
this.shadow_p_y = sy
let target_direction =
this.shadow_p_x + this.shadow_w / 2 >= this.get_root_x()
? jsMind.direction.right
: jsMind.direction.left
let overlapping_node = this.lookup_overlapping_node_parent(target_direction)
let target_node = overlapping_node || this.lookup_close_node(target_direction)
if (!!target_node) {
let points = this.calc_point_of_node(target_node, target_direction)
let invalid = jsMind.node.inherited(this.active_node, target_node)
this.magnet_shadow(points.sp, points.np, invalid)
this.target_node = target_node
this.target_direct = target_direction
}
}
get_root_x() {
let root = this.jm.get_root()
let root_location = root.get_location()
let root_size = root.get_size()
return root_location.x + root_size.w / 2
}
lookup_overlapping_node_parent(direction) {
let shadowRect = this.shadow.getBoundingClientRect()
let x = shadowRect.x + (shadowRect.width * (1 - direction)) / 2
let deltaX = (this.jm.options.layout.hspace + this.jm.options.layout.pspace) * direction
let deltaY = shadowRect.height
let points = [
[x, shadowRect.y],
[x, shadowRect.y + deltaY / 2],
[x, shadowRect.y + deltaY],
[x + deltaX / 2, shadowRect.y],
[x + deltaX / 2, shadowRect.y + deltaY / 2],
[x + deltaX / 2, shadowRect.y + deltaY],
[x + deltaX, shadowRect.y],
[x + deltaX, shadowRect.y + deltaY / 2],
[x + deltaX, shadowRect.y + deltaY],
]
for (const p of points) {
let n = this.lookup_node_parent_by_location(p[0], p[1])
if (!!n) {
return n
}
}
}
lookup_node_parent_by_location(x, y) {
return $.d
.elementsFromPoint(x, y)
.filter(
(x) => x.tagName === 'JMNODE' && x.className !== this.options.shadow_node_class_name
)
.map((el) => this.jm.view.get_binded_nodeid(el))
.map((id) => id && this.jm.mind.nodes[id])
.map((n) => n && n.parent)
.find((n) => n)
}
lookup_close_node(direction) {
return Object.values(this.jm.mind.nodes)
.filter((n) => n.direction == direction || n.isroot)
.filter((n) => this.jm.layout.is_visible(n))
.filter((n) => this.shadow_on_target_side(n, direction))
.map((n) => ({ node: n, distance: this.shadow_to_node(n, direction) }))
.reduce(
(prev, curr) => {
return prev.distance < curr.distance ? prev : curr
},
{ node: this.jm.get_root(), distance: Number.MAX_VALUE }
).node
}
shadow_on_target_side(node, dir) {
return (
(dir == jsMind.direction.right && this.shadow_to_right_of_node(node) > 0) ||
(dir == jsMind.direction.left && this.shadow_to_left_of_node(node) > 0)
)
}
shadow_to_right_of_node(node) {
return this.shadow_p_x - node.get_location().x - node.get_size().w
}
shadow_to_left_of_node(node) {
return node.get_location().x - this.shadow_p_x - this.shadow_w
}
shadow_to_base_line_of_node(node) {
return this.shadow_p_y + this.shadow_h / 2 - node.get_location().y - node.get_size().h / 2
}
shadow_to_node(node, dir) {
let distance_x =
dir === jsMind.direction.right
? Math.abs(this.shadow_to_right_of_node(node))
: Math.abs(this.shadow_to_left_of_node(node))
let distance_y = Math.abs(this.shadow_to_base_line_of_node(node))
return distance_x + distance_y
}
calc_point_of_node(node, dir) {
let ns = node.get_size()
let nl = node.get_location()
let node_x = node.isroot
? nl.x + ns.w / 2
: nl.x + (ns.w * (1 + dir)) / 2 + this.options.line_width * dir
let node_y = nl.y + ns.h / 2
let shadow_x =
this.shadow_p_x + (this.shadow_w * (1 - dir)) / 2 - this.options.line_width * dir
let shadow_y = this.shadow_p_y + this.shadow_h / 2
return {
sp: { x: shadow_x, y: shadow_y },
np: { x: node_x, y: node_y },
}
}
move_node(src_node, target_node, target_direct) {
var shadow_h = this.shadow.offsetTop
if (!!target_node && !!src_node && !jsMind.node.inherited(src_node, target_node)) {
// lookup before_node
var sibling_nodes = target_node.children
var sc = sibling_nodes.length
var node = null
var delta_y = Number.MAX_VALUE
var node_before = null
var beforeid = '_last_'
while (sc--) {
node = sibling_nodes[sc]
if (node.direction == target_direct && node.id != src_node.id) {
var dy = node.get_location().y - shadow_h
if (dy > 0 && dy < delta_y) {
delta_y = dy
node_before = node
beforeid = '_first_'
}
}
}
if (!!node_before) {
beforeid = node_before.id
}
this.jm.move_node(src_node.id, beforeid, target_node.id, target_direct)
}
this.active_node = null
this.target_node = null
this.target_direct = null
}
jm_event_handle(type, data) {
if (type === jsMind.event_type.resize) {
this.resize()
}
}
}
var draggable_plugin = new jsMind.plugin('draggable_node', function (jm, options) {
var jd = new DraggableNode(jm, options)
jd.init()
jm.add_event_listener(function (type, data) {
jd.jm_event_handle.call(jd, type, data)
})
})
jsMind.register_plugin(draggable_plugin)
import jsMind from '../jsmind.js'
if (!jsMind) {
throw new Error('jsMind is not defined')
}
const $ = jsMind.$
const clear_selection =
'getSelection' in $.w
? function () {
$.w.getSelection().removeAllRanges()
}
: function () {
$.d.selection.empty()
}
const DEFAULT_OPTIONS = {
line_width: 5,
line_color: 'rgba(0,0,0,0.3)',
line_color_invalid: 'rgba(255,51,51,0.6)',
lookup_delay: 200,
lookup_interval: 100,
scrolling_trigger_width: 20,
scrolling_step_length: 10,
shadow_node_class_name: 'jsmind-draggable-shadow-node',
}
class DraggableNode {
constructor(jm, options) {
var opts = {}
jsMind.util.json.merge(opts, DEFAULT_OPTIONS)
jsMind.util.json.merge(opts, options)
this.version = '0.4.0'
this.jm = jm
this.options = opts
this.e_canvas = null
this.canvas_ctx = null
this.shadow = null
this.shadow_p_x = 0
this.shadow_p_y = 0
this.shadow_w = 0
this.shadow_h = 0
this.active_node = null
this.target_node = null
this.target_direct = null
this.client_w = 0
this.client_h = 0
this.offset_x = 0
this.offset_y = 0
this.hlookup_delay = 0
this.hlookup_timer = 0
this.capture = false
this.moved = false
this.canvas_draggable = jm.get_view_draggable()
this.view_panel = jm.view.e_panel
this.view_panel_rect = null
}
init() {
this.create_canvas()
this.create_shadow()
this.event_bind()
}
resize() {
this.jm.view.e_nodes.appendChild(this.shadow)
this.e_canvas.width = this.jm.view.size.w
this.e_canvas.height = this.jm.view.size.h
}
create_canvas() {
var c = $.c('canvas')
this.jm.view.e_panel.appendChild(c)
var ctx = c.getContext('2d')
this.e_canvas = c
this.canvas_ctx = ctx
}
create_shadow() {
var s = $.c('jmnode')
s.style.visibility = 'hidden'
s.style.zIndex = '3'
s.style.cursor = 'move'
s.style.opacity = '0.7'
s.className = this.options.shadow_node_class_name
this.shadow = s
}
reset_shadow(el) {
var s = this.shadow.style
this.shadow.innerHTML = el.innerHTML
s.left = el.style.left
s.top = el.style.top
s.width = el.style.width
s.height = el.style.height
s.backgroundImage = el.style.backgroundImage
s.backgroundSize = el.style.backgroundSize
s.transform = el.style.transform
this.shadow_w = this.shadow.clientWidth
this.shadow_h = this.shadow.clientHeight
}
show_shadow() {
if (!this.moved) {
this.shadow.style.visibility = 'visible'
}
}
hide_shadow() {
this.shadow.style.visibility = 'hidden'
}
magnet_shadow(shadow_p, node_p, invalid) {
this.canvas_ctx.lineWidth = this.options.line_width
this.canvas_ctx.strokeStyle = invalid
? this.options.line_color_invalid
: this.options.line_color
this.canvas_ctx.lineCap = 'round'
this.clear_lines()
this.canvas_lineto(shadow_p.x, shadow_p.y, node_p.x, node_p.y)
}
clear_lines() {
this.canvas_ctx.clearRect(0, 0, this.jm.view.size.w, this.jm.view.size.h)
}
canvas_lineto(x1, y1, x2, y2) {
this.canvas_ctx.beginPath()
this.canvas_ctx.moveTo(x1, y1)
this.canvas_ctx.lineTo(x2, y2)
this.canvas_ctx.stroke()
}
event_bind() {
var jd = this
var container = this.jm.view.container
$.on(container, 'mousedown', function (e) {
if (e.button === 0) {
jd.dragstart.call(jd, e)
}
})
$.on(container, 'mousemove', function (e) {
if (e.movementX !== 0 || e.movementY !== 0) {
jd.drag.call(jd, e)
}
})
$.on(container, 'mouseup', function (e) {
jd.dragend.call(jd, e)
})
$.on(container, 'touchstart', function (e) {
jd.dragstart.call(jd, e)
})
$.on(container, 'touchmove', function (e) {
jd.drag.call(jd, e)
})
$.on(container, 'touchend', function (e) {
jd.dragend.call(jd, e)
})
}
dragstart(e) {
if (!this.jm.get_editable()) {
return
}
if (this.capture) {
return
}
var jview = this.jm.view
if (jview.is_editing()) {
return
}
this.active_node = null
this.view_draggable = this.jm.get_view_draggable()
var el = this.find_node_element(e.target)
if (!el) {
return
}
if (this.view_draggable) {
this.jm.disable_view_draggable()
}
var nodeid = jview.get_binded_nodeid(el)
if (!!nodeid) {
var node = this.jm.get_node(nodeid)
if (!node.isroot) {
this.reset_shadow(el)
this.view_panel_rect = this.view_panel.getBoundingClientRect()
this.active_node = node
this.offset_x =
(e.clientX || e.touches[0].clientX) / jview.zoom_current - el.offsetLeft
this.offset_y =
(e.clientY || e.touches[0].clientY) / jview.zoom_current - el.offsetTop
this.client_hw = Math.floor(el.clientWidth / 2)
this.client_hh = Math.floor(el.clientHeight / 2)
if (this.hlookup_delay != 0) {
$.w.clearTimeout(this.hlookup_delay)
}
if (this.hlookup_timer != 0) {
$.w.clearInterval(this.hlookup_timer)
}
var jd = this
this.hlookup_delay = $.w.setTimeout(function () {
jd.hlookup_delay = 0
jd.hlookup_timer = $.w.setInterval(function () {
jd.lookup_target_node.call(jd)
}, jd.options.lookup_interval)
}, this.options.lookup_delay)
jd.capture = true
}
}
}
drag(e) {
if (!this.jm.get_editable()) {
return
}
if (this.capture) {
e.preventDefault()
this.show_shadow()
this.moved = true
clear_selection()
var jview = this.jm.view
var px = (e.clientX || e.touches[0].clientX) / jview.zoom_current - this.offset_x
var py = (e.clientY || e.touches[0].clientY) / jview.zoom_current - this.offset_y
// scrolling container axisY if drag nodes exceeding container
if (
e.clientY - this.view_panel_rect.top < this.options.scrolling_trigger_width &&
this.view_panel.scrollTop > this.options.scrolling_step_length
) {
this.view_panel.scrollBy(0, -this.options.scrolling_step_length)
this.offset_y += this.options.scrolling_step_length / jview.zoom_current
} else if (
this.view_panel_rect.bottom - e.clientY < this.options.scrolling_trigger_width &&
this.view_panel.scrollTop <
this.view_panel.scrollHeight -
this.view_panel_rect.height -
this.options.scrolling_step_length
) {
this.view_panel.scrollBy(0, this.options.scrolling_step_length)
this.offset_y -= this.options.scrolling_step_length / jview.zoom_current
}
// scrolling container axisX if drag nodes exceeding container
if (
e.clientX - this.view_panel_rect.left < this.options.scrolling_trigger_width &&
this.view_panel.scrollLeft > this.options.scrolling_step_length
) {
this.view_panel.scrollBy(-this.options.scrolling_step_length, 0)
this.offset_x += this.options.scrolling_step_length / jview.zoom_current
} else if (
this.view_panel_rect.right - e.clientX < this.options.scrolling_trigger_width &&
this.view_panel.scrollLeft <
this.view_panel.scrollWidth -
this.view_panel_rect.width -
this.options.scrolling_step_length
) {
this.view_panel.scrollBy(this.options.scrolling_step_length, 0)
this.offset_x -= this.options.scrolling_step_length / jview.zoom_current
}
this.shadow.style.left = px + 'px'
this.shadow.style.top = py + 'px'
clear_selection()
}
}
dragend(e) {
if (!this.jm.get_editable()) {
return
}
if (this.view_draggable) {
this.jm.enable_view_draggable()
}
if (this.capture) {
if (this.hlookup_delay != 0) {
$.w.clearTimeout(this.hlookup_delay)
this.hlookup_delay = 0
this.clear_lines()
}
if (this.hlookup_timer != 0) {
$.w.clearInterval(this.hlookup_timer)
this.hlookup_timer = 0
this.clear_lines()
}
if (this.moved) {
var src_node = this.active_node
var target_node = this.target_node
var target_direct = this.target_direct
this.move_node(src_node, target_node, target_direct)
}
this.hide_shadow()
}
this.view_panel_rect = null
this.moved = false
this.capture = false
}
find_node_element(el) {
if (
!el ||
el === this.jm.view.e_nodes ||
el === this.jm.view.e_panel ||
el === this.jm.view.container
) {
return null
}
if (el.tagName.toLowerCase() === 'jmnode') {
return el
}
return this.find_node_element(el.parentNode)
}
lookup_target_node() {
let sx = this.shadow.offsetLeft
let sy = this.shadow.offsetTop
if (sx === this.shadow_p_x && sy === this.shadow_p_y) {
return
}
this.shadow_p_x = sx
this.shadow_p_y = sy
let target_direction =
this.shadow_p_x + this.shadow_w / 2 >= this.get_root_x()
? jsMind.direction.right
: jsMind.direction.left
let overlapping_node = this.lookup_overlapping_node_parent(target_direction)
let target_node = overlapping_node || this.lookup_close_node(target_direction)
if (!!target_node) {
let points = this.calc_point_of_node(target_node, target_direction)
let invalid = jsMind.node.inherited(this.active_node, target_node)
this.magnet_shadow(points.sp, points.np, invalid)
this.target_node = target_node
this.target_direct = target_direction
}
}
get_root_x() {
let root = this.jm.get_root()
let root_location = root.get_location()
let root_size = root.get_size()
return root_location.x + root_size.w / 2
}
lookup_overlapping_node_parent(direction) {
let shadowRect = this.shadow.getBoundingClientRect()
let x = shadowRect.x + (shadowRect.width * (1 - direction)) / 2
let deltaX = (this.jm.options.layout.hspace + this.jm.options.layout.pspace) * direction
let deltaY = shadowRect.height
let points = [
[x, shadowRect.y],
[x, shadowRect.y + deltaY / 2],
[x, shadowRect.y + deltaY],
[x + deltaX / 2, shadowRect.y],
[x + deltaX / 2, shadowRect.y + deltaY / 2],
[x + deltaX / 2, shadowRect.y + deltaY],
[x + deltaX, shadowRect.y],
[x + deltaX, shadowRect.y + deltaY / 2],
[x + deltaX, shadowRect.y + deltaY],
]
for (const p of points) {
let n = this.lookup_node_parent_by_location(p[0], p[1])
if (!!n) {
return n
}
}
}
lookup_node_parent_by_location(x, y) {
return $.d
.elementsFromPoint(x, y)
.filter(
(x) => x.tagName === 'JMNODE' && x.className !== this.options.shadow_node_class_name
)
.map((el) => this.jm.view.get_binded_nodeid(el))
.map((id) => id && this.jm.mind.nodes[id])
.map((n) => n && n.parent)
.find((n) => n)
}
lookup_close_node(direction) {
return Object.values(this.jm.mind.nodes)
.filter((n) => n.direction == direction || n.isroot)
.filter((n) => this.jm.layout.is_visible(n))
.filter((n) => this.shadow_on_target_side(n, direction))
.map((n) => ({ node: n, distance: this.shadow_to_node(n, direction) }))
.reduce(
(prev, curr) => {
return prev.distance < curr.distance ? prev : curr
},
{ node: this.jm.get_root(), distance: Number.MAX_VALUE }
).node
}
shadow_on_target_side(node, dir) {
return (
(dir == jsMind.direction.right && this.shadow_to_right_of_node(node) > 0) ||
(dir == jsMind.direction.left && this.shadow_to_left_of_node(node) > 0)
)
}
shadow_to_right_of_node(node) {
return this.shadow_p_x - node.get_location().x - node.get_size().w
}
shadow_to_left_of_node(node) {
return node.get_location().x - this.shadow_p_x - this.shadow_w
}
shadow_to_base_line_of_node(node) {
return this.shadow_p_y + this.shadow_h / 2 - node.get_location().y - node.get_size().h / 2
}
shadow_to_node(node, dir) {
let distance_x =
dir === jsMind.direction.right
? Math.abs(this.shadow_to_right_of_node(node))
: Math.abs(this.shadow_to_left_of_node(node))
let distance_y = Math.abs(this.shadow_to_base_line_of_node(node))
return distance_x + distance_y
}
calc_point_of_node(node, dir) {
let ns = node.get_size()
let nl = node.get_location()
let node_x = node.isroot
? nl.x + ns.w / 2
: nl.x + (ns.w * (1 + dir)) / 2 + this.options.line_width * dir
let node_y = nl.y + ns.h / 2
let shadow_x =
this.shadow_p_x + (this.shadow_w * (1 - dir)) / 2 - this.options.line_width * dir
let shadow_y = this.shadow_p_y + this.shadow_h / 2
return {
sp: { x: shadow_x, y: shadow_y },
np: { x: node_x, y: node_y },
}
}
move_node(src_node, target_node, target_direct) {
var shadow_h = this.shadow.offsetTop
if (!!target_node && !!src_node && !jsMind.node.inherited(src_node, target_node)) {
// lookup before_node
var sibling_nodes = target_node.children
var sc = sibling_nodes.length
var node = null
var delta_y = Number.MAX_VALUE
var node_before = null
var beforeid = '_last_'
while (sc--) {
node = sibling_nodes[sc]
if (node.direction == target_direct && node.id != src_node.id) {
var dy = node.get_location().y - shadow_h
if (dy > 0 && dy < delta_y) {
delta_y = dy
node_before = node
beforeid = '_first_'
}
}
}
if (!!node_before) {
beforeid = node_before.id
}
this.jm.move_node(src_node.id, beforeid, target_node.id, target_direct)
}
this.active_node = null
this.target_node = null
this.target_direct = null
}
jm_event_handle(type, data) {
if (type === jsMind.event_type.resize) {
this.resize()
}
}
}
var draggable_plugin = new jsMind.plugin('draggable_node', function (jm, options) {
var jd = new DraggableNode(jm, options)
jd.init()
jm.add_event_listener(function (type, data) {
jd.jm_event_handle.call(jd, type, data)
})
})
jsMind.register_plugin(draggable_plugin)

View File

@ -1,158 +1,158 @@
import jsMind from 'jsmind'
import domtoimage from 'dom-to-image'
if (!jsMind) {
throw new Error('jsMind is not defined')
}
if (!domtoimage) {
throw new Error('dom-to-image is required')
}
const $ = jsMind.$
const DEFAULT_OPTIONS = {
filename: null,
watermark: {
left: $.w.location,
right: 'https://github.com/hizzgdev/jsmind',
},
background: 'transparent',
}
class JmScreenshot {
constructor(jm, options) {
var opts = {}
jsMind.util.json.merge(opts, DEFAULT_OPTIONS)
jsMind.util.json.merge(opts, options)
this.version = '0.2.0'
this.jm = jm
this.options = opts
this.dpr = jm.view.device_pixel_ratio
}
shoot() {
let c = this.create_canvas()
let ctx = c.getContext('2d')
ctx.scale(this.dpr, this.dpr)
Promise.resolve(ctx)
.then(() => this.draw_background(ctx))
.then(() => this.draw_lines(ctx))
.then(() => this.draw_nodes(ctx))
.then(() => this.draw_watermark(c, ctx))
.then(() => this.download(c))
.then(() => this.clear(c))
}
create_canvas() {
let c = $.c('canvas')
const w = this.jm.view.size.w
const h = this.jm.view.size.h
c.width = w * this.dpr
c.height = h * this.dpr
c.style.width = w + 'px'
c.style.height = h + 'px'
c.style.visibility = 'hidden'
this.jm.view.e_panel.appendChild(c)
return c
}
clear(c) {
c.parentNode.removeChild(c)
}
draw_background(ctx) {
return new Promise(
function (resolve, _) {
const bg = this.options.background
if (!!bg && bg !== 'transparent') {
ctx.fillStyle = this.options.background
ctx.fillRect(0, 0, this.jm.view.size.w, this.jm.view.size.h)
}
resolve(ctx)
}.bind(this)
)
}
draw_lines(ctx) {
return new Promise(
function (resolve, _) {
this.jm.view.graph.copy_to(ctx, function () {
resolve(ctx)
})
}.bind(this)
)
}
draw_nodes(ctx) {
return domtoimage
.toSvg(this.jm.view.e_nodes, { style: { zoom: 1 } })
.then(this.load_image)
.then(function (img) {
ctx.drawImage(img, 0, 0)
return ctx
})
}
draw_watermark(c, ctx) {
ctx.textBaseline = 'bottom'
ctx.fillStyle = '#000'
ctx.font = '11px Verdana,Arial,Helvetica,sans-serif'
if (!!this.options.watermark.left) {
ctx.textAlign = 'left'
ctx.fillText(this.options.watermark.left, 5.5, c.height - 2.5)
}
if (!!this.options.watermark.right) {
ctx.textAlign = 'right'
ctx.fillText(this.options.watermark.right, c.width - 5.5, c.height - 2.5)
}
return ctx
}
load_image(url) {
return new Promise(function (resolve, reject) {
let img = new Image()
img.onload = function () {
resolve(img)
}
img.onerror = reject
img.src = url
})
}
download(c) {
var name = (this.options.filename || this.jm.mind.name) + '.png'
if (navigator.msSaveBlob && !!c.msToBlob) {
var blob = c.msToBlob()
navigator.msSaveBlob(blob, name)
} else {
var blob_url = c.toDataURL()
var anchor = $.c('a')
if ('download' in anchor) {
anchor.style.visibility = 'hidden'
anchor.href = blob_url
anchor.download = name
$.d.body.appendChild(anchor)
var evt = $.d.createEvent('MouseEvents')
evt.initEvent('click', true, true)
anchor.dispatchEvent(evt)
$.d.body.removeChild(anchor)
} else {
location.href = blob_url
}
}
}
}
let screenshot_plugin = new jsMind.plugin('screenshot', function (jm, options) {
var jmss = new JmScreenshot(jm, options)
jm.screenshot = jmss
jm.shoot = function () {
jmss.shoot()
}
})
jsMind.register_plugin(screenshot_plugin)
import jsMind from 'jsmind'
import domtoimage from 'dom-to-image'
if (!jsMind) {
throw new Error('jsMind is not defined')
}
if (!domtoimage) {
throw new Error('dom-to-image is required')
}
const $ = jsMind.$
const DEFAULT_OPTIONS = {
filename: null,
watermark: {
left: $.w.location,
right: 'https://github.com/hizzgdev/jsmind',
},
background: 'transparent',
}
class JmScreenshot {
constructor(jm, options) {
var opts = {}
jsMind.util.json.merge(opts, DEFAULT_OPTIONS)
jsMind.util.json.merge(opts, options)
this.version = '0.2.0'
this.jm = jm
this.options = opts
this.dpr = jm.view.device_pixel_ratio
}
shoot() {
let c = this.create_canvas()
let ctx = c.getContext('2d')
ctx.scale(this.dpr, this.dpr)
Promise.resolve(ctx)
.then(() => this.draw_background(ctx))
.then(() => this.draw_lines(ctx))
.then(() => this.draw_nodes(ctx))
.then(() => this.draw_watermark(c, ctx))
.then(() => this.download(c))
.then(() => this.clear(c))
}
create_canvas() {
let c = $.c('canvas')
const w = this.jm.view.size.w
const h = this.jm.view.size.h
c.width = w * this.dpr
c.height = h * this.dpr
c.style.width = w + 'px'
c.style.height = h + 'px'
c.style.visibility = 'hidden'
this.jm.view.e_panel.appendChild(c)
return c
}
clear(c) {
c.parentNode.removeChild(c)
}
draw_background(ctx) {
return new Promise(
function (resolve, _) {
const bg = this.options.background
if (!!bg && bg !== 'transparent') {
ctx.fillStyle = this.options.background
ctx.fillRect(0, 0, this.jm.view.size.w, this.jm.view.size.h)
}
resolve(ctx)
}.bind(this)
)
}
draw_lines(ctx) {
return new Promise(
function (resolve, _) {
this.jm.view.graph.copy_to(ctx, function () {
resolve(ctx)
})
}.bind(this)
)
}
draw_nodes(ctx) {
return domtoimage
.toSvg(this.jm.view.e_nodes, { style: { zoom: 1 } })
.then(this.load_image)
.then(function (img) {
ctx.drawImage(img, 0, 0)
return ctx
})
}
draw_watermark(c, ctx) {
ctx.textBaseline = 'bottom'
ctx.fillStyle = '#000'
ctx.font = '11px Verdana,Arial,Helvetica,sans-serif'
if (!!this.options.watermark.left) {
ctx.textAlign = 'left'
ctx.fillText(this.options.watermark.left, 5.5, c.height - 2.5)
}
if (!!this.options.watermark.right) {
ctx.textAlign = 'right'
ctx.fillText(this.options.watermark.right, c.width - 5.5, c.height - 2.5)
}
return ctx
}
load_image(url) {
return new Promise(function (resolve, reject) {
let img = new Image()
img.onload = function () {
resolve(img)
}
img.onerror = reject
img.src = url
})
}
download(c) {
var name = (this.options.filename || this.jm.mind.name) + '.png'
if (navigator.msSaveBlob && !!c.msToBlob) {
var blob = c.msToBlob()
navigator.msSaveBlob(blob, name)
} else {
var blob_url = c.toDataURL()
var anchor = $.c('a')
if ('download' in anchor) {
anchor.style.visibility = 'hidden'
anchor.href = blob_url
anchor.download = name
$.d.body.appendChild(anchor)
var evt = $.d.createEvent('MouseEvents')
evt.initEvent('click', true, true)
anchor.dispatchEvent(evt)
$.d.body.removeChild(anchor)
} else {
location.href = blob_url
}
}
}
}
let screenshot_plugin = new jsMind.plugin('screenshot', function (jm, options) {
var jmss = new JmScreenshot(jm, options)
jm.screenshot = jmss
jm.shoot = function () {
jmss.shoot()
}
})
jsMind.register_plugin(screenshot_plugin)

206
app/assets/javascripts/mind_map/utils/custom.config.js Normal file → Executable file
View File

@ -1,103 +1,103 @@
// 顏色選項集中管理
// Color options management
export const PALETTE_COLORS = [
'#c93a42',
'#fbefdb',
'#0000FF',
'#06c755',
'#FFBF48',
'#1e5b9e',
'#000000',
'#666666',
'#999999',
'#FFFFFF',
// 紅色系Morandi Red
'#c8a39e',
'#b4746c',
'#a16666',
'#d3a29d',
'#8e5d5a',
// 橘色系Morandi Orange
'#d4a186',
'#e1b7a7',
'#c98e63',
'#e5b58e',
'#b57758',
// 黃色系Morandi Yellow
'#d8c29d',
'#e6d3aa',
'#c4b07c',
'#e2c892',
'#a8985c',
// 綠色系Morandi Green
'#a3b1a8',
'#8ca39b',
'#9fb7ad',
'#b0c0ae',
'#798d87',
// 藍色系Morandi Blue
'#9ca8b8',
'#a0b1c2',
'#8193a8',
'#6e7d91',
'#c0c8d2',
'#b8a9c9',
'#c1adc8',
'#a68ca9',
'#cabed4',
'#8f799e',
'#a0a0a0',
'#bcbcbc',
'#8c8c8c',
'#747474',
'#5e5e5e',
]
// 心智圖初始數據
export const INITIAL_MIND = {
meta: {},
format: 'node_array',
data: [
{
id: 'root',
topic: 'FirstNode',
expanded: true,
isroot: true,
},
],
}
// 模擬打 API
// Simulate an API call to search based on the query
export async function mockSearchApi(query, tableUID) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", '/admin/universal_tables/get_entries?uid=' + tableUID + "&q=" + query + "&links=true");
xhr.setRequestHeader("Accept", "application/json");
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText);
resolve(data); // success
} catch (e) {
reject(new Error("Invalid JSON response"));
}
} else {
reject(new Error(`Request failed with status ${xhr.status}`));
}
};
xhr.onerror = function () {
reject(new Error("Network error"));
};
xhr.send();
});
}
// 顏色選項集中管理
// Color options management
export const PALETTE_COLORS = [
'#c93a42',
'#fbefdb',
'#0000FF',
'#06c755',
'#FFBF48',
'#1e5b9e',
'#000000',
'#666666',
'#999999',
'#FFFFFF',
// 紅色系Morandi Red
'#c8a39e',
'#b4746c',
'#a16666',
'#d3a29d',
'#8e5d5a',
// 橘色系Morandi Orange
'#d4a186',
'#e1b7a7',
'#c98e63',
'#e5b58e',
'#b57758',
// 黃色系Morandi Yellow
'#d8c29d',
'#e6d3aa',
'#c4b07c',
'#e2c892',
'#a8985c',
// 綠色系Morandi Green
'#a3b1a8',
'#8ca39b',
'#9fb7ad',
'#b0c0ae',
'#798d87',
// 藍色系Morandi Blue
'#9ca8b8',
'#a0b1c2',
'#8193a8',
'#6e7d91',
'#c0c8d2',
'#b8a9c9',
'#c1adc8',
'#a68ca9',
'#cabed4',
'#8f799e',
'#a0a0a0',
'#bcbcbc',
'#8c8c8c',
'#747474',
'#5e5e5e',
]
// 心智圖初始數據
export const INITIAL_MIND = {
meta: {},
format: 'node_array',
data: [
{
id: 'root',
topic: 'FirstNode',
expanded: true,
isroot: true,
},
],
}
// 模擬打 API
// Simulate an API call to search based on the query
export async function mockSearchApi(query, tableUID) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", '/admin/universal_tables/get_entries?uid=' + tableUID + "&q=" + query + "&links=true");
xhr.setRequestHeader("Accept", "application/json");
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText);
resolve(data); // success
} catch (e) {
reject(new Error("Invalid JSON response"));
}
} else {
reject(new Error(`Request failed with status ${xhr.status}`));
}
};
xhr.onerror = function () {
reject(new Error("Network error"));
};
xhr.send();
});
}

102
app/assets/javascripts/mind_map/utils/custom.main.js Normal file → Executable file
View File

@ -1,51 +1,51 @@
import jsMind from '../jsmind/jsmind.js'
import '../jsmind/plugins/jsmind.draggable-node.js'
import { JsmindSearch } from './custom.search.js'
import { JsmindToolbar } from './custom.toolbar.js'
import { mockSearchApi } from './custom.config.js'
/**
* 初始化 jsMind 心智圖
* Initialize jsMind mind map
* @param {Object} mind - 心智圖的資料 (Mind map data)
* @param {Object} options - 配置選項 (Configuration options)
* @param {boolean} isEditable - 是否可編輯 (Is editable)
* @returns {Object} - jsMind 實例 (jsMind instance)
*/
export function initJsmind(mind, options, isEditable) {
const container = document.getElementById(options.container)
container.innerHTML = ''
options.editable = isEditable
const jm = new jsMind(options)
// 依據是否可編輯調整顯示為連結或文字
// Adjust display as a link or text based on editability
const formattedData = mind.data.map((node) => {
node.topic =
!isEditable && node.link
? `<a href="${node.link}" target="_blank">${node.text}</a>`
: node.text || node.topic
return node
})
jm.show({ meta: mind.meta, format: 'node_array', data: formattedData })
// 掛載附加模組(遠程搜尋 & 工具列)
// Attach additional modules (Remote search & Toolbar)
if (isEditable) {
new JsmindSearch(jm, mockSearchApi, options.tableUID);
new JsmindToolbar(jm, options)
}
return jm
}
/**
* 獲取當前心智圖數據
* Get the current mind map data
* @param {Object} jm - jsMind 實例 (jsMind instance)
* @returns {Object} - 心智圖數據 (Mind map data)
*/
export function getJsmindData(jm) {
return jm.get_data('node_array')
}
import jsMind from '../jsmind/jsmind.js'
import '../jsmind/plugins/jsmind.draggable-node.js'
import { JsmindSearch } from './custom.search.js'
import { JsmindToolbar } from './custom.toolbar.js'
import { mockSearchApi } from './custom.config.js'
/**
* 初始化 jsMind 心智圖
* Initialize jsMind mind map
* @param {Object} mind - 心智圖的資料 (Mind map data)
* @param {Object} options - 配置選項 (Configuration options)
* @param {boolean} isEditable - 是否可編輯 (Is editable)
* @returns {Object} - jsMind 實例 (jsMind instance)
*/
export function initJsmind(mind, options, isEditable) {
const container = document.getElementById(options.container)
container.innerHTML = ''
options.editable = isEditable
const jm = new jsMind(options)
// 依據是否可編輯調整顯示為連結或文字
// Adjust display as a link or text based on editability
const formattedData = mind.data.map((node) => {
node.topic =
!isEditable && node.link
? `<a href="${node.link}" target="_blank">${node.text}</a>`
: node.text || node.topic
return node
})
jm.show({ meta: mind.meta, format: 'node_array', data: formattedData })
// 掛載附加模組(遠程搜尋 & 工具列)
// Attach additional modules (Remote search & Toolbar)
if (isEditable) {
new JsmindSearch(jm, mockSearchApi, options.tableUID);
new JsmindToolbar(jm, options)
}
return jm
}
/**
* 獲取當前心智圖數據
* Get the current mind map data
* @param {Object} jm - jsMind 實例 (jsMind instance)
* @returns {Object} - 心智圖數據 (Mind map data)
*/
export function getJsmindData(jm) {
return jm.get_data('node_array')
}

148
app/assets/javascripts/mind_map/utils/custom.overrides.js Normal file → Executable file
View File

@ -1,74 +1,74 @@
import { util } from '../jsmind/jsmind.util.js'
import { Mind } from '../jsmind/jsmind.mind.js'
import { ViewProvider } from '../jsmind/jsmind.view_provider.js'
import jsMind from '../jsmind/jsmind.js'
ViewProvider.prototype.edit_node_end = function () {
if (this.editing_node != null) {
var node = this.editing_node
this.editing_node = null
var view_data = node._data.view
var element = view_data.element
// 客製化修改:顯示文字由 node.data 控制
// Customization: Display text is controlled by node.data
var topic = node.data.text
element.style.zIndex = 'auto'
element.removeChild(this.e_editor)
if (util.text.is_empty(topic) || node.topic === topic) {
this.render_node(element, node)
} else {
this.jm.update_node(node.id, topic)
}
}
this.e_panel.focus()
}
ViewProvider.prototype.select_node = function (node) {
if (!!this.selected_node) {
var element = this.selected_node._data.view.element
element.className = element.className.replace(/\s*selected\b/i, '')
this.restore_selected_node_custom_style(this.selected_node)
}
if (!!node) {
this.selected_node = node
node._data.view.element.className += ' selected'
// 客製化修改:不清除自定義樣式
// Customization: Do not clear custom styles
// this.clear_selected_node_custom_style(node)
}
}
const originalAddNode = Mind.prototype.add_node
Mind.prototype.add_node = function (
parent_node,
node_id,
topic,
data = undefined,
direction,
expanded,
idx
) {
if (data == undefined) {
data = {}
for (let style of [
'leading-line-color',
'background-color',
'foreground-color',
]) {
if (data[style] == undefined && parent_node.data?.[style]) {
data[style] = parent_node.data[style]
}
}
}
arguments[3] = data
return originalAddNode.apply(this, arguments)
}
const originalMousedownHandle = jsMind.prototype.mousedown_handle
jsMind.prototype.mousedown_handle = function (e) {
if (e.button !== 0) return
return originalMousedownHandle.apply(this, arguments)
}
import { util } from '../jsmind/jsmind.util.js'
import { Mind } from '../jsmind/jsmind.mind.js'
import { ViewProvider } from '../jsmind/jsmind.view_provider.js'
import jsMind from '../jsmind/jsmind.js'
ViewProvider.prototype.edit_node_end = function () {
if (this.editing_node != null) {
var node = this.editing_node
this.editing_node = null
var view_data = node._data.view
var element = view_data.element
// 客製化修改:顯示文字由 node.data 控制
// Customization: Display text is controlled by node.data
var topic = node.data.text
element.style.zIndex = 'auto'
element.removeChild(this.e_editor)
if (util.text.is_empty(topic) || node.topic === topic) {
this.render_node(element, node)
} else {
this.jm.update_node(node.id, topic)
}
}
this.e_panel.focus()
}
ViewProvider.prototype.select_node = function (node) {
if (!!this.selected_node) {
var element = this.selected_node._data.view.element
element.className = element.className.replace(/\s*selected\b/i, '')
this.restore_selected_node_custom_style(this.selected_node)
}
if (!!node) {
this.selected_node = node
node._data.view.element.className += ' selected'
// 客製化修改:不清除自定義樣式
// Customization: Do not clear custom styles
// this.clear_selected_node_custom_style(node)
}
}
const originalAddNode = Mind.prototype.add_node
Mind.prototype.add_node = function (
parent_node,
node_id,
topic,
data = undefined,
direction,
expanded,
idx
) {
if (data == undefined) {
data = {}
for (let style of [
'leading-line-color',
'background-color',
'foreground-color',
]) {
if (data[style] == undefined && parent_node.data?.[style]) {
data[style] = parent_node.data[style]
}
}
}
arguments[3] = data
return originalAddNode.apply(this, arguments)
}
const originalMousedownHandle = jsMind.prototype.mousedown_handle
jsMind.prototype.mousedown_handle = function (e) {
if (e.button !== 0) return
return originalMousedownHandle.apply(this, arguments)
}

532
app/assets/javascripts/mind_map/utils/custom.search.js Normal file → Executable file
View File

@ -1,267 +1,267 @@
import { getRelativePosition } from './custom.util.js'
const EDITOR_CLASS = 'jsmind-editor' // jsmind class name
const SUGGESTION_BOX_CLASS = 'jsmind-suggestions'
const SUGGESTION_ITEM_CLASS = 'suggestion-item'
/**
* jsMind 搜尋管理
* jsMind Search Manager
*/
export class JsmindSearch {
/**
* 建構搜尋
* Constructor for search
* @param {Object} jm - jsMind 實例 (jsMind instance)
* @param {Function} searchAPI - 遠程搜尋 API 函式 (Remote search API function)
* @param {string} tableUID
*/
constructor(jm, searchAPI, tableUID) {
this.jm = jm
this.searchAPI = searchAPI
this.container = document.getElementById(jm.options.container)
this.suggestionBox = null
this.tableUID = tableUID
// 新增記錄節點與事件 handler
this.currentNode = null
this._keydownHandler = null
this._inputHandler = null
this.init()
}
/**
* 初始化搜尋事件
* Initialize search events
*/
init() {
// 確保不會重複綁定 dblclick 事件
// Ensure double-click event is not bound multiple times
this.container.removeEventListener('dblclick', this.onDoubleClick)
this.container.addEventListener('dblclick', this.onDoubleClick.bind(this))
}
/**
* 處理雙擊事件以觸發搜尋
* Handle double-click event to trigger search
* @param {Event} e - 事件對象 (Event object)
*/
onDoubleClick(e) {
// 非可編輯狀態不執行
// Ignore if not editable
if (!this.jm.options.editable) return
const node = this.jm.get_selected_node()
if (!node) return
// 避免影響原生編輯功能,稍後執行
// Prevent interfering with native edit mode
setTimeout(() => this.handleSearch(node), 100)
}
/**
* 開始處理搜尋
* Start handling search
* @param {Object} node - 當前選中節點 (Selected node)
*/
handleSearch(node) {
const inputField = document.querySelector(`.${EDITOR_CLASS}`)
if (!inputField) return
// 記住目前的 node
this.currentNode = node
// 清除之前的 handler
if (this._keydownHandler) inputField.removeEventListener('keydown', this._keydownHandler)
if (this._inputHandler) inputField.removeEventListener('input', this._inputHandler)
// 新綁定 handler
this._keydownHandler = this.onKeyDown.bind(this)
this._inputHandler = this.onInput.bind(this)
inputField.addEventListener('keydown', this._keydownHandler)
inputField.addEventListener('input', this._inputHandler)
}
/**
* 處理 Enter 鍵完成輸入
* Handle Enter key to finalize input
* @param {Object} node - 當前節點
* @param {KeyboardEvent} e - 鍵盤事件
*/
onKeyDown(e) {
if (e.key === 'Enter') {
e.preventDefault()
const input = e.target.value.trim()
const node = this.currentNode
if (input && node) {
node.data.text = input
this.jm.end_edit()
this.jm.update_node(node.id, input)
if (this.suggestionBox) {
this.suggestionBox.style.display = 'none'
}
this.currentNode = null // 清除參考
}
}
}
/**
* 處理使用者輸入
* Handle user input
* @param {Object} node - 當前選中節點 (Selected node)
* @param {Event} e - 輸入事件 (Input event)
*/
async onInput(e) {
const query = e.target.value.trim()
if (!query) return
await new Promise((resolve) => setTimeout(resolve, 500))
try {
const results = await this.searchAPI(query, this.tableUID)
this.showSuggestion(this.currentNode, e.target, results)
} catch (error) {
console.error('搜尋 API 錯誤:', error)
}
}
/**
* 顯示搜尋建議框
* Show search suggestion box
* @param {Object} node - 當前選中節點 (Selected node)
* @param {HTMLElement} inputElement - 輸入框 (Input field)
* @param {Array} results - 搜尋結果 (Search results)
*/
showSuggestion(node, inputElement, results) {
const container = this.container
const nodeElement = inputElement.parentNode
if (!nodeElement) return
const { left, top, height } = getRelativePosition(nodeElement, container)
this.suggestionBox = this.suggestionBox || this.createSuggestionBox()
// 更新建議框內容
// Update suggestion box content
this.suggestionBox.innerHTML = results
.map(item => {
const fieldHtml = item.fields.map(f => {
const txt = f.url
? `<a href="${f.url}" target="_blank">${f.text}</a>`
: f.text;
return `<div class="field-row"><strong>${f.title}:</strong> ${txt}</div>`;
}).join("");
return `
<div class="suggestion-item" data-link="${item.link}">
${fieldHtml}
</div>
`;
})
.join('')
this.suggestionBox.style.left = `${left}px`
this.suggestionBox.style.top = `${top + height}px`
this.suggestionBox.style.display = 'block'
// 綁定建議點擊事件
// Bind suggestion click events
document.querySelectorAll(`.${SUGGESTION_ITEM_CLASS}`).forEach((item) => {
item.removeEventListener('mousedown', this.onSuggestionClick)
item.addEventListener('mousedown', this.onSuggestionClick.bind(this, node))
})
}
/**
* 建立搜尋建議框
* Create search suggestion box
* @returns {HTMLElement} - 建議框 DOM (Suggestion box DOM)
*/
createSuggestionBox() {
let suggestionBox = document.getElementById(SUGGESTION_BOX_CLASS)
if (!suggestionBox) {
suggestionBox = document.createElement('div')
suggestionBox.classList.add(SUGGESTION_BOX_CLASS)
this.container.appendChild(suggestionBox)
}
return suggestionBox
}
/**
* 處理點擊建議
* Handle suggestion click
* @param {Object} node - 當前選中節點 (Selected node)
* @param {Event} e - 點擊事件 (Click event)
*/
// onSuggestionClick(node, e) {
// e.preventDefault()
// const text = e.target.getAttribute('data-text')
// const link = e.target.getAttribute('data-link')
// node.data.text = text
// node.data.link = link
// this.jm.end_edit()
// this.jm.update_node(node.id, text)
// // 選擇後隱藏建議框
// // Hide suggestions after selection
// this.suggestionBox.style.display = 'none'
// }
onSuggestionClick(node, e) {
e.preventDefault()
const item = e.currentTarget // 確保抓到整個 .suggestion-item DIV
const html = item.innerHTML // 取得完整 HTML 當作 topic
node.data.text = html
node.data.link = item.getAttribute('data-link')
this.jm.end_edit()
this.jm.update_node(node.id, html)
this.suggestionBox.style.display = 'none'
}
}
// ✅ 新增播放語音事件委派,支援動態插入的 voice-player
let audio;
document.addEventListener('click', function(e) {
const target = e.target.closest('.voice-player');
if (!target) return;
e.preventDefault();
let status = target.getAttribute('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status === 'playing') {
target.setAttribute('status', '');
const icon = target.querySelector('i');
icon?.classList.remove('fa-pause');
icon?.classList.add('fa-play');
} else {
let mp3_url = target.getAttribute('data-content');
audio = new Audio(mp3_url);
audio.play();
target.setAttribute('status', 'playing');
const icon = target.querySelector('i');
icon?.classList.remove('fa-play');
icon?.classList.add('fa-pause');
audio.onended = function() {
target.setAttribute('status', '');
icon?.classList.remove('fa-pause');
icon?.classList.add('fa-play');
};
}
import { getRelativePosition } from './custom.util.js'
const EDITOR_CLASS = 'jsmind-editor' // jsmind class name
const SUGGESTION_BOX_CLASS = 'jsmind-suggestions'
const SUGGESTION_ITEM_CLASS = 'suggestion-item'
/**
* jsMind 搜尋管理
* jsMind Search Manager
*/
export class JsmindSearch {
/**
* 建構搜尋
* Constructor for search
* @param {Object} jm - jsMind 實例 (jsMind instance)
* @param {Function} searchAPI - 遠程搜尋 API 函式 (Remote search API function)
* @param {string} tableUID
*/
constructor(jm, searchAPI, tableUID) {
this.jm = jm
this.searchAPI = searchAPI
this.container = document.getElementById(jm.options.container)
this.suggestionBox = null
this.tableUID = tableUID
// 新增記錄節點與事件 handler
this.currentNode = null
this._keydownHandler = null
this._inputHandler = null
this.init()
}
/**
* 初始化搜尋事件
* Initialize search events
*/
init() {
// 確保不會重複綁定 dblclick 事件
// Ensure double-click event is not bound multiple times
this.container.removeEventListener('dblclick', this.onDoubleClick)
this.container.addEventListener('dblclick', this.onDoubleClick.bind(this))
}
/**
* 處理雙擊事件以觸發搜尋
* Handle double-click event to trigger search
* @param {Event} e - 事件對象 (Event object)
*/
onDoubleClick(e) {
// 非可編輯狀態不執行
// Ignore if not editable
if (!this.jm.options.editable) return
const node = this.jm.get_selected_node()
if (!node) return
// 避免影響原生編輯功能,稍後執行
// Prevent interfering with native edit mode
setTimeout(() => this.handleSearch(node), 100)
}
/**
* 開始處理搜尋
* Start handling search
* @param {Object} node - 當前選中節點 (Selected node)
*/
handleSearch(node) {
const inputField = document.querySelector(`.${EDITOR_CLASS}`)
if (!inputField) return
// 記住目前的 node
this.currentNode = node
// 清除之前的 handler
if (this._keydownHandler) inputField.removeEventListener('keydown', this._keydownHandler)
if (this._inputHandler) inputField.removeEventListener('input', this._inputHandler)
// 新綁定 handler
this._keydownHandler = this.onKeyDown.bind(this)
this._inputHandler = this.onInput.bind(this)
inputField.addEventListener('keydown', this._keydownHandler)
inputField.addEventListener('input', this._inputHandler)
}
/**
* 處理 Enter 鍵完成輸入
* Handle Enter key to finalize input
* @param {Object} node - 當前節點
* @param {KeyboardEvent} e - 鍵盤事件
*/
onKeyDown(e) {
if (e.key === 'Enter') {
e.preventDefault()
const input = e.target.value.trim()
const node = this.currentNode
if (input && node) {
node.data.text = input
this.jm.end_edit()
this.jm.update_node(node.id, input)
if (this.suggestionBox) {
this.suggestionBox.style.display = 'none'
}
this.currentNode = null // 清除參考
}
}
}
/**
* 處理使用者輸入
* Handle user input
* @param {Object} node - 當前選中節點 (Selected node)
* @param {Event} e - 輸入事件 (Input event)
*/
async onInput(e) {
const query = e.target.value.trim()
if (!query) return
await new Promise((resolve) => setTimeout(resolve, 500))
try {
const results = await this.searchAPI(query, this.tableUID)
this.showSuggestion(this.currentNode, e.target, results)
} catch (error) {
console.error('搜尋 API 錯誤:', error)
}
}
/**
* 顯示搜尋建議框
* Show search suggestion box
* @param {Object} node - 當前選中節點 (Selected node)
* @param {HTMLElement} inputElement - 輸入框 (Input field)
* @param {Array} results - 搜尋結果 (Search results)
*/
showSuggestion(node, inputElement, results) {
const container = this.container
const nodeElement = inputElement.parentNode
if (!nodeElement) return
const { left, top, height } = getRelativePosition(nodeElement, container)
this.suggestionBox = this.suggestionBox || this.createSuggestionBox()
// 更新建議框內容
// Update suggestion box content
this.suggestionBox.innerHTML = results
.map(item => {
const fieldHtml = item.fields.map(f => {
const txt = f.url
? `<a href="${f.url}" target="_blank">${f.text}</a>`
: f.text;
return `<div class="field-row"><strong>${f.title}:</strong> ${txt}</div>`;
}).join("");
return `
<div class="suggestion-item" data-link="${item.link}">
${fieldHtml}
</div>
`;
})
.join('')
this.suggestionBox.style.left = `${left}px`
this.suggestionBox.style.top = `${top + height}px`
this.suggestionBox.style.display = 'block'
// 綁定建議點擊事件
// Bind suggestion click events
document.querySelectorAll(`.${SUGGESTION_ITEM_CLASS}`).forEach((item) => {
item.removeEventListener('mousedown', this.onSuggestionClick)
item.addEventListener('mousedown', this.onSuggestionClick.bind(this, node))
})
}
/**
* 建立搜尋建議框
* Create search suggestion box
* @returns {HTMLElement} - 建議框 DOM (Suggestion box DOM)
*/
createSuggestionBox() {
let suggestionBox = document.getElementById(SUGGESTION_BOX_CLASS)
if (!suggestionBox) {
suggestionBox = document.createElement('div')
suggestionBox.classList.add(SUGGESTION_BOX_CLASS)
this.container.appendChild(suggestionBox)
}
return suggestionBox
}
/**
* 處理點擊建議
* Handle suggestion click
* @param {Object} node - 當前選中節點 (Selected node)
* @param {Event} e - 點擊事件 (Click event)
*/
// onSuggestionClick(node, e) {
// e.preventDefault()
// const text = e.target.getAttribute('data-text')
// const link = e.target.getAttribute('data-link')
// node.data.text = text
// node.data.link = link
// this.jm.end_edit()
// this.jm.update_node(node.id, text)
// // 選擇後隱藏建議框
// // Hide suggestions after selection
// this.suggestionBox.style.display = 'none'
// }
onSuggestionClick(node, e) {
e.preventDefault()
const item = e.currentTarget // 確保抓到整個 .suggestion-item DIV
const html = item.innerHTML // 取得完整 HTML 當作 topic
node.data.text = html
node.data.link = item.getAttribute('data-link')
this.jm.end_edit()
this.jm.update_node(node.id, html)
this.suggestionBox.style.display = 'none'
}
}
// ✅ 新增播放語音事件委派,支援動態插入的 voice-player
let audio;
document.addEventListener('click', function(e) {
const target = e.target.closest('.voice-player');
if (!target) return;
e.preventDefault();
let status = target.getAttribute('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status === 'playing') {
target.setAttribute('status', '');
const icon = target.querySelector('i');
icon?.classList.remove('fa-pause');
icon?.classList.add('fa-play');
} else {
let mp3_url = target.getAttribute('data-content');
audio = new Audio(mp3_url);
audio.play();
target.setAttribute('status', 'playing');
const icon = target.querySelector('i');
icon?.classList.remove('fa-play');
icon?.classList.add('fa-pause');
audio.onended = function() {
target.setAttribute('status', '');
icon?.classList.remove('fa-pause');
icon?.classList.add('fa-play');
};
}
});

532
app/assets/javascripts/mind_map/utils/custom.toolbar.js Normal file → Executable file
View File

@ -1,266 +1,266 @@
import { util } from '../jsmind/jsmind.util.js'
import { getRelativePosition } from './custom.util.js'
import { PALETTE_COLORS } from './custom.config.js'
const TOOLBAR_ID = 'jsmind-toolbar'
/**
* jsMind 工具列管理
* jsMind Toolbar Manager
*/
export class JsmindToolbar {
/**
* 建構工具列
* Constructor for toolbar
* @param {Object} jm - jsMind 實例 (jsMind instance)
* @param {Object} options - jsMind 實例 (options)
*/
constructor(jm, options) {
this.jm = jm
this.container = document.getElementById(jm.options.container)
this.toolbarNodeId = null
this.toolbar = null
this.bgColorPalette = null
this.strokeColorPalette = null
this.textColorPalette = null
this.options = options
this.init()
}
/**
* 初始化工具列事件
* Initialize toolbar events
*/
init() {
// 監聽節點選取事件
// Listen for node selection events
this.jm.add_event_listener((e, f, g) => {
// 忽略非選擇節點事件
// Ignore non-selection events
if (e !== 4) return
const node = this.jm.get_selected_node()
if (!node || node.id === this.toolbarNodeId) return
this.toolbarNodeId = node.id
if (!this.toolbar) {
this.createToolbar()
}
this.moveToolbar(node)
})
// 確保不會重複綁定點擊事件
// Ensure click event is not bound multiple times
this.container.removeEventListener('click', this.onClickOutside)
this.container.addEventListener('click', this.onClickOutside.bind(this))
}
/**
* 處理點擊事件來隱藏工具列
* Handle click event to hide toolbar
* @param {Event} e - 事件對象 (Event object)
*/
onClickOutside(e) {
const clickedNode = e.target.tagName === 'JMNODE'
const clickedToolbar = e.target.closest(`#${TOOLBAR_ID}`)
if (!clickedNode && !clickedToolbar && this.toolbar) {
this.hideToolbar()
}
}
/**
* 建立工具列 UI
* Create toolbar UI
*/
createToolbar() {
this.toolbar = document.createElement('div')
this.toolbar.id = TOOLBAR_ID
// 建立工具列按鈕
// Create toolbar buttons
const buttons = [
{ id: 'toolbar-add-child-btn', text: this.options.text.addNode, onClick: this.handleAddChild.bind(this) },
{ id: 'toolbar-delete-btn', text: this.options.text.deleteNode, onClick: this.handleDelete.bind(this) },
{
id: 'toolbar-stroke-color-btn',
text: this.options.text.strokeColor,
onClick: this.handleStrokeColor.bind(this),
},
{
id: 'toolbar-bg-color-btn',
text: this.options.text.bgColor,
onClick: this.handleBgColor.bind(this),
},
{
id: 'toolbar-text-color-btn',
text: this.options.text.textColor,
onClick: this.handleTextColor.bind(this),
},
]
buttons.forEach((button) => {
const btn = document.createElement('button')
btn.id = button.id
btn.innerText = button.text
btn.onclick = button.onClick
this.toolbar.appendChild(btn)
// 附加顏色選單
// Append color palettes to corresponding buttons
if (button.id === 'toolbar-bg-color-btn') {
this.bgColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('background-color', color),
btn
)
}
if (button.id === 'toolbar-stroke-color-btn') {
this.strokeColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('leading-line-color', color),
btn
)
}
if (button.id === 'toolbar-text-color-btn') {
this.textColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('foreground-color', color),
btn
)
}
})
this.container.appendChild(this.toolbar)
}
/**
* 移動工具列至選中節點
* Move the toolbar to the selected node
*/
moveToolbar(node) {
const nodeElement = node._data.view.element
if (!nodeElement) return
const { left, top } = getRelativePosition(nodeElement, this.container)
this.toolbar.style.left = `${left}px`
this.toolbar.style.top = `${top - 40}px`
this.toolbar.style.display = 'block'
// 根節點則隱藏刪除與線條顏色按鈕
// Hide delete & stroke color buttons if the node is root
const deleteBtn = this.toolbar.querySelector('#toolbar-delete-btn')
const strokeColorBtn = this.toolbar.querySelector('#toolbar-stroke-color-btn')
if (node.id === 'root') {
deleteBtn.style.display = 'none'
strokeColorBtn.style.display = 'none'
} else {
deleteBtn.style.display = 'inline-block'
strokeColorBtn.style.display = 'inline-block'
}
}
/**
* 隱藏工具列
* Hide the toolbar
*/
hideToolbar() {
this.toolbar.style.display = 'none'
this.bgColorPalette.style.display = 'none'
this.strokeColorPalette.style.display = 'none'
this.textColorPalette.style.display = 'none'
this.toolbarNodeId = null
}
/**
* 建立顏色選單
* Create color palette
*/
createColorPalette(onSelect, button) {
const colorPalette = document.createElement('div')
colorPalette.classList.add('toolbar-color-palette')
colorPalette.style.display = 'none'
PALETTE_COLORS.forEach((color) => {
const colorBox = document.createElement('div')
colorBox.classList.add('toolbar-color-palette-box')
colorBox.style.backgroundColor = color
colorBox.onclick = () => {
onSelect(color)
colorPalette.style.display = 'none'
}
colorPalette.appendChild(colorBox)
})
button.appendChild(colorPalette)
return colorPalette
}
/**
* 設定節點樣式
* Set node style
*/
setNodeStyle(style, color) {
if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return
node.data[style] = color
if (style === 'leading-line-color') this.jm.view.show_lines()
else this.jm.view.restore_selected_node_custom_style(node)
}
/**
* 處理新增節點事件
* Handle add child node event
*/
handleAddChild(e) {
e.preventDefault();
e.stopPropagation()
if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return
const newNode = this.jm.add_node(node, util.uuid.newid(), 'NewNode')
this.jm.select_node(newNode)
}
/**
* 處理刪除節點事件
* Handle delete node event
*/
handleDelete(e) {
e.preventDefault();
e.stopPropagation()
if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return
this.jm.remove_node(node)
this.hideToolbar()
}
/**
* 處理其他樣式設定事件
* Handle style setting event
*/
handleStrokeColor(e) {
e.preventDefault();
e.stopPropagation()
this.toggleColorPalette(this.strokeColorPalette)
}
handleBgColor(e) {
e.preventDefault();
e.stopPropagation()
this.toggleColorPalette(this.bgColorPalette)
}
handleTextColor(e) {
e.preventDefault();
e.stopPropagation()
this.toggleColorPalette(this.textColorPalette)
}
/**
* 顯示或隱藏顏色選單
* Toggle color palette display
*/
toggleColorPalette(palette) {
;[this.bgColorPalette, this.strokeColorPalette, this.textColorPalette].forEach((p) => {
if (p !== palette) p.style.display = 'none'
})
palette.style.display = palette.style.display === 'block' ? 'none' : 'block'
}
}
import { util } from '../jsmind/jsmind.util.js'
import { getRelativePosition } from './custom.util.js'
import { PALETTE_COLORS } from './custom.config.js'
const TOOLBAR_ID = 'jsmind-toolbar'
/**
* jsMind 工具列管理
* jsMind Toolbar Manager
*/
export class JsmindToolbar {
/**
* 建構工具列
* Constructor for toolbar
* @param {Object} jm - jsMind 實例 (jsMind instance)
* @param {Object} options - jsMind 實例 (options)
*/
constructor(jm, options) {
this.jm = jm
this.container = document.getElementById(jm.options.container)
this.toolbarNodeId = null
this.toolbar = null
this.bgColorPalette = null
this.strokeColorPalette = null
this.textColorPalette = null
this.options = options
this.init()
}
/**
* 初始化工具列事件
* Initialize toolbar events
*/
init() {
// 監聽節點選取事件
// Listen for node selection events
this.jm.add_event_listener((e, f, g) => {
// 忽略非選擇節點事件
// Ignore non-selection events
if (e !== 4) return
const node = this.jm.get_selected_node()
if (!node || node.id === this.toolbarNodeId) return
this.toolbarNodeId = node.id
if (!this.toolbar) {
this.createToolbar()
}
this.moveToolbar(node)
})
// 確保不會重複綁定點擊事件
// Ensure click event is not bound multiple times
this.container.removeEventListener('click', this.onClickOutside)
this.container.addEventListener('click', this.onClickOutside.bind(this))
}
/**
* 處理點擊事件來隱藏工具列
* Handle click event to hide toolbar
* @param {Event} e - 事件對象 (Event object)
*/
onClickOutside(e) {
const clickedNode = e.target.tagName === 'JMNODE'
const clickedToolbar = e.target.closest(`#${TOOLBAR_ID}`)
if (!clickedNode && !clickedToolbar && this.toolbar) {
this.hideToolbar()
}
}
/**
* 建立工具列 UI
* Create toolbar UI
*/
createToolbar() {
this.toolbar = document.createElement('div')
this.toolbar.id = TOOLBAR_ID
// 建立工具列按鈕
// Create toolbar buttons
const buttons = [
{ id: 'toolbar-add-child-btn', text: this.options.text.addNode, onClick: this.handleAddChild.bind(this) },
{ id: 'toolbar-delete-btn', text: this.options.text.deleteNode, onClick: this.handleDelete.bind(this) },
{
id: 'toolbar-stroke-color-btn',
text: this.options.text.strokeColor,
onClick: this.handleStrokeColor.bind(this),
},
{
id: 'toolbar-bg-color-btn',
text: this.options.text.bgColor,
onClick: this.handleBgColor.bind(this),
},
{
id: 'toolbar-text-color-btn',
text: this.options.text.textColor,
onClick: this.handleTextColor.bind(this),
},
]
buttons.forEach((button) => {
const btn = document.createElement('button')
btn.id = button.id
btn.innerText = button.text
btn.onclick = button.onClick
this.toolbar.appendChild(btn)
// 附加顏色選單
// Append color palettes to corresponding buttons
if (button.id === 'toolbar-bg-color-btn') {
this.bgColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('background-color', color),
btn
)
}
if (button.id === 'toolbar-stroke-color-btn') {
this.strokeColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('leading-line-color', color),
btn
)
}
if (button.id === 'toolbar-text-color-btn') {
this.textColorPalette = this.createColorPalette(
(color) => this.setNodeStyle('foreground-color', color),
btn
)
}
})
this.container.appendChild(this.toolbar)
}
/**
* 移動工具列至選中節點
* Move the toolbar to the selected node
*/
moveToolbar(node) {
const nodeElement = node._data.view.element
if (!nodeElement) return
const { left, top } = getRelativePosition(nodeElement, this.container)
this.toolbar.style.left = `${left}px`
this.toolbar.style.top = `${top - 40}px`
this.toolbar.style.display = 'block'
// 根節點則隱藏刪除與線條顏色按鈕
// Hide delete & stroke color buttons if the node is root
const deleteBtn = this.toolbar.querySelector('#toolbar-delete-btn')
const strokeColorBtn = this.toolbar.querySelector('#toolbar-stroke-color-btn')
if (node.id === 'root') {
deleteBtn.style.display = 'none'
strokeColorBtn.style.display = 'none'
} else {
deleteBtn.style.display = 'inline-block'
strokeColorBtn.style.display = 'inline-block'
}
}
/**
* 隱藏工具列
* Hide the toolbar
*/
hideToolbar() {
this.toolbar.style.display = 'none'
this.bgColorPalette.style.display = 'none'
this.strokeColorPalette.style.display = 'none'
this.textColorPalette.style.display = 'none'
this.toolbarNodeId = null
}
/**
* 建立顏色選單
* Create color palette
*/
createColorPalette(onSelect, button) {
const colorPalette = document.createElement('div')
colorPalette.classList.add('toolbar-color-palette')
colorPalette.style.display = 'none'
PALETTE_COLORS.forEach((color) => {
const colorBox = document.createElement('div')
colorBox.classList.add('toolbar-color-palette-box')
colorBox.style.backgroundColor = color
colorBox.onclick = () => {
onSelect(color)
colorPalette.style.display = 'none'
}
colorPalette.appendChild(colorBox)
})
button.appendChild(colorPalette)
return colorPalette
}
/**
* 設定節點樣式
* Set node style
*/
setNodeStyle(style, color) {
if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return
node.data[style] = color
if (style === 'leading-line-color') this.jm.view.show_lines()
else this.jm.view.restore_selected_node_custom_style(node)
}
/**
* 處理新增節點事件
* Handle add child node event
*/
handleAddChild(e) {
e.preventDefault();
e.stopPropagation()
if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return
const newNode = this.jm.add_node(node, util.uuid.newid(), 'NewNode')
this.jm.select_node(newNode)
}
/**
* 處理刪除節點事件
* Handle delete node event
*/
handleDelete(e) {
e.preventDefault();
e.stopPropagation()
if (!this.toolbarNodeId) return
const node = this.jm.get_node(this.toolbarNodeId)
if (!node) return
this.jm.remove_node(node)
this.hideToolbar()
}
/**
* 處理其他樣式設定事件
* Handle style setting event
*/
handleStrokeColor(e) {
e.preventDefault();
e.stopPropagation()
this.toggleColorPalette(this.strokeColorPalette)
}
handleBgColor(e) {
e.preventDefault();
e.stopPropagation()
this.toggleColorPalette(this.bgColorPalette)
}
handleTextColor(e) {
e.preventDefault();
e.stopPropagation()
this.toggleColorPalette(this.textColorPalette)
}
/**
* 顯示或隱藏顏色選單
* Toggle color palette display
*/
toggleColorPalette(palette) {
;[this.bgColorPalette, this.strokeColorPalette, this.textColorPalette].forEach((p) => {
if (p !== palette) p.style.display = 'none'
})
palette.style.display = palette.style.display === 'block' ? 'none' : 'block'
}
}

34
app/assets/javascripts/mind_map/utils/custom.util.js Normal file → Executable file
View File

@ -1,17 +1,17 @@
/**
* 獲取元素相對於指定容器的位置
* Get the relative position of an element within a given container
* @param {HTMLElement} element - 目標元素 (Target element)
* @param {HTMLElement} container - 參考容器 (Reference container)
* @returns {Object} - { left, top, height } 位置資訊 (Position details)
*/
export function getRelativePosition(element, container) {
let nodeRect = element.getBoundingClientRect()
let containerRect = container.getBoundingClientRect()
return {
left: nodeRect.left - containerRect.left,
top: nodeRect.top - containerRect.top,
height: nodeRect.height,
}
}
/**
* 獲取元素相對於指定容器的位置
* Get the relative position of an element within a given container
* @param {HTMLElement} element - 目標元素 (Target element)
* @param {HTMLElement} container - 參考容器 (Reference container)
* @returns {Object} - { left, top, height } 位置資訊 (Position details)
*/
export function getRelativePosition(element, container) {
let nodeRect = element.getBoundingClientRect()
let containerRect = container.getBoundingClientRect()
return {
left: nodeRect.left - containerRect.left,
top: nodeRect.top - containerRect.top,
height: nodeRect.height,
}
}

26
app/assets/javascripts/universal_table/application.js Normal file → Executable file
View File

@ -1,13 +1,13 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require_tree .
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require_tree .

12
app/assets/javascripts/universal_table/jquery-ui.min.js vendored Normal file → Executable file

File diff suppressed because one or more lines are too long

382
app/assets/stylesheets/mind_map/mindmap.css Normal file → Executable file
View File

@ -1,192 +1,192 @@
/* ============================
基礎布局 (Layout)
============================ */
.jsmind-inner {
position: relative;
overflow: auto;
width: 100%;
height: 100%;
outline: none;
user-select: none; /* 防止文字選取 */
}
.jsmind-inner canvas {
position: absolute;
}
#jsmind_container {
position: relative;
height: 800px;
border: 1px solid #ccc;
background: #f4f4f4;
}
/* ============================
層級管理 (Z-index)
============================ */
svg.jsmind,
canvas.jsmind {
position: absolute;
z-index: 1;
}
jmnodes {
position: absolute;
z-index: 2;
background-color: rgba(0, 0, 0, 0); /* 透明背景,確保可點擊 */
}
jmnode {
position: absolute;
cursor: default;
max-width: 400px;
}
jmexpander {
position: absolute;
width: 11px;
height: 11px;
display: block;
overflow: hidden;
line-height: 12px;
font-size: 10px;
text-align: center;
border-radius: 6px;
border-width: 1px;
border-style: solid;
cursor: pointer;
}
/* ============================
文字溢出控制 (Overflow)
============================ */
.jmnode-overflow-wrap jmnodes {
min-width: 420px;
}
.jmnode-overflow-hidden jmnode {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ============================
預設主題 (Default Theme)
============================ */
jmnode {
padding: 10px;
background-color: #fff;
color: #333;
border-radius: 5px;
box-shadow: 1px 1px 1px #666;
font: 1em/1.125 Verdana, Arial, Helvetica, sans-serif;
}
jmnode:hover {
box-shadow: 2px 2px 8px #000;
filter: brightness(95%);
a{
color: unset!important;
}
}
jmnode.selected {
box-shadow: 2px 2px 8px #000;
filter: brightness(90%);
}
jmnode.root {
font-size:1.2em;
/* 展開/收合按鈕 */
border-color: gray;
}
jmexpander:hover {
border-color: #000;
}
/* ============================
響應式設計 (Responsive)
============================ */
@media screen and (max-device-width: 1024px) {
jmnode {
padding: 5px;
border-radius: 3px;
font-size: 1.2em;
}
jmnode.root {
/* font-size: 21px; */
font-size: 1.2em;
}
}
/* ============================
工具列樣式 (Toolbar Styles)
============================ */
#jsmind-toolbar {
position: absolute;
z-index: 1000;
}
/* 顏色選單 */
.toolbar-color-palette {
position: absolute;
bottom: 30px;
background: #ccc;
border: 1px solid #ccc;
z-index: 1001;
min-width: 10em;
}
.toolbar-color-palette-box {
width: 20px;
height: 20px;
margin: 2px;
display: inline-block;
cursor: pointer;
}
/* ============================
遠程搜尋下拉選單 (Search Dropdown)
============================ */
.jsmind-suggestions {
position: absolute;
height: fit-content;
width: 200px;
background: white;
border: 1px solid #ccc;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.suggestion-item {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.suggestion-item:hover {
background: #f0f0f0;
}
.field-row{
.column_entry_files{
margin-top: 1em;
}
a{
color: unset;
}
strong{
display: none;
}
&:nth-child(2){
a{
font-weight: 500;
font-size: 1.5em;
}
}
&:nth-child(4){
display: none;
}
/* ============================
基礎布局 (Layout)
============================ */
.jsmind-inner {
position: relative;
overflow: auto;
width: 100%;
height: 100%;
outline: none;
user-select: none; /* 防止文字選取 */
}
.jsmind-inner canvas {
position: absolute;
}
#jsmind_container {
position: relative;
height: 800px;
border: 1px solid #ccc;
background: #f4f4f4;
}
/* ============================
層級管理 (Z-index)
============================ */
svg.jsmind,
canvas.jsmind {
position: absolute;
z-index: 1;
}
jmnodes {
position: absolute;
z-index: 2;
background-color: rgba(0, 0, 0, 0); /* 透明背景,確保可點擊 */
}
jmnode {
position: absolute;
cursor: default;
max-width: 400px;
}
jmexpander {
position: absolute;
width: 11px;
height: 11px;
display: block;
overflow: hidden;
line-height: 12px;
font-size: 10px;
text-align: center;
border-radius: 6px;
border-width: 1px;
border-style: solid;
cursor: pointer;
}
/* ============================
文字溢出控制 (Overflow)
============================ */
.jmnode-overflow-wrap jmnodes {
min-width: 420px;
}
.jmnode-overflow-hidden jmnode {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ============================
預設主題 (Default Theme)
============================ */
jmnode {
padding: 10px;
background-color: #fff;
color: #333;
border-radius: 5px;
box-shadow: 1px 1px 1px #666;
font: 1em/1.125 Verdana, Arial, Helvetica, sans-serif;
}
jmnode:hover {
box-shadow: 2px 2px 8px #000;
filter: brightness(95%);
a{
color: unset!important;
}
}
jmnode.selected {
box-shadow: 2px 2px 8px #000;
filter: brightness(90%);
}
jmnode.root {
font-size:1.2em;
/* 展開/收合按鈕 */
border-color: gray;
}
jmexpander:hover {
border-color: #000;
}
/* ============================
響應式設計 (Responsive)
============================ */
@media screen and (max-device-width: 1024px) {
jmnode {
padding: 5px;
border-radius: 3px;
font-size: 1.2em;
}
jmnode.root {
/* font-size: 21px; */
font-size: 1.2em;
}
}
/* ============================
工具列樣式 (Toolbar Styles)
============================ */
#jsmind-toolbar {
position: absolute;
z-index: 1000;
}
/* 顏色選單 */
.toolbar-color-palette {
position: absolute;
bottom: 30px;
background: #ccc;
border: 1px solid #ccc;
z-index: 1001;
min-width: 10em;
}
.toolbar-color-palette-box {
width: 20px;
height: 20px;
margin: 2px;
display: inline-block;
cursor: pointer;
}
/* ============================
遠程搜尋下拉選單 (Search Dropdown)
============================ */
.jsmind-suggestions {
position: absolute;
height: fit-content;
width: 200px;
background: white;
border: 1px solid #ccc;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.suggestion-item {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.suggestion-item:hover {
background: #f0f0f0;
}
.field-row{
.column_entry_files{
margin-top: 1em;
}
a{
color: unset;
}
strong{
display: none;
}
&:nth-child(2){
a{
font-weight: 500;
font-size: 1.5em;
}
}
&:nth-child(4){
display: none;
}
}

30
app/assets/stylesheets/universal_table/application.css Normal file → Executable file
View File

@ -1,15 +1,15 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any styles
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope.
*
*= require_tree .
*= require_self
*/
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any styles
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope.
*
*= require_tree .
*= require_self
*/

View File

@ -1,471 +1,471 @@
.main-forms .utable-heading-wrap {
margin-bottom: 8px;
border-radius: 0;
padding: 20px;
border-top-right-radius: 4px;
border-top-left-radius: 4px;
.control-group:last-child {
margin-bottom: 0;
}
.controls {
margin-left: 0;
}
}
.utable-heading-header {
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
padding-bottom: 10px;
h4 {
font-family: 'Chivo';
line-height: 26px;
margin: 0;
}
}
.utable-content {
.attributes {
padding: 20px;
&:nth-child(even) {
background-color: #e8e8e8;
}
&:nth-child(odd) {
background-color: #fff;
}
.draggable {
cursor: move;
}
.draggable i{
cursor: move;
vertical-align: middle;
}
}
}
.attributes-checkbox {
margin-left: 8px;
}
.main-forms > h3 {
margin: 5px 0 10px;
color: #333;
text-shadow: 0 1px 0 #ffffff;
font-family: 'Playfair Display SC', sans-serif;
}
.main-forms fieldset {
background-color: #FFFFFF;
border: 1px solid #EDEDED;
margin-bottom: 20px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
.select-holder{
margin-left: 20px;
display: inline-block;
span {
margin: 0 5px;
}
}
}
.main-forms fieldset .input-area:after {
content: "";
clear: both;
display: block;
height: 0;
visibility: hidden;
}
.main-forms fieldset .input-area .nav-name {
float: left;
width: 100px;
padding-top: 5px;
text-align: right;
display: block;
margin-bottom: 5px;
font-size: 14px;
font-weight: normal;
line-height: 20px;
}
.main-forms fieldset .input-area .controls textarea {
max-width: 500px;
max-height: 300px;
min-height: 86px;
}
.main-forms fieldset .input-area .controls textarea.cke_source {
max-width: 100%;
max-height: 100%;
}
.main-forms fieldset .input-area .controls hr {
margin: 5px 0 10px;
}
.main-forms fieldset .input-area .controls h5 {
margin: 5px 0;
}
.main-forms fieldset .input-area .controls .file-link {
margin-right: 10px;
display: inline-block;
width: 177px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.main-forms fieldset .input-area .controls .input-prepend {
margin-bottom: 5px;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group {
padding: 4px 12px;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group {
padding: 4px 12px;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group .radio input[type="radio"],
.main-forms fieldset .input-area .controls .input-prepend .btn-group .checkbox input[type="checkbox"] {
margin: 5px 5px 0 0;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group li {
text-align: left;
padding: 3px 10px;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group li label {
padding-left: 5px;
display: block;
}
.main-forms fieldset .input-area .controls .exist .input-prepend .btn-group:hover .dropdown-menu,
.main-forms fieldset .input-area .controls .add-target .input-prepend .btn-group:hover .dropdown-menu {
display: block;
}
.main-forms fieldset .input-area .controls .exist .input-prepend,
.main-forms fieldset .input-area .controls .add-target .input-prepend {
margin-bottom: 10px;
display: inline-block;
}
.main-forms fieldset .input-area .controls .exist .fileupload-new {
display: block;
}
.main-forms fieldset .input-area .controls .exist .fileupload-new .input-prepend {
display: inline-block;
}
.main-forms fieldset .input-area .controls .input-prepend a {
text-decoration: none;
color: #333333;
}
.main-forms fieldset .input-area .controls .input-prepend .tab-content > .active {
display: inline-block;
}
.main-forms fieldset .input-area .controls .add-btn {
margin: 3px 0;
}
.main-forms fieldset .input-area .fileupload {
margin-right: 15px;
margin-bottom: 0;
}
.main-forms fieldset .input-area .datetimepick {
margin-right: 5px;
margin-bottom: 5px;
}
.main-forms fieldset .input-area .datetimepick .add-on {
line-height: 24px;
cursor: pointer;
}
.main-forms fieldset .input-area .language-area .input-content .mceLayout {
width: 100%!important;
}
.main-forms fieldset .input-area .module-nav,
.main-forms fieldset .input-area .language-nav {
margin: 0 0 20px;
padding: 0 0 15px 120px;
border-bottom: 1px solid #ddd;
}
.main-forms fieldset .input-area .module-nav li,
.main-forms fieldset .input-area .language-nav li {
position: relative;
}
.main-forms fieldset .input-area .module-nav li.active:before,
.main-forms fieldset .input-area .module-nav li.active:after,
.main-forms fieldset .input-area .language-nav li.active:before,
.main-forms fieldset .input-area .language-nav li.active:after {
display: block;
height: 0px;
width: 0px;
position: absolute;
bottom: -15px;
left: 50%;
margin-left: -5px;
content: "";
border-style: solid;
border-width: 0 6px 6px 6px;
border-color: transparent transparent #EDEDED transparent;
z-index: 5
}
.main-forms fieldset .input-area .module-nav li.active:after {
display: none;
}
.main-forms fieldset .input-area .language-nav li.active:after {
bottom: -16px;
margin-left: -4px;
border-width: 0 5px 5px 5px;
border-color: transparent transparent #FFFFFF transparent;
}
.main-forms fieldset .input-area .module-nav {
margin-bottom: 0;
border-bottom: none;
}
.main-forms fieldset .input-area .language-area,
.main-forms fieldset .input-area .module-area {
overflow: visible;
}
.main-forms fieldset .input-area .module-area {
padding-top: 20px;
margin-bottom: 40px;
background-color: #EDEDED;
border-radius: 5px;
overflow: hidden;
}
.main-forms fieldset .form-actions {
padding-left: 200px;
margin: 0px;
-webkit-border-radius: 0 0 4px 4px;
-moz-border-radius: 0 0 4px 4px;
border-radius: 0 0 4px 4px;
}
.main-forms fieldset .input-area .nav-scroll {
margin-left: 120px;
width: 800px;
position: relative;
z-index: 1;
overflow: hidden;
}
.main-forms fieldset .input-area .nav-scroll .scroller {
width: 1000px;
height: 100%;
float: left;
padding: 0;
}
.main-forms fieldset .input-area .controls[data-toggle^="buttons-"] label {
position: relative;
margin: 0 0 5px;
}
.main-forms fieldset .input-area .controls[data-toggle^="buttons-"] input[type="radio"],
.main-forms fieldset .input-area .controls[data-toggle^="buttons-"] input[type="checkbox"] {
margin-left: 0;
margin-top: 0;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: block;
opacity: 0;
}
.main-forms fieldset .input-area .question {
margin-top: 5px;
}
/* User Role Forms */
#attributes-area.clickHere {
min-height: 150px;
position: relative;
}
#attributes-area.clickHere:before {
font-family: 'entypo';
content: '\e0be';
position: absolute;
font-size: 8em;
display: block;
bottom: 50px;
left: 175px;
color: #51a351;
opacity: .4;
}
.main-forms .input-append .tab-content {
display: inline-block;
overflow: inherit;
}
.main-forms .input-append .tab-content .active {
display: inline-block;
background-color: transparent;
}
.main-forms .input-append .active {
border-color: #c5c5c5;
border-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25);
}
.main-forms .input-append > .btn-group > .btn:first-child {
margin-left: -1px;
-webkit-border-bottom-left-radius: 0px;
border-bottom-left-radius: 0px;
-webkit-border-top-left-radius: 0px;
border-top-left-radius: 0px;
-moz-border-radius-bottomleft: 0px;
-moz-border-radius-topleft: 0px;
}
.main-forms .attributes {
padding-bottom: 20px;
}
.main-forms .attributes .tab-content {
overflow: inherit;
}
.main-forms .attributes.disabled label,
.main-forms .attributes.disabled h4 {
color: #e6e6e6;
}
.main-forms .attributes-header {
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
padding-bottom: 10px;
}
.main-forms .attributes-header .btn {
margin-left: 5px;
}
.main-forms .attributes-header h4 {
font-family: 'Chivo';
line-height: 26px;
margin: 0;
}
.main-forms .attributes-header h4 b {
padding: 0 1px;
border-style: dotted;
border-width: 0 2px;
border-color: #AAA;
margin-right: 5px;
cursor: move;
}
.main-forms .attributes-header h4 i {
cursor: pointer;
}
.main-forms .field-type {
background-color: #f5f5f5;
border-radius: 5px;
margin-bottom: 20px;
padding: 10px;
}
/* Responsive */
@media (max-width: 480px) {
.main-forms fieldset .input-area .nav-name {
float: none;
width: auto;
padding-top: 0;
text-align: left;
}
.main-forms fieldset .input-area .module-area {
padding: 20px;
}
.main-forms fieldset .input-area .module-nav,
.main-forms fieldset .input-area .language-nav {
padding: 0 0 15px 0px;
}
.main-forms fieldset .form-actions {
padding-right: 20px;
padding-left: 20px;
}
.main-forms fieldset .input-area .control-label {
width: auto;
}
.main-forms fieldset .input-area .controls {
margin-left: 0;
}
.main-forms fieldset .form-actions {
padding-left: 20px;
}
}
// Bootstrap override
// fixing datepicker appearing behind modal
.ut-table {
.image-preview {
width: 100px;
}
.image-expander {
position: relative;
display: inline-block;
&:hover {
.image-large {
opacity: 1;
}
}
}
.image-large {
border-radius: 2px;
-webkit-transition: .3s all ease-in-out;
transition: .3s all ease-in-out;
opacity: 0;
position: absolute;
left: calc(100% + 10px);
top: -10px;
width: 100%;
background-color: #fff;
padding: 10px;
box-shadow: 0 0 5px 0 rgba(0,0,0,.1);
}
}
.ut-control-group-col {
float: left;
width: 350px;
}
.ut-control-group-col-right {
width: 190px;
.control-label {
width: auto;
margin-right: 10px;
}
.controls {
margin-left: 0;
}
}
.main-forms .utable-heading-wrap {
margin-bottom: 8px;
border-radius: 0;
padding: 20px;
border-top-right-radius: 4px;
border-top-left-radius: 4px;
.control-group:last-child {
margin-bottom: 0;
}
.controls {
margin-left: 0;
}
}
.utable-heading-header {
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
padding-bottom: 10px;
h4 {
font-family: 'Chivo';
line-height: 26px;
margin: 0;
}
}
.utable-content {
.attributes {
padding: 20px;
&:nth-child(even) {
background-color: #e8e8e8;
}
&:nth-child(odd) {
background-color: #fff;
}
.draggable {
cursor: move;
}
.draggable i{
cursor: move;
vertical-align: middle;
}
}
}
.attributes-checkbox {
margin-left: 8px;
}
.main-forms > h3 {
margin: 5px 0 10px;
color: #333;
text-shadow: 0 1px 0 #ffffff;
font-family: 'Playfair Display SC', sans-serif;
}
.main-forms fieldset {
background-color: #FFFFFF;
border: 1px solid #EDEDED;
margin-bottom: 20px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
.select-holder{
margin-left: 20px;
display: inline-block;
span {
margin: 0 5px;
}
}
}
.main-forms fieldset .input-area:after {
content: "";
clear: both;
display: block;
height: 0;
visibility: hidden;
}
.main-forms fieldset .input-area .nav-name {
float: left;
width: 100px;
padding-top: 5px;
text-align: right;
display: block;
margin-bottom: 5px;
font-size: 14px;
font-weight: normal;
line-height: 20px;
}
.main-forms fieldset .input-area .controls textarea {
max-width: 500px;
max-height: 300px;
min-height: 86px;
}
.main-forms fieldset .input-area .controls textarea.cke_source {
max-width: 100%;
max-height: 100%;
}
.main-forms fieldset .input-area .controls hr {
margin: 5px 0 10px;
}
.main-forms fieldset .input-area .controls h5 {
margin: 5px 0;
}
.main-forms fieldset .input-area .controls .file-link {
margin-right: 10px;
display: inline-block;
width: 177px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.main-forms fieldset .input-area .controls .input-prepend {
margin-bottom: 5px;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group {
padding: 4px 12px;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group {
padding: 4px 12px;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group .radio input[type="radio"],
.main-forms fieldset .input-area .controls .input-prepend .btn-group .checkbox input[type="checkbox"] {
margin: 5px 5px 0 0;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group li {
text-align: left;
padding: 3px 10px;
}
.main-forms fieldset .input-area .controls .input-prepend .btn-group li label {
padding-left: 5px;
display: block;
}
.main-forms fieldset .input-area .controls .exist .input-prepend .btn-group:hover .dropdown-menu,
.main-forms fieldset .input-area .controls .add-target .input-prepend .btn-group:hover .dropdown-menu {
display: block;
}
.main-forms fieldset .input-area .controls .exist .input-prepend,
.main-forms fieldset .input-area .controls .add-target .input-prepend {
margin-bottom: 10px;
display: inline-block;
}
.main-forms fieldset .input-area .controls .exist .fileupload-new {
display: block;
}
.main-forms fieldset .input-area .controls .exist .fileupload-new .input-prepend {
display: inline-block;
}
.main-forms fieldset .input-area .controls .input-prepend a {
text-decoration: none;
color: #333333;
}
.main-forms fieldset .input-area .controls .input-prepend .tab-content > .active {
display: inline-block;
}
.main-forms fieldset .input-area .controls .add-btn {
margin: 3px 0;
}
.main-forms fieldset .input-area .fileupload {
margin-right: 15px;
margin-bottom: 0;
}
.main-forms fieldset .input-area .datetimepick {
margin-right: 5px;
margin-bottom: 5px;
}
.main-forms fieldset .input-area .datetimepick .add-on {
line-height: 24px;
cursor: pointer;
}
.main-forms fieldset .input-area .language-area .input-content .mceLayout {
width: 100%!important;
}
.main-forms fieldset .input-area .module-nav,
.main-forms fieldset .input-area .language-nav {
margin: 0 0 20px;
padding: 0 0 15px 120px;
border-bottom: 1px solid #ddd;
}
.main-forms fieldset .input-area .module-nav li,
.main-forms fieldset .input-area .language-nav li {
position: relative;
}
.main-forms fieldset .input-area .module-nav li.active:before,
.main-forms fieldset .input-area .module-nav li.active:after,
.main-forms fieldset .input-area .language-nav li.active:before,
.main-forms fieldset .input-area .language-nav li.active:after {
display: block;
height: 0px;
width: 0px;
position: absolute;
bottom: -15px;
left: 50%;
margin-left: -5px;
content: "";
border-style: solid;
border-width: 0 6px 6px 6px;
border-color: transparent transparent #EDEDED transparent;
z-index: 5
}
.main-forms fieldset .input-area .module-nav li.active:after {
display: none;
}
.main-forms fieldset .input-area .language-nav li.active:after {
bottom: -16px;
margin-left: -4px;
border-width: 0 5px 5px 5px;
border-color: transparent transparent #FFFFFF transparent;
}
.main-forms fieldset .input-area .module-nav {
margin-bottom: 0;
border-bottom: none;
}
.main-forms fieldset .input-area .language-area,
.main-forms fieldset .input-area .module-area {
overflow: visible;
}
.main-forms fieldset .input-area .module-area {
padding-top: 20px;
margin-bottom: 40px;
background-color: #EDEDED;
border-radius: 5px;
overflow: hidden;
}
.main-forms fieldset .form-actions {
padding-left: 200px;
margin: 0px;
-webkit-border-radius: 0 0 4px 4px;
-moz-border-radius: 0 0 4px 4px;
border-radius: 0 0 4px 4px;
}
.main-forms fieldset .input-area .nav-scroll {
margin-left: 120px;
width: 800px;
position: relative;
z-index: 1;
overflow: hidden;
}
.main-forms fieldset .input-area .nav-scroll .scroller {
width: 1000px;
height: 100%;
float: left;
padding: 0;
}
.main-forms fieldset .input-area .controls[data-toggle^="buttons-"] label {
position: relative;
margin: 0 0 5px;
}
.main-forms fieldset .input-area .controls[data-toggle^="buttons-"] input[type="radio"],
.main-forms fieldset .input-area .controls[data-toggle^="buttons-"] input[type="checkbox"] {
margin-left: 0;
margin-top: 0;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: block;
opacity: 0;
}
.main-forms fieldset .input-area .question {
margin-top: 5px;
}
/* User Role Forms */
#attributes-area.clickHere {
min-height: 150px;
position: relative;
}
#attributes-area.clickHere:before {
font-family: 'entypo';
content: '\e0be';
position: absolute;
font-size: 8em;
display: block;
bottom: 50px;
left: 175px;
color: #51a351;
opacity: .4;
}
.main-forms .input-append .tab-content {
display: inline-block;
overflow: inherit;
}
.main-forms .input-append .tab-content .active {
display: inline-block;
background-color: transparent;
}
.main-forms .input-append .active {
border-color: #c5c5c5;
border-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25);
}
.main-forms .input-append > .btn-group > .btn:first-child {
margin-left: -1px;
-webkit-border-bottom-left-radius: 0px;
border-bottom-left-radius: 0px;
-webkit-border-top-left-radius: 0px;
border-top-left-radius: 0px;
-moz-border-radius-bottomleft: 0px;
-moz-border-radius-topleft: 0px;
}
.main-forms .attributes {
padding-bottom: 20px;
}
.main-forms .attributes .tab-content {
overflow: inherit;
}
.main-forms .attributes.disabled label,
.main-forms .attributes.disabled h4 {
color: #e6e6e6;
}
.main-forms .attributes-header {
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
padding-bottom: 10px;
}
.main-forms .attributes-header .btn {
margin-left: 5px;
}
.main-forms .attributes-header h4 {
font-family: 'Chivo';
line-height: 26px;
margin: 0;
}
.main-forms .attributes-header h4 b {
padding: 0 1px;
border-style: dotted;
border-width: 0 2px;
border-color: #AAA;
margin-right: 5px;
cursor: move;
}
.main-forms .attributes-header h4 i {
cursor: pointer;
}
.main-forms .field-type {
background-color: #f5f5f5;
border-radius: 5px;
margin-bottom: 20px;
padding: 10px;
}
/* Responsive */
@media (max-width: 480px) {
.main-forms fieldset .input-area .nav-name {
float: none;
width: auto;
padding-top: 0;
text-align: left;
}
.main-forms fieldset .input-area .module-area {
padding: 20px;
}
.main-forms fieldset .input-area .module-nav,
.main-forms fieldset .input-area .language-nav {
padding: 0 0 15px 0px;
}
.main-forms fieldset .form-actions {
padding-right: 20px;
padding-left: 20px;
}
.main-forms fieldset .input-area .control-label {
width: auto;
}
.main-forms fieldset .input-area .controls {
margin-left: 0;
}
.main-forms fieldset .form-actions {
padding-left: 20px;
}
}
// Bootstrap override
// fixing datepicker appearing behind modal
.ut-table {
.image-preview {
width: 100px;
}
.image-expander {
position: relative;
display: inline-block;
&:hover {
.image-large {
opacity: 1;
}
}
}
.image-large {
border-radius: 2px;
-webkit-transition: .3s all ease-in-out;
transition: .3s all ease-in-out;
opacity: 0;
position: absolute;
left: calc(100% + 10px);
top: -10px;
width: 100%;
background-color: #fff;
padding: 10px;
box-shadow: 0 0 5px 0 rgba(0,0,0,.1);
}
}
.ut-control-group-col {
float: left;
width: 350px;
}
.ut-control-group-col-right {
width: 190px;
.control-label {
width: auto;
margin-right: 10px;
}
.controls {
margin-left: 0;
}
}

104
app/controllers/admin/mind_maps_controller.rb Normal file → Executable file
View File

@ -1,52 +1,52 @@
class Admin::MindMapsController < OrbitAdminController
def index
@table_fields = ["universal_table.mind_map","universal_table.created_time"]
@table = UTable.find(params[:id])
@mind_maps = Kaminari.paginate_array(@table.mind_maps).page(params[:page]).per(10)
end
def new
@table = UTable.find(params[:table])
@mind_map = MindMap.new
end
def edit
uid = params[:id].split("-").last
@mind_map = MindMap.where(:uid => uid).first
@table = @mind_map.u_table
end
def create
mind_map = MindMap.new
mind_params = mind_map_params
mind_params[:mind_map_data] = JSON.parse(mind_params[:mind_map_data])
mind_map.update_attributes(mind_map_params)
mind_map.save
redirect_to "/admin/universal_table/#{mind_map.u_table.id.to_s}/mind_maps"
end
def update
mind_map = MindMap.find(params[:id])
mind_params = mind_map_params
mind_params[:mind_map_data] = JSON.parse(mind_params[:mind_map_data])
mind_map.update_attributes(mind_map_params)
mind_map.save
redirect_to "/admin/universal_table/#{mind_map.u_table.id.to_s}/mind_maps"
end
def destroy
uid = params[:id].split("-").last
mind_map = MindMap.where(:uid => uid).first
table = mind_map.u_table
mind_map.destroy
redirect_to "/admin/universal_table/#{table.id.to_s}/mind_maps"
end
private
def mind_map_params
params.require(:mind_map).permit!
end
end
class Admin::MindMapsController < OrbitAdminController
def index
@table_fields = ["universal_table.mind_map","universal_table.created_time"]
@table = UTable.find(params[:id])
@mind_maps = Kaminari.paginate_array(@table.mind_maps).page(params[:page]).per(10)
end
def new
@table = UTable.find(params[:table])
@mind_map = MindMap.new
end
def edit
uid = params[:id].split("-").last
@mind_map = MindMap.where(:uid => uid).first
@table = @mind_map.u_table
end
def create
mind_map = MindMap.new
mind_params = mind_map_params
mind_params[:mind_map_data] = JSON.parse(mind_params[:mind_map_data])
mind_map.update_attributes(mind_map_params)
mind_map.save
redirect_to "/admin/universal_table/#{mind_map.u_table.id.to_s}/mind_maps"
end
def update
mind_map = MindMap.find(params[:id])
mind_params = mind_map_params
mind_params[:mind_map_data] = JSON.parse(mind_params[:mind_map_data])
mind_map.update_attributes(mind_map_params)
mind_map.save
redirect_to "/admin/universal_table/#{mind_map.u_table.id.to_s}/mind_maps"
end
def destroy
uid = params[:id].split("-").last
mind_map = MindMap.where(:uid => uid).first
table = mind_map.u_table
mind_map.destroy
redirect_to "/admin/universal_table/#{table.id.to_s}/mind_maps"
end
private
def mind_map_params
params.require(:mind_map).permit!
end
end

232
app/controllers/admin/universal_tables_controller.rb Normal file → Executable file
View File

@ -135,111 +135,131 @@ end
render :json => {"success" => true, "title" => title}.to_json
end
def import_data_from_excel
workbook = RubyXL::Parser.parse(params["import_data"].tempfile)
response = {}
current_locale = I18n.locale
table = UTable.find(params["universal_table_id"]) rescue nil
if !table.nil?
sheet = workbook[0]
if sheet.count <= 503
columns = sheet[1].cells.collect.with_index{|c,i|
c.value.blank? ? table.table_columns.where(:title => sheet[0].cells[i].value.to_s.split("-").first.strip).first : table.table_columns.where(:key => c.value.to_s).first
}
languages = sheet[2].cells.collect{|c|
c.value.split("-").last rescue nil
}
sheet.each_with_index do |row, i|
next if i < 3
te = TableEntry.new
te.u_table = table
skip = 0
row.cells.each_with_index do |cell,index|
if skip > 1
skip = skip - 1
next
end
skip = 0
val = cell.value rescue nil
tc = columns[index]
if !tc.nil?
ce = ColumnEntry.new
case tc.type
when "text"
v = {}
@site_in_use_locales.sort.each_with_index do |locale,x|
v[locale.to_s] = row.cells[index + x].value rescue nil
skip = skip + 1
end
ce.text_translations = v
when "integer"
ce.number = (val.blank? ? nil : val)
when "image"
ce.remote_image_url = val
when "file"
if !val.nil?
val.split("\;").each do |remote_file|
file = ColumnEntryFile.new
file.remote_file_url = remote_file
filename = {}
file.choose_lang.reject(&:empty?).each do |lang|
filename[lang] = file.file.file.filename
end
file.file_title_translations = filename
# file.column_entry_id = ce.id
file.save
ce.column_entry_files << file
end
end
when "editor"
v = {}
@site_in_use_locales.sort.each_with_index do |locale,x|
v[locale.to_s] = row.cells[index + x].value rescue nil
skip = skip + 1
end
ce.content_translations = v
when "date"
ce.date = val
when "period"
if tc.date_format == "yyyy" && !val.nil?
val = val.to_s + "/01/01"
end
skip = 2
ce.period_from = val
val = row.cells[index + 1].value rescue nil
if tc.date_format == "yyyy" && !val.nil?
val = val.to_s + "/01/01"
end
ce.period_to = val
end
ce.table_column_id = tc.id
ce.save
te.column_entries << ce
else
if index == (columns.count - 2)
create_get_table_tags(te, val.split("\;"))
end
if index == (columns.count - 1)
te.related_entries = TableEntry.where(:uid.in => val.to_s.split("\;")).pluck(:id).join(",")
end
end
end
te.save
te.fix_have_data
end
response["success"] = true
response["count"] = table.table_entries.count
response["id"] = table.id.to_s
else
response["success"] = false
response["msg"] = "More than 500 entries. Please split the entries in different files."
end
else
response["success"] = false
response["msg"] = "Table not found."
end
render :json => response.to_json
end
def import_data_from_excel
site_in_use_locales = @site_in_use_locales.sort
workbook = RubyXL::Parser.parse(params["import_data"].tempfile)
response = {}
current_locale = I18n.locale
table = UTable.find(params["universal_table_id"]) rescue nil
if table.nil?
render json: { success: false, msg: "Table not found." }.to_json and return
end
sheet = workbook[0]
if sheet.count > 503
render json: { success: false, msg: "More than 500 entries. Please split the entries in different files." }.to_json and return
end
# 前三列是欄位名、key、格式描述
column_titles = sheet[0].cells.map { |c| c&.value.to_s.strip }
column_keys = sheet[1].cells.map { |c| c&.value.to_s.strip }
column_types = sheet[2].cells.map { |c| c&.value.to_s.strip }
# 準備欄位對應
columns = column_keys.uniq.map.with_index do |key, i|
tc = table.table_columns.where(key: key).first
[i, tc]
end.to_h
sheet.each_with_index do |row, i|
next if i < 3 || row.cells.compact.map { |c| c.value.to_s.strip }.all?(&:blank?)
uid_val = row[0]&.value.to_s.strip rescue nil
te = uid_val.present? ?
TableEntry.where(uid: uid_val, u_table_id: table.id).first_or_initialize :
TableEntry.new
te.u_table = table
skip = 0
tc_idx = 0
row.cells.each_with_index do |cell, col_idx|
next if skip > 0 && (skip -= 1) >= 0
val = cell&.value
tc = columns[tc_idx]
tc_idx += 1
next if tc.nil?
ce = te.column_entries.where(table_column_id: tc.id).first
ce = ColumnEntry.new(table_column_id: tc.id) if ce.nil?
case tc.type
when "text", "editor"
v = {}
site_in_use_locales.each_with_index do |locale, offset|
v[locale.to_s] = row[col_idx + offset]&.value.to_s rescue ""
end
skip = site_in_use_locales.size - 1
if tc.type == "text"
ce.text_translations = v
else
ce.content_translations = v
end
when "integer"
ce.number = val.present? ? val.to_i : nil
when "image"
ce.remote_image_url = val if val.present?
when "file"
file_urls = val.to_s.split(";").map(&:strip)
file_titles = row[col_idx + 1]&.value.to_s.split(";").map(&:strip)
skip = 1
ce.column_entry_files.destroy_all
ce.column_entry_files = []
file_urls.each_with_index do |remote_url, file_idx|
next if remote_url.blank?
file = ColumnEntryFile.new
file.remote_file_url = remote_url
# 處理多語言標題
titles = {}
site_in_use_locales.each do |locale|
titles[locale.to_s] = file_titles[file_idx] rescue file.file.file.filename
end
file.file_title_translations = titles
file.save!
ce.column_entry_files << file
end
when "date"
ce.date = val
when "period"
skip = 1
ce.period_from = val
ce.period_to = row[col_idx + 1]&.value rescue nil
end
ce.save!
te.column_entries << ce
end
# hashtags (倒數第2欄)
if row.cells.count >= 2
tags_text = row.cells[-2]&.value.to_s rescue ""
create_get_table_tags(te, tags_text.split(";"))
end
# related_entries (倒數第1欄)
if row.cells.count >= 1
related_uids = row.cells[-1]&.value.to_s.split(";").map(&:strip)
related_ids = TableEntry.where(:uid.in => related_uids).pluck(:id)
te.related_entries = related_ids.join(",")
end
te.save!
te.fix_have_data
te.uid = uid_val if uid_val.present?
te.save!
end
render json: {
success: true,
count: table.table_entries.count,
id: table.id.to_s
}.to_json
end
def new_entry
uid = params[:universal_table_id].split("-").last
@ -402,4 +422,4 @@ end
def is_uuid?(str)
!!(str =~ /\A[\da-f]{24}\z/i || str =~ /\A[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}\z/i)
end
end
end

165
app/controllers/universal_tables_controller.rb Normal file → Executable file
View File

@ -10,67 +10,114 @@ class UniversalTablesController < ApplicationController
end
end
def export_filtered
table = UTable.where(:category_id => params[:cat]).first rescue nil
page = Page.where(:page_id => params[:page_id]).first
if !table.nil?
host_url = Site.first.root_url
if host_url == "http://"
host_url = request.protocol + request.host_with_port
end
@rows = []
@tablecolumns = table.table_columns.where(:display_in_index => true).asc(:order)
entries = get_entries(params, table, page, false)
entries.each do |te|
cols = []
sort_value = ""
@tablecolumns.each do |column|
ce = te.column_entries.where(:table_column_id => column.id).first rescue nil
if !ce.nil?
text = ""
case ce.type
when "text"
text = ce.text
when "integer"
text = ce.number
when "editor"
text = ce.content
when "date"
text = format_date(ce.date, column.date_format)
when "period"
text = format_date(ce.period_from, column.date_format) + " ~ " + format_date(ce.period_to, column.date_format)
text = "" if text.starts_with?(" ~")
when "image"
text = host_url + ce.image.thumb.url
when "file"
file_links = []
locale = I18n.locale.to_s
ce.column_entry_files.desc(:sort_number).each do |entry_file|
next unless entry_file.choose_lang_display(locale)
file_links << (host_url + entry_file.get_link)
end
text = file_links.join("\r\n")
end
cols << {"text" => text}
else
cols << {"text" => ""}
end
end
@rows << {
"columns" => cols
}
end
excel_name = table.title + ".xlsx"
excel_name = 'attachment; filename="' + excel_name + '"'
def export_filtered
table = UTable.where(:category_id => params[:cat]).first rescue nil
page = Page.where(:page_id => params[:page_id]).first
return unless table
end
respond_to do |format|
format.xlsx {
response.headers['Content-Disposition'] = excel_name
}
end
end
host_url = Site.first.root_url
host_url = request.protocol + request.host_with_port if host_url == "http://"
@rows = []
@tablecolumns = []
# 處理 file 欄位雙欄輸出(連結與註解)
table.table_columns.where(display_in_index: true).asc(:order).each do |column|
if column.type == "file"
@tablecolumns << column
@tablecolumns << column.dup.tap { |c| c.define_singleton_method(:_file_title_column?) { true } }
else
@tablecolumns << column
end
end
entries = get_entries(params, table, page, false)
entries.each do |te|
cols = []
@tablecolumns.each do |column|
# 跳過副註解欄(由主 file 欄處理)
if column.respond_to?(:_file_title_column?) && column._file_title_column?
next
end
ce = te.column_entries.where(table_column_id: column.id).first rescue nil
if ce.present?
case column.type
when "text"
cols << { "text" => ce.text.to_s }
when "integer"
cols << { "text" => ce.number.to_s }
when "editor"
cols << { "text" => ce.content.to_s }
when "date"
cols << { "text" => format_date(ce.date, column.date_format).to_s }
when "period"
from = format_date(ce.period_from, column.date_format)
to = format_date(ce.period_to, column.date_format)
text = from.blank? && to.present? ? to : "#{from} ~ #{to}"
cols << { "text" => text.to_s }
when "image"
text = ce.image&.thumb&.url ? (host_url + ce.image.thumb.url) : ""
cols << { "text" => text }
when "file"
file_links = []
file_titles = []
locale = I18n.locale.to_s
ce.column_entry_files.desc(:sort_number).each do |entry_file|
next unless entry_file.choose_lang_display(locale)
file_links << (host_url + entry_file.get_link)
title = if entry_file.respond_to?(:file_title_translations) && entry_file.file_title_translations.is_a?(Hash)
entry_file.file_title_translations[locale]
elsif entry_file.file_title.is_a?(Hash)
entry_file.file_title[locale]
else
entry_file.file_title
end
title = entry_file.file.filename.to_s if title.blank?
file_titles << title
end
cols << { "text" => file_links.join("\r\n") }
cols << { "text" => file_titles.join("\r\n") }
else
cols << { "text" => "" }
end
else
if column.type == "file"
cols << { "text" => "" }
cols << { "text" => "" }
else
cols << { "text" => "" }
end
end
end
@rows << { "columns" => cols }
end
excel_name = "#{table.title}.xlsx"
excel_name = 'attachment; filename="' + excel_name + '"'
respond_to do |format|
format.xlsx {
response.headers['Content-Disposition'] = excel_name
}
end
end
def get_query(params)
if params["column"].present?
q = {params["column"] => params["q"]}

86
app/helpers/admin/universal_tables_helper.rb Normal file → Executable file
View File

@ -1,43 +1,43 @@
module Admin::UniversalTablesHelper
def format_date(date, format, for_editing=false)
case format
when "yyyy/MM/dd hh:mm"
f = "%Y/%m/%d %H:%M"
when "yyyy/MM/dd"
f = "%Y/%m/%d"
when "yyyy/MM"
f = "%Y/%m"
when "yyyy"
f = "%Y"
f = "%Y/%m" if for_editing
end
d = date.strftime(f) rescue ""
return d
end
def render_unique_texts(f,column,i)
select_values = column.column_entries.distinct(:text)
select = "<select id='#{column.key}_#{i}'>"
s = {"en" => "", "zh_tw" => ""}
select = select + "<option class='muted' value='#{s.to_json.html_safe}'>---------------Select---------------</option>"
select_values.each do |sv|
select = select + "<option value='#{sv.to_json.html_safe}'>#{sv[I18n.locale]}</option>"
end
select = select + "</select>"
"<div class='select-holder'> <span>Or</span> " + select + "</div>"
end
def render_unique_number(f,column,i)
select_values = column.column_entries.distinct(:number)
select = "<select id='#{column.key}_#{i}'>"
s = ""
select = select + "<option class='muted' value='#{s}'>---------------Select---------------</option>"
select_values.each do |sv|
select = select + "<option value='#{sv}'>#{sv}</option>"
end
select = select + "</select>"
"<div class='select-holder'> <span>Or</span> " + select + "</div>"
end
end
module Admin::UniversalTablesHelper
def format_date(date, format, for_editing=false)
case format
when "yyyy/MM/dd hh:mm"
f = "%Y/%m/%d %H:%M"
when "yyyy/MM/dd"
f = "%Y/%m/%d"
when "yyyy/MM"
f = "%Y/%m"
when "yyyy"
f = "%Y"
f = "%Y/%m" if for_editing
end
d = date.strftime(f) rescue ""
return d
end
def render_unique_texts(f,column,i)
select_values = column.column_entries.distinct(:text)
select = "<select id='#{column.key}_#{i}'>"
s = {"en" => "", "zh_tw" => ""}
select = select + "<option class='muted' value='#{s.to_json.html_safe}'>---------------Select---------------</option>"
select_values.each do |sv|
select = select + "<option value='#{sv.to_json.html_safe}'>#{sv[I18n.locale]}</option>"
end
select = select + "</select>"
"<div class='select-holder'> <span>Or</span> " + select + "</div>"
end
def render_unique_number(f,column,i)
select_values = column.column_entries.distinct(:number)
select = "<select id='#{column.key}_#{i}'>"
s = ""
select = select + "<option class='muted' value='#{s}'>---------------Select---------------</option>"
select_values.each do |sv|
select = select + "<option value='#{sv}'>#{sv}</option>"
end
select = select + "</select>"
"<div class='select-holder'> <span>Or</span> " + select + "</div>"
end
end

204
app/models/column_entry.rb Normal file → Executable file
View File

@ -1,102 +1,102 @@
class ColumnEntry
include Mongoid::Document
include Mongoid::Timestamps
include Admin::UniversalTablesHelper
include ActionView::Helpers::NumberHelper
field :text, :localize => true
field :content, :localize => true
field :date, type: DateTime
field :period_from, type: DateTime
field :period_to, type: DateTime
field :number, type: Integer
mount_uploader :image, ImageUploader
has_many :column_entry_files, :autosave => true, :dependent => :destroy
accepts_nested_attributes_for :column_entry_files, :allow_destroy => true
after_save :save_column_entry_files
belongs_to :table_entry, index: true
belongs_to :table_column, index: true
I18n.available_locales.each do |locale|
index({"text.#{locale}" => 1}, { unique: false, background: true })
index({"content.#{locale}" => 1}, { unique: false, background: true })
end
def type
self.table_column.type
end
def save_column_entry_files
return if @skip_callback
self.column_entry_files.each do |t|
if t.should_destroy
t.destroy
end
end
end
def have_data(locale)
flag = nil
case self.type
when "text"
flag = self.text_translations[locale].present?
when "integer"
flag = true
when "editor"
flag = self.content_translations[locale].present?
when "date"
flag = self.date.present?
when "period"
flag = self.period_from.present? || self.period_to.present?
when "image"
flag = self.image.present?
when "file"
flag = false
self.column_entry_files.each do |entry_file|
next unless entry_file.choose_lang_display(locale) && entry_file.file.present?
flag = true
end
else
flag = true
end
flag
end
def get_frontend_text(column)
text = ""
case self.type
when "text"
text = self.text
when "integer"
text = self.number
when "editor"
text = self.content
when "date"
text = format_date(self.date, column.date_format)
when "period"
text = format_date(self.period_from, column.date_format) + " ~ " + format_date(self.period_to, column.date_format)
text = "" if text.starts_with?(" ~")
when "image"
text = "<img src='#{self.image.thumb.url}' class='image-preview' />"
when "file"
locale = I18n.locale.to_s
text = "<ul class=\"column_entry_files\">"
self.column_entry_files.desc(:sort_number).each do |entry_file|
next unless entry_file.choose_lang_display(locale)
file_title = entry_file.get_file_title
if entry_file.file.content_type.start_with?('audio/')
text += "<div class=\"voice-player\"><span class=\"voice-title\">#{file_title}</span><a class=\"voice-player\" data-content=\"#{entry_file.file.url}\" href="" title=\"#{file_title}\"><i class=\"fa fa-play\" aria-hidden=\"true\"></i></a></div>"
else
text += "<li class=\"column_entry_file\"><a class=\"column_entry_file_link\" href=\"#{entry_file.get_link}\" title=\"#{file_title}\" target=\"_blank\">#{file_title}</a><span class=\"file_size\">(#{number_to_human_size(entry_file.file.size)})</span><span class=\"view_count\"><i class=\"fa fa-eye\" title=\"#{I18n.t("universal_table.downloaded_times")}\"></i><span class=\"view-count\">#{entry_file.download_count}</span></span></li>"
end
end
text += "</ul>"
end
text
end
end
class ColumnEntry
include Mongoid::Document
include Mongoid::Timestamps
include Admin::UniversalTablesHelper
include ActionView::Helpers::NumberHelper
field :text, :localize => true
field :content, :localize => true
field :date, type: DateTime
field :period_from, type: DateTime
field :period_to, type: DateTime
field :number, type: Integer
mount_uploader :image, ImageUploader
has_many :column_entry_files, :autosave => true, :dependent => :destroy
accepts_nested_attributes_for :column_entry_files, :allow_destroy => true
after_save :save_column_entry_files
belongs_to :table_entry, index: true
belongs_to :table_column, index: true
I18n.available_locales.each do |locale|
index({"text.#{locale}" => 1}, { unique: false, background: true })
index({"content.#{locale}" => 1}, { unique: false, background: true })
end
def type
self.table_column.type
end
def save_column_entry_files
return if @skip_callback
self.column_entry_files.each do |t|
if t.should_destroy
t.destroy
end
end
end
def have_data(locale)
flag = nil
case self.type
when "text"
flag = self.text_translations[locale].present?
when "integer"
flag = true
when "editor"
flag = self.content_translations[locale].present?
when "date"
flag = self.date.present?
when "period"
flag = self.period_from.present? || self.period_to.present?
when "image"
flag = self.image.present?
when "file"
flag = false
self.column_entry_files.each do |entry_file|
next unless entry_file.choose_lang_display(locale) && entry_file.file.present?
flag = true
end
else
flag = true
end
flag
end
def get_frontend_text(column)
text = ""
case self.type
when "text"
text = self.text
when "integer"
text = self.number
when "editor"
text = self.content
when "date"
text = format_date(self.date, column.date_format)
when "period"
text = format_date(self.period_from, column.date_format) + " ~ " + format_date(self.period_to, column.date_format)
text = "" if text.starts_with?(" ~")
when "image"
text = "<img src='#{self.image.thumb.url}' class='image-preview' />"
when "file"
locale = I18n.locale.to_s
text = "<ul class=\"column_entry_files\">"
self.column_entry_files.desc(:sort_number).each do |entry_file|
next unless entry_file.choose_lang_display(locale)
file_title = entry_file.get_file_title
if entry_file.file.content_type.start_with?('audio/')
text += "<div class=\"voice-player\"><span class=\"voice-title\">#{file_title}</span><a class=\"voice-player\" data-content=\"#{entry_file.file.url}\" href="" title=\"#{file_title}\"><i class=\"fa fa-play\" aria-hidden=\"true\"></i></a></div>"
else
text += "<li class=\"column_entry_file\"><a class=\"column_entry_file_link\" href=\"#{entry_file.get_link}\" title=\"#{file_title}\" target=\"_blank\">#{file_title}</a><span class=\"file_size\">(#{number_to_human_size(entry_file.file.size)})</span><span class=\"view_count\"><i class=\"fa fa-eye\" title=\"#{I18n.t("universal_table.downloaded_times")}\"></i><span class=\"view-count\">#{entry_file.download_count}</span></span></li>"
end
end
text += "</ul>"
end
text
end
end

2
app/models/column_entry_file.rb Normal file → Executable file
View File

@ -5,7 +5,7 @@ class ColumnEntryFile
mount_uploader :file, AssetUploader
field :file_title, localize: true
field :file_title, type: String, localize: true
# field :description
field :download_count, type: Integer, default: 0
field :choose_lang, :type => Array, :default => I18n.available_locales.map{|l| l.to_s}

22
app/models/mind_map.rb Normal file → Executable file
View File

@ -1,11 +1,11 @@
class MindMap
include Mongoid::Document
include Mongoid::Timestamps
include Slug
field :title, as: :slug_title, localize: true
field :mind_map_data, type: Array, default: []
belongs_to :u_table
# has_many :mind_map_nodes, :dependent => :destroy
end
class MindMap
include Mongoid::Document
include Mongoid::Timestamps
include Slug
field :title, as: :slug_title, localize: true
field :mind_map_data, type: Array, default: []
belongs_to :u_table
# has_many :mind_map_nodes, :dependent => :destroy
end

112
app/models/table_column.rb Normal file → Executable file
View File

@ -1,57 +1,57 @@
class TableColumn
include Mongoid::Document
include Mongoid::Timestamps
field :key
field :title, localize: true
field :display_in_index, type: Boolean, default: true
field :type
field :date_format, default: "YYYY/MM/DD"
field :is_link_to_show, type: Boolean, default: false
field :is_searchable, type: Boolean
field :order, type: Integer
field :make_categorizable, type: Boolean, default: false
field :default_ordered_field, type: Boolean, default: false
field :order_direction,type: String,default: 'desc'
belongs_to :u_table, index: true
has_many :column_entries
index({display_in_index: -1}, { unique: false, background: true })
index({order: 1}, { unique: false, background: true })
index({key: 1}, { unique: false, background: true })
def sort_hash(direction)
case self.type
when "text"
{text: direction}
when "integer"
{number: direction}
when "editor"
{content: direction}
when "image"
{image: direction}
when "date"
{date: direction}
when "period"
{period_from: direction,period_to: direction}
end
end
def is_searchable
tmp = self[:is_searchable]
if tmp.nil?
case self.type
when "date", "period","image"
tmp = false
else
tmp = self.display_in_index
end
end
tmp
end
def self.filter_searchable
self.any_of({is_searchable: true}, {is_searchable:nil, display_in_index: true, :type.nin=> ["date", "period","image"]})
end
class TableColumn
include Mongoid::Document
include Mongoid::Timestamps
field :key
field :title, localize: true
field :display_in_index, type: Boolean, default: true
field :type
field :date_format, default: "YYYY/MM/DD"
field :is_link_to_show, type: Boolean, default: false
field :is_searchable, type: Boolean
field :order, type: Integer
field :make_categorizable, type: Boolean, default: false
field :default_ordered_field, type: Boolean, default: false
field :order_direction,type: String,default: 'desc'
belongs_to :u_table, index: true
has_many :column_entries
index({display_in_index: -1}, { unique: false, background: true })
index({order: 1}, { unique: false, background: true })
index({key: 1}, { unique: false, background: true })
def sort_hash(direction)
case self.type
when "text"
{text: direction}
when "integer"
{number: direction}
when "editor"
{content: direction}
when "image"
{image: direction}
when "date"
{date: direction}
when "period"
{period_from: direction,period_to: direction}
end
end
def is_searchable
tmp = self[:is_searchable]
if tmp.nil?
case self.type
when "date", "period","image"
tmp = false
else
tmp = self.display_in_index
end
end
tmp
end
def self.filter_searchable
self.any_of({is_searchable: true}, {is_searchable:nil, display_in_index: true, :type.nin=> ["date", "period","image"]})
end
end

340
app/models/table_entry.rb Normal file → Executable file
View File

@ -1,170 +1,170 @@
class TableEntry
include Mongoid::Document
include Mongoid::Timestamps
include OrbitModel::Status
include Slug
attr_accessor :sort_value
field :have_data, type: Boolean, localize: true
field :sort_number, type: Integer
field :view_count, type: Integer, default: 0
field :related_entries, type: String, default: ""
has_many :column_entries, :dependent => :destroy
belongs_to :u_table, index: true
has_and_belongs_to_many :table_tags, inverse_of: :table_entries
accepts_nested_attributes_for :column_entries, :allow_destroy => true
scope :can_display, ->{where(:is_hidden.ne=>true)}
I18n.available_locales.each do |locale|
index({"have_data.#{locale}" => 1}, { unique: false, background: true })
end
before_save do
if self[:sort_number].nil?
other_record = self.class.where(:u_table_id=> self.u_table_id, :id.ne=> self.id).order_by(sort_number: :desc).first
sort_number_to_set = other_record ? other_record.sort_number : 0
self.sort_number = sort_number_to_set.to_i + 1
end
self.get_have_data
end
def fix_have_data
have_data_translations = self.get_have_data
self.class.where(:id=> self.id).update_all(have_data_translations.map{|l, v| ["have_data.#{l}", v]}.to_h)
end
def get_related_entries
tids = self.related_entries.split(',')
TableEntry.find(tids)
end
def get_related_entries_uid
tids = self.related_entries.split(',')
TableEntry.where(:id.in => tids).pluck(:uid).join(", ")
end
def get_have_data
searchable_field_ids = TableColumn.filter_searchable.where(u_table_id: self.u_table_id).pluck(:id)
searchable_column_entries = self.column_entries.where(:table_column_id.in=> searchable_field_ids).to_a
self.have_data_translations = I18n.available_locales.map do |locale|
flag = searchable_column_entries.detect{|ce| ce.have_data(locale)}.present?
[locale.to_s, flag]
end.to_h
end
def tags_for_frontend
params = OrbitHelper.params
self.table_tags.map{|tt|
"<a class='tag' href='/#{params[:locale]}#{params[:url]}?tag=#{tt.title}'>#" + tt.title + "</a>"
}.join("&nbsp;")
end
def self.u_table
UTable.find(criteria.selector['u_table_id'])
end
def self.get_sort_field(params: nil, table: nil)
field = nil
direction = nil
if table.nil?
table = self.u_table
end
if params
if !params[:sortcolumn].blank?
field = params[:sortcolumn]
direction = params[:sort]
else
field = params[:sort].blank? ? nil : params[:sort]
direction = params[:order].blank? ? 'desc' : params[:order]
end
if field.nil?
field, direction = table.default_ordered
else
field = field.to_s
if !(field=='created_at' || field == 'sort_number')
field = table.table_columns.where(key: field).first || table.table_columns.where(title: field).first
end
end
end
[table, field, direction]
end
def self.sorted(entries: nil, params: nil, table: nil, field: nil, direction: nil, paginated: true)
if field.nil? || direction.nil?
table, field, direction = self.get_sort_field(params: params, table: table)
end
if entries.nil?
entries = table.table_entries
end
if (field=='created_at' || field == 'sort_number')
values = entries.order_by({field => direction})
else
column_to_sort = field
if entries.selector.present?
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id,:table_entry_id.in => entries.pluck(:id)).order_by(column_to_sort.sort_hash(direction))
else
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id).order_by(column_to_sort.sort_hash(direction))
end
values = column_entries.map{|v| v.table_entry}
if paginated
values = Kaminari.paginate_array(values)
end
end
values
end
def self.sorting(params: nil,table: nil,field: nil,direction: nil,page_num: nil,per: nil,column_entries: nil,paginated: true)
page_num = 1 if page_num.blank?
page_num = page_num.to_i
if field.nil? || direction.nil?
table, field, direction = self.get_sort_field(params: params, table: table)
end
if (field=='created_at' || field == 'sort_number')
if column_entries.nil?
values = self.order_by({field => direction})
else
values = column_entries.map{|v| v.table_entry}.compact
values = values.sort_by{|v| v.send(field)}
if direction == 'desc'
values = values.reverse
end
if paginated || !per.nil?
values_count = values.count
values_count = 1 if values_count==0
values = Kaminari.paginate_array(values,limit: values_count)
end
end
if !per.nil?
values = values.page(page_num).per(per)
end
else
column_to_sort = field
if column_entries.nil?
if criteria.selector.keys != ['u_table_id']
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id,:table_entry_id.in => criteria.pluck(:id)).order_by(column_to_sort.sort_hash(direction))
else
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id).order_by(column_to_sort.sort_hash(direction))
end
else
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id,:table_entry_id.in => (column_entries.class==Kaminari::PaginatableArray ? column_entries.map(&:table_entry_id) : column_entries.pluck(:table_entry_id))).order_by(column_to_sort.sort_hash(direction))
end
if !per.nil?
total_count = column_entries.count
column_entries = column_entries.page(page_num).per(per)
offset = page_num==0 ? 0 : (page_num-1)*per
end_offset = (total_count-offset-per)
end_offset = 0 if end_offset<0
values = Kaminari.paginate_array([nil]*offset+column_entries.map{|v| v.table_entry}+[nil]*end_offset).page(page_num).per(per)
else
values = column_entries.map{|v| v.table_entry}
if paginated
values = Kaminari.paginate_array(values)
end
end
end
values
end
end
class TableEntry
include Mongoid::Document
include Mongoid::Timestamps
include OrbitModel::Status
include Slug
attr_accessor :sort_value
field :have_data, type: Boolean, localize: true
field :sort_number, type: Integer
field :view_count, type: Integer, default: 0
field :related_entries, type: String, default: ""
has_many :column_entries, :dependent => :destroy
belongs_to :u_table, index: true
has_and_belongs_to_many :table_tags, inverse_of: :table_entries
accepts_nested_attributes_for :column_entries, :allow_destroy => true
scope :can_display, ->{where(:is_hidden.ne=>true)}
I18n.available_locales.each do |locale|
index({"have_data.#{locale}" => 1}, { unique: false, background: true })
end
before_save do
if self[:sort_number].nil?
other_record = self.class.where(:u_table_id=> self.u_table_id, :id.ne=> self.id).order_by(sort_number: :desc).first
sort_number_to_set = other_record ? other_record.sort_number : 0
self.sort_number = sort_number_to_set.to_i + 1
end
self.get_have_data
end
def fix_have_data
have_data_translations = self.get_have_data
self.class.where(:id=> self.id).update_all(have_data_translations.map{|l, v| ["have_data.#{l}", v]}.to_h)
end
def get_related_entries
tids = self.related_entries.split(',')
TableEntry.find(tids)
end
def get_related_entries_uid
tids = self.related_entries.split(',')
TableEntry.where(:id.in => tids).pluck(:uid).join(", ")
end
def get_have_data
searchable_field_ids = TableColumn.filter_searchable.where(u_table_id: self.u_table_id).pluck(:id)
searchable_column_entries = self.column_entries.where(:table_column_id.in=> searchable_field_ids).to_a
self.have_data_translations = I18n.available_locales.map do |locale|
flag = searchable_column_entries.detect{|ce| ce.have_data(locale)}.present?
[locale.to_s, flag]
end.to_h
end
def tags_for_frontend
params = OrbitHelper.params
self.table_tags.map{|tt|
"<a class='tag' href='/#{params[:locale]}#{params[:url]}?tag=#{tt.title}'>#" + tt.title + "</a>"
}.join("&nbsp;")
end
def self.u_table
UTable.find(criteria.selector['u_table_id'])
end
def self.get_sort_field(params: nil, table: nil)
field = nil
direction = nil
if table.nil?
table = self.u_table
end
if params
if !params[:sortcolumn].blank?
field = params[:sortcolumn]
direction = params[:sort]
else
field = params[:sort].blank? ? nil : params[:sort]
direction = params[:order].blank? ? 'desc' : params[:order]
end
if field.nil?
field, direction = table.default_ordered
else
field = field.to_s
if !(field=='created_at' || field == 'sort_number')
field = table.table_columns.where(key: field).first || table.table_columns.where(title: field).first
end
end
end
[table, field, direction]
end
def self.sorted(entries: nil, params: nil, table: nil, field: nil, direction: nil, paginated: true)
if field.nil? || direction.nil?
table, field, direction = self.get_sort_field(params: params, table: table)
end
if entries.nil?
entries = table.table_entries
end
if (field=='created_at' || field == 'sort_number')
values = entries.order_by({field => direction})
else
column_to_sort = field
if entries.selector.present?
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id,:table_entry_id.in => entries.pluck(:id)).order_by(column_to_sort.sort_hash(direction))
else
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id).order_by(column_to_sort.sort_hash(direction))
end
values = column_entries.map{|v| v.table_entry}
if paginated
values = Kaminari.paginate_array(values)
end
end
values
end
def self.sorting(params: nil,table: nil,field: nil,direction: nil,page_num: nil,per: nil,column_entries: nil,paginated: true)
page_num = 1 if page_num.blank?
page_num = page_num.to_i
if field.nil? || direction.nil?
table, field, direction = self.get_sort_field(params: params, table: table)
end
if (field=='created_at' || field == 'sort_number')
if column_entries.nil?
values = self.order_by({field => direction})
else
values = column_entries.map{|v| v.table_entry}.compact
values = values.sort_by{|v| v.send(field)}
if direction == 'desc'
values = values.reverse
end
if paginated || !per.nil?
values_count = values.count
values_count = 1 if values_count==0
values = Kaminari.paginate_array(values,limit: values_count)
end
end
if !per.nil?
values = values.page(page_num).per(per)
end
else
column_to_sort = field
if column_entries.nil?
if criteria.selector.keys != ['u_table_id']
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id,:table_entry_id.in => criteria.pluck(:id)).order_by(column_to_sort.sort_hash(direction))
else
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id).order_by(column_to_sort.sort_hash(direction))
end
else
column_entries = ColumnEntry.where(:table_column_id=>column_to_sort.id,:table_entry_id.in => (column_entries.class==Kaminari::PaginatableArray ? column_entries.map(&:table_entry_id) : column_entries.pluck(:table_entry_id))).order_by(column_to_sort.sort_hash(direction))
end
if !per.nil?
total_count = column_entries.count
column_entries = column_entries.page(page_num).per(per)
offset = page_num==0 ? 0 : (page_num-1)*per
end_offset = (total_count-offset-per)
end_offset = 0 if end_offset<0
values = Kaminari.paginate_array([nil]*offset+column_entries.map{|v| v.table_entry}+[nil]*end_offset).page(page_num).per(per)
else
values = column_entries.map{|v| v.table_entry}
if paginated
values = Kaminari.paginate_array(values)
end
end
end
values
end
end

16
app/models/table_tag.rb Normal file → Executable file
View File

@ -1,8 +1,8 @@
class TableTag
include Mongoid::Document
include Mongoid::Timestamps
field :title, type: String
field :u_table_id
has_and_belongs_to_many :table_entries, inverse_of: :table_tags
end
class TableTag
include Mongoid::Document
include Mongoid::Timestamps
field :title, type: String
field :u_table_id
has_and_belongs_to_many :table_entries, inverse_of: :table_tags
end

84
app/models/u_table.rb Normal file → Executable file
View File

@ -1,42 +1,42 @@
class UTable
include Mongoid::Document
include Mongoid::Timestamps
include OrbitCategory::Categorizable
include Slug
field :title, as: :slug_title, localize: true
field :ordered_with_sort_number, type: Boolean, default: false
field :sort_number_order_direction, type: String, default: 'desc'
field :ordered_with_created_at, type: Boolean, default: true
field :created_at_order_direction, type: String, default: 'desc'
has_many :table_columns, :dependent => :destroy
has_many :table_entries, :dependent => :destroy
has_many :mind_maps, :dependent => :destroy
accepts_nested_attributes_for :table_columns, :allow_destroy => true
FIELD_TYPES = ["text", "integer", "editor", "image", "date", "period", "file"]
DATE_FORMATS = ["yyyy/MM/dd hh:mm", "yyyy/MM/dd","yyyy/MM", "yyyy"]
AUDIO_EXTENSIONS = %w[.mp3 .wav .ogg .m4a .aac .flac]
def default_ordered
if self.ordered_with_created_at
sort_column = 'created_at'
direction = self.created_at_order_direction
elsif self.ordered_with_sort_number
sort_column = 'sort_number'
direction = self.sort_number_order_direction
else
sort_column = self.table_columns.where(default_ordered_field: true).first
if sort_column
direction = sort_column.order_direction
else
sort_column = 'created_at'
direction = self.created_at_order_direction
end
end
[sort_column,direction]
end
end
class UTable
include Mongoid::Document
include Mongoid::Timestamps
include OrbitCategory::Categorizable
include Slug
field :title, as: :slug_title, localize: true
field :ordered_with_sort_number, type: Boolean, default: false
field :sort_number_order_direction, type: String, default: 'desc'
field :ordered_with_created_at, type: Boolean, default: true
field :created_at_order_direction, type: String, default: 'desc'
has_many :table_columns, :dependent => :destroy
has_many :table_entries, :dependent => :destroy
has_many :mind_maps, :dependent => :destroy
accepts_nested_attributes_for :table_columns, :allow_destroy => true
FIELD_TYPES = ["text", "integer", "editor", "image", "date", "period", "file"]
DATE_FORMATS = ["yyyy/MM/dd hh:mm", "yyyy/MM/dd","yyyy/MM", "yyyy"]
AUDIO_EXTENSIONS = %w[.mp3 .wav .ogg .m4a .aac .flac]
def default_ordered
if self.ordered_with_created_at
sort_column = 'created_at'
direction = self.created_at_order_direction
elsif self.ordered_with_sort_number
sort_column = 'sort_number'
direction = self.sort_number_order_direction
else
sort_column = self.table_columns.where(default_ordered_field: true).first
if sort_column
direction = sort_column.order_direction
else
sort_column = 'created_at'
direction = self.created_at_order_direction
end
end
[sort_column,direction]
end
end

88
app/views/admin/mind_maps/_form.html.erb Normal file → Executable file
View File

@ -1,45 +1,45 @@
<fieldset class="utable-heading-wrap">
<div class="utable-heading-header">
<h4><%= t("universal_table.table_name") %> - <%= @table.title %></h4>
</div>
<div class="control-group">
<div class="controls">
<div class="input-append">
<div class="tab-content">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<div class="tab-pane fade <%= active %>" id="mind_map_<%= locale.to_s %>">
<%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :placeholder => "Title", :value => @mind_map.title_translations[locale] %>
<% end %>
</div>
<% end %>
</div>
<div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<%= link_to t(locale).to_s,"#mind_map_#{locale.to_s}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="utable-heading-wrap">
<div class="utable-heading-header">
<h4><%= t("universal_table.mind_map") %></h4>
</div>
<div class="control-group">
<div class="controls">
<button id="toggle_editable"><%= t("universal_table.disable_editing") %></button>
</div>
<div id="jsmind_container"></div>
</div>
</fieldset>
<fieldset class="utable-content">
<div class="form-actions">
<%= f.hidden_field :mind_map_data, id: "mind_map_data_field", value: "[]" %>
<%= f.hidden_field :u_table_id, value: @table.id %>
<input class="btn btn-primary pull-right" name="commit" type="submit" value="<%= t("save") %>">
</div>
<fieldset class="utable-heading-wrap">
<div class="utable-heading-header">
<h4><%= t("universal_table.table_name") %> - <%= @table.title %></h4>
</div>
<div class="control-group">
<div class="controls">
<div class="input-append">
<div class="tab-content">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<div class="tab-pane fade <%= active %>" id="mind_map_<%= locale.to_s %>">
<%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :placeholder => "Title", :value => @mind_map.title_translations[locale] %>
<% end %>
</div>
<% end %>
</div>
<div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<%= link_to t(locale).to_s,"#mind_map_#{locale.to_s}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="utable-heading-wrap">
<div class="utable-heading-header">
<h4><%= t("universal_table.mind_map") %></h4>
</div>
<div class="control-group">
<div class="controls">
<button id="toggle_editable"><%= t("universal_table.disable_editing") %></button>
</div>
<div id="jsmind_container"></div>
</div>
</fieldset>
<fieldset class="utable-content">
<div class="form-actions">
<%= f.hidden_field :mind_map_data, id: "mind_map_data_field", value: "[]" %>
<%= f.hidden_field :u_table_id, value: @table.id %>
<input class="btn btn-primary pull-right" name="commit" type="submit" value="<%= t("save") %>">
</div>
</fieldset>

64
app/views/admin/mind_maps/_index.html.erb Normal file → Executable file
View File

@ -1,33 +1,33 @@
<table class="table main-list">
<thead>
<tr class="sort-header">
<% @table_fields.each do |f| %>
<%= thead(f) %>
<% end %>
</tr>
</thead>
<tbody>
<% @mind_maps.each do |mindmap| %>
<tr id="mindmap_<%= mindmap.id.to_s %>">
<td>
<a href="<%= admin_mind_map_path(mindmap) %>"><%= mindmap.title %></a>
<div class="quick-edit">
<ul class="nav nav-pills">
<li><a href="<%= edit_admin_mind_map_path(mindmap) %>"><%= t(:edit) %></a></li>
<li><a href="<%= admin_mind_map_path(mindmap) %>" class="delete text-error" data-method="delete" data-confirm="Are you sure?"><%= t(:delete_) %></a></li>
</ul>
</div>
</td>
<td>
<%= mindmap.created_at.strftime("%Y-%m-%d") %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%=
content_tag :div, class: "bottomnav clearfix" do
content_tag(:div, paginate(@mind_maps), class: "pagination pagination-centered") +
content_tag(:div, link_to(t(:new_),new_admin_mind_map_path(:table => @table.id.to_s), :class=>"btn btn-primary"), class: "pull-right")
end
<table class="table main-list">
<thead>
<tr class="sort-header">
<% @table_fields.each do |f| %>
<%= thead(f) %>
<% end %>
</tr>
</thead>
<tbody>
<% @mind_maps.each do |mindmap| %>
<tr id="mindmap_<%= mindmap.id.to_s %>">
<td>
<a href="<%= admin_mind_map_path(mindmap) %>"><%= mindmap.title %></a>
<div class="quick-edit">
<ul class="nav nav-pills">
<li><a href="<%= edit_admin_mind_map_path(mindmap) %>"><%= t(:edit) %></a></li>
<li><a href="<%= admin_mind_map_path(mindmap) %>" class="delete text-error" data-method="delete" data-confirm="Are you sure?"><%= t(:delete_) %></a></li>
</ul>
</div>
</td>
<td>
<%= mindmap.created_at.strftime("%Y-%m-%d") %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%=
content_tag :div, class: "bottomnav clearfix" do
content_tag(:div, paginate(@mind_maps), class: "pagination pagination-centered") +
content_tag(:div, link_to(t(:new_),new_admin_mind_map_path(:table => @table.id.to_s), :class=>"btn btn-primary"), class: "pull-right")
end
%>

186
app/views/admin/mind_maps/edit.html.erb Normal file → Executable file
View File

@ -1,93 +1,93 @@
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<%= stylesheet_link_tag "mind_map/mindmap" %>
<% end %>
<%= form_for @mind_map, url: admin_mind_map_path(@mind_map.id), html: {class: "form-horizontal main-forms", id: "mind_map_form"} do |f| %>
<%= render :partial => "form", locals: {f: f} %>
<% end %>
<script type="module">
import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯
// Control whether the mind map is editable
let isEditable = true
// 心智圖實例
// Mind map instance
let jm
// 心智圖初始數據
// Initial mind map data
let mind = {
meta: {},
format: 'node_array',
data: <%= raw @mind_map.mind_map_data.to_json %>
}
// 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation)
const options = {
container: 'jsmind_container',
editable: isEditable,
theme: 'primary',
mode: 'full',
tableUID: '<%= @table.uid %>',
text: {
addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>"
},
view: {
engine: 'svg',
draggable: true,
node_overflow: 'wrap',
},
shortcut: {
mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13,
},
},
}
// 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable)
// 儲存當前數據
// Save the current data
// document.getElementById('save_mind_map').addEventListener('click', (e) => {
// e.preventDefault();
// e.stopPropagation();
// let data = getJsmindData(jm);
// console.log(data);
// })
// 調整可編輯狀態
// Toggle the editable state
document.getElementById('toggle_editable').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
isEditable = !isEditable;
e.target.innerHTML = isEditable ? '<%= t("universal_table.disable_editing") %>' : '<%= t("universal_table.enable_editing") %>';
mind = getJsmindData(jm);
jm = initJsmind(mind, options, isEditable);
return false;
})
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("mind_map_form");
const hiddenField = document.getElementById("mind_map_data_field");
form.addEventListener("submit", function (e) {
const mindMapData = getJsmindData(jm);
console.log(mindMapData);
hiddenField.value = JSON.stringify(mindMapData.data);
});
});
</script>
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<%= stylesheet_link_tag "mind_map/mindmap" %>
<% end %>
<%= form_for @mind_map, url: admin_mind_map_path(@mind_map.id), html: {class: "form-horizontal main-forms", id: "mind_map_form"} do |f| %>
<%= render :partial => "form", locals: {f: f} %>
<% end %>
<script type="module">
import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯
// Control whether the mind map is editable
let isEditable = true
// 心智圖實例
// Mind map instance
let jm
// 心智圖初始數據
// Initial mind map data
let mind = {
meta: {},
format: 'node_array',
data: <%= raw @mind_map.mind_map_data.to_json %>
}
// 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation)
const options = {
container: 'jsmind_container',
editable: isEditable,
theme: 'primary',
mode: 'full',
tableUID: '<%= @table.uid %>',
text: {
addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>"
},
view: {
engine: 'svg',
draggable: true,
node_overflow: 'wrap',
},
shortcut: {
mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13,
},
},
}
// 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable)
// 儲存當前數據
// Save the current data
// document.getElementById('save_mind_map').addEventListener('click', (e) => {
// e.preventDefault();
// e.stopPropagation();
// let data = getJsmindData(jm);
// console.log(data);
// })
// 調整可編輯狀態
// Toggle the editable state
document.getElementById('toggle_editable').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
isEditable = !isEditable;
e.target.innerHTML = isEditable ? '<%= t("universal_table.disable_editing") %>' : '<%= t("universal_table.enable_editing") %>';
mind = getJsmindData(jm);
jm = initJsmind(mind, options, isEditable);
return false;
})
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("mind_map_form");
const hiddenField = document.getElementById("mind_map_data_field");
form.addEventListener("submit", function (e) {
const mindMapData = getJsmindData(jm);
console.log(mindMapData);
hiddenField.value = JSON.stringify(mindMapData.data);
});
});
</script>

10
app/views/admin/mind_maps/index.html.erb Normal file → Executable file
View File

@ -1,6 +1,6 @@
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/jquery.form" %>
<% end %>
<div id="index_table">
<%= render 'index'%>
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/jquery.form" %>
<% end %>
<div id="index_table">
<%= render 'index'%>
</div>

178
app/views/admin/mind_maps/new.html.erb Normal file → Executable file
View File

@ -1,89 +1,89 @@
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<%= stylesheet_link_tag "mind_map/mindmap" %>
<% end %>
<%= form_for @mind_map, url: admin_mind_maps_path, html: {class: "form-horizontal main-forms", id: "mind_map_form"} do |f| %>
<%= render :partial => "form", locals: {f: f} %>
<% end %>
<script type="module">
import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯
// Control whether the mind map is editable
let isEditable = true
// 心智圖實例
// Mind map instance
let jm
// 心智圖初始數據
// Initial mind map data
let mind = INITIAL_MIND
// 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation)
const options = {
container: 'jsmind_container',
editable: isEditable,
theme: 'primary',
mode: 'full',
tableUID: '<%= @table.uid %>',
text: {
addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>"
},
view: {
engine: 'svg',
draggable: true,
node_overflow: 'wrap',
},
shortcut: {
mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13,
},
},
}
// 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable)
// 儲存當前數據
// Save the current data
// document.getElementById('save_mind_map').addEventListener('click', (e) => {
// e.preventDefault();
// e.stopPropagation();
// let data = getJsmindData(jm);
// console.log(data);
// })
// 調整可編輯狀態
// Toggle the editable state
document.getElementById('toggle_editable').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
isEditable = !isEditable;
e.target.innerHTML = isEditable ? '<%= t("universal_table.disable_editing") %>' : '<%= t("universal_table.enable_editing") %>';
mind = getJsmindData(jm);
jm = initJsmind(mind, options, isEditable);
return false;
})
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("mind_map_form");
const hiddenField = document.getElementById("mind_map_data_field");
form.addEventListener("submit", function (e) {
const mindMapData = getJsmindData(jm);
console.log(mindMapData);
hiddenField.value = JSON.stringify(mindMapData.data);
});
});
</script>
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<%= stylesheet_link_tag "mind_map/mindmap" %>
<% end %>
<%= form_for @mind_map, url: admin_mind_maps_path, html: {class: "form-horizontal main-forms", id: "mind_map_form"} do |f| %>
<%= render :partial => "form", locals: {f: f} %>
<% end %>
<script type="module">
import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯
// Control whether the mind map is editable
let isEditable = true
// 心智圖實例
// Mind map instance
let jm
// 心智圖初始數據
// Initial mind map data
let mind = INITIAL_MIND
// 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation)
const options = {
container: 'jsmind_container',
editable: isEditable,
theme: 'primary',
mode: 'full',
tableUID: '<%= @table.uid %>',
text: {
addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>"
},
view: {
engine: 'svg',
draggable: true,
node_overflow: 'wrap',
},
shortcut: {
mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13,
},
},
}
// 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable)
// 儲存當前數據
// Save the current data
// document.getElementById('save_mind_map').addEventListener('click', (e) => {
// e.preventDefault();
// e.stopPropagation();
// let data = getJsmindData(jm);
// console.log(data);
// })
// 調整可編輯狀態
// Toggle the editable state
document.getElementById('toggle_editable').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
isEditable = !isEditable;
e.target.innerHTML = isEditable ? '<%= t("universal_table.disable_editing") %>' : '<%= t("universal_table.enable_editing") %>';
mind = getJsmindData(jm);
jm = initJsmind(mind, options, isEditable);
return false;
})
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("mind_map_form");
const hiddenField = document.getElementById("mind_map_data_field");
form.addEventListener("submit", function (e) {
const mindMapData = getJsmindData(jm);
console.log(mindMapData);
hiddenField.value = JSON.stringify(mindMapData.data);
});
});
</script>

202
app/views/admin/universal_tables/_column.html.erb Normal file → Executable file
View File

@ -1,102 +1,102 @@
<% if !defined?(i) %>
<div class="attributes">
<% end %>
<div class="attributes-header clearfix">
<a class="btn btn-mini pull-right btn-danger delete" href="#"><i class="icon-trash"></i> Delete</a>
<% if defined?(i) %>
<%= f.hidden_field :_destroy, :value => "false", :class => "attribute_field_to_delete" %>
<h4 class="draggable"><i class="icons-list-2"></i> <%= column.title %></h4>
<% else %>
<h4 class="draggable"><i class="icons-list-2"></i> ColumnXX</h4>
<% end %>
</div>
<div class="attributes-body">
<div class="control-group">
<label class="control-label muted" for="key_0">Key</label>
<div class="controls">
<%= f.text_field :key, :autocomplete => "off", :'data-type' => 'key' %>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for="">Title</label>
<div class="controls">
<div class="input-append">
<div class="tab-content">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<% id = (defined?(i) ? "table_column_#{i}_title_translations_#{locale.to_s}" : "table_column_XXX_title_translations_#{locale.to_s}") %>
<div class="tab-pane fade in <%= active %>" id="<%= id %>">
<%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :value => column.title_translations[locale] %>
<% end %>
</div>
<% end %>
</div>
<div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<% id = (defined?(i) ? "table_column_#{i}_title_translations_#{locale.to_s}" : "table_column_XXX_title_translations_#{locale.to_s}") %>
<%= link_to t(locale).to_s,"##{id}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %>
</div>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for="">Display in index</label>
<div class="controls">
<label class="radio inline">
<%= f.radio_button :display_in_index, "true" %>Yes
</label>
<label class="radio inline">
<%= f.radio_button :display_in_index, "false" %>No
</label>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.default_ordered_field') %></label>
<div class="controls">
<div>
<%= f.check_box :default_ordered_field, class: 'default_ordered_field' %>
</div>
<div class="order_direction<%= ' hidden' if !f.object.default_ordered_field %>">
<%= f.select :order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for="">Type</label>
<div class="controls">
<% select_values = UTable::FIELD_TYPES.collect{|ft| [ft.capitalize,ft]} %>
<%= f.select :type, select_values, {}, {class: "type-selector"} %>
<span class="link_to_show <%= (!defined?(i) || column.type == "text" || column.type == "integer") ? "" : "hide" %>">
<label class="checkbox inline attributes-checkbox ">
<%= f.check_box :is_link_to_show %> Link to show
</label>
<label class="checkbox inline attributes-checkbox">
<%= f.check_box :make_categorizable %> Categorizable
</label>
<label class="checkbox inline attributes-checkbox ">
<%= f.check_box :is_searchable %> Searchable
</label>
</span>
<% select_values = UTable::DATE_FORMATS.collect{|ft| [ft.upcase,ft]} %>
<label class="checkbox date_format inline attributes-checkbox <%= column.type == "date" || column.type == "period" ? "" : "hide" %>">
Date Format <%= f.select :date_format, select_values%>
</label>
</div>
</div>
<% if defined?(i) %>
<% if column.order.nil? %>
<%= f.hidden_field :order, :value => i, :class => "order-hidden-field" %>
<% else %>
<%= f.hidden_field :order, :class => "order-hidden-field" %>
<% end %>
<% else %>
<%= f.hidden_field :order, :value=> "XXX", :class => "order-hidden-field" %>
<% end %>
</div>
<% if !defined?(i) %>
</div>
<% if !defined?(i) %>
<div class="attributes">
<% end %>
<div class="attributes-header clearfix">
<a class="btn btn-mini pull-right btn-danger delete" href="#"><i class="icon-trash"></i> Delete</a>
<% if defined?(i) %>
<%= f.hidden_field :_destroy, :value => "false", :class => "attribute_field_to_delete" %>
<h4 class="draggable"><i class="icons-list-2"></i> <%= column.title %></h4>
<% else %>
<h4 class="draggable"><i class="icons-list-2"></i> ColumnXX</h4>
<% end %>
</div>
<div class="attributes-body">
<div class="control-group">
<label class="control-label muted" for="key_0">Key</label>
<div class="controls">
<%= f.text_field :key, :autocomplete => "off", :'data-type' => 'key' %>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for="">Title</label>
<div class="controls">
<div class="input-append">
<div class="tab-content">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<% id = (defined?(i) ? "table_column_#{i}_title_translations_#{locale.to_s}" : "table_column_XXX_title_translations_#{locale.to_s}") %>
<div class="tab-pane fade in <%= active %>" id="<%= id %>">
<%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :value => column.title_translations[locale] %>
<% end %>
</div>
<% end %>
</div>
<div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<% id = (defined?(i) ? "table_column_#{i}_title_translations_#{locale.to_s}" : "table_column_XXX_title_translations_#{locale.to_s}") %>
<%= link_to t(locale).to_s,"##{id}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %>
</div>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for="">Display in index</label>
<div class="controls">
<label class="radio inline">
<%= f.radio_button :display_in_index, "true" %>Yes
</label>
<label class="radio inline">
<%= f.radio_button :display_in_index, "false" %>No
</label>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.default_ordered_field') %></label>
<div class="controls">
<div>
<%= f.check_box :default_ordered_field, class: 'default_ordered_field' %>
</div>
<div class="order_direction<%= ' hidden' if !f.object.default_ordered_field %>">
<%= f.select :order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for="">Type</label>
<div class="controls">
<% select_values = UTable::FIELD_TYPES.collect{|ft| [ft.capitalize,ft]} %>
<%= f.select :type, select_values, {}, {class: "type-selector"} %>
<span class="link_to_show <%= (!defined?(i) || column.type == "text" || column.type == "integer") ? "" : "hide" %>">
<label class="checkbox inline attributes-checkbox ">
<%= f.check_box :is_link_to_show %> Link to show
</label>
<label class="checkbox inline attributes-checkbox">
<%= f.check_box :make_categorizable %> Categorizable
</label>
<label class="checkbox inline attributes-checkbox ">
<%= f.check_box :is_searchable %> Searchable
</label>
</span>
<% select_values = UTable::DATE_FORMATS.collect{|ft| [ft.upcase,ft]} %>
<label class="checkbox date_format inline attributes-checkbox <%= column.type == "date" || column.type == "period" ? "" : "hide" %>">
Date Format <%= f.select :date_format, select_values%>
</label>
</div>
</div>
<% if defined?(i) %>
<% if column.order.nil? %>
<%= f.hidden_field :order, :value => i, :class => "order-hidden-field" %>
<% else %>
<%= f.hidden_field :order, :class => "order-hidden-field" %>
<% end %>
<% else %>
<%= f.hidden_field :order, :value=> "XXX", :class => "order-hidden-field" %>
<% end %>
</div>
<% if !defined?(i) %>
</div>
<% end %>

36
app/views/admin/universal_tables/_date_field.html.erb Normal file → Executable file
View File

@ -1,18 +1,18 @@
<div class="control-group">
<%= f.label :date, column.title, :class => "control-label" %>
<div class="controls">
<div>
<div class="default_picker input-append" style="">
<% v = !date_field.new_record? ? format_date(date_field.date, column.date_format, true) : "" %>
<%= f.text_field :date, :value => v, :placeholder => column.date_format.upcase, :data => {:format => (column.date_format == "yyyy" ? "yyyy/MM" : column.date_format)} %>
<span class="add-on clearDate"><i class="icons-cross-3"></i></span>
<span class="add-on iconbtn"><i data-date-icon="icons-calendar" data-time-icon="icons-clock" class="icons-calendar"></i></span>
</div>
</div>
</div>
<% if !date_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>
<div class="control-group">
<%= f.label :date, column.title, :class => "control-label" %>
<div class="controls">
<div>
<div class="default_picker input-append" style="">
<% v = !date_field.new_record? ? format_date(date_field.date, column.date_format, true) : "" %>
<%= f.text_field :date, :value => v, :placeholder => column.date_format.upcase, :data => {:format => (column.date_format == "yyyy" ? "yyyy/MM" : column.date_format)} %>
<span class="add-on clearDate"><i class="icons-cross-3"></i></span>
<span class="add-on iconbtn"><i data-date-icon="icons-calendar" data-time-icon="icons-clock" class="icons-calendar"></i></span>
</div>
</div>
</div>
<% if !date_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>

158
app/views/admin/universal_tables/_edit_sort.html.erb Normal file → Executable file
View File

@ -1,79 +1,79 @@
<div id="data-table" class="ut-table">
<table class="table main-list">
<thead>
<tr class="sort-header">
<% @table_fields.each do |field| %>
<%
field_text = field.to_s.include?('.') ? t(field.to_s) : field.to_s
sort = field.to_s.split('.')[-1]
active = params[:sort].eql? sort
order = active ? (["asc", "desc"]-[params[:order]]).first : "asc"
arrow = (order.eql? "desc") ? "<b class='icons-arrow-up-3'></b>" : "<b class='icons-arrow-down-4'></b>"
klass = field.eql?(:title) ? "span5" : "span2"
th_data = "<a href='?sort=#{sort}&order=#{order}'>#{field_text} #{active ? arrow : ""}</a>"
%>
<th class='<%= klass %> <%= active ? "active" : "" %>'><%= th_data.html_safe %></th>
<% end %>
</tr>
</thead>
<tbody id="sortable">
<% can_edit = can_edit_or_delete?(@entries.first.u_table) if !(@entries.first.nil?) %>
<% @entries.each do |entry| %>
<tr data-id="<%= entry.id %>">
<td>
<%= number_field_tag nil,entry.sort_number,class: 'sort_number',step: 1 %>
</td>
<% @columns.each_with_index do |column, index| %>
<% ce = entry.column_entries.where(:table_column_id => column.id).first rescue nil %>
<% if !ce.nil? %>
<td>
<% case ce.type %>
<% when "text" %>
<%= ce.text %>
<% when "integer" %>
<%= ce.number %>
<% when "editor" %>
<%= ce.content.html_safe rescue "" %>
<% when "image" %>
<div class="image-expander">
<% if !ce.image.nil? %>
<a href="<%= ce.image.url %>" target="_blank"><img src="<%= ce.image.thumb.url %>" class="image-preview" /></a>
<% end %>
</div>
<% when "date" %>
<%= format_date(ce.date, column.date_format) %>
<% when "period" %>
<% if !ce.period_from.nil? %>
<%= format_date(ce.period_from, column.date_format) %> ~ <%= format_date(ce.period_to, column.date_format) %>
<% end %>
<% end %>
<% if index == 0 && can_edit %>
<div class="quick-edit">
<ul class="nav nav-pills">
<li><a href="<%= admin_universal_table_edit_entry_path(entry) %>"><%= t(:edit) %></a></li>
<li><a href="<%= admin_universal_table_delete_entry_path(entry.id) %>" class="delete text-error" data-method="delete" data-confirm="Are you sure?"><%= t(:delete_) %></a></li>
</ul>
</div>
<% end %>
</td>
<% else %>
<td>
&nbsp;
</td>
<% end %>
<% end %>
<td>
<%= entry.created_at %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div id="data-table" class="ut-table">
<table class="table main-list">
<thead>
<tr class="sort-header">
<% @table_fields.each do |field| %>
<%
field_text = field.to_s.include?('.') ? t(field.to_s) : field.to_s
sort = field.to_s.split('.')[-1]
active = params[:sort].eql? sort
order = active ? (["asc", "desc"]-[params[:order]]).first : "asc"
arrow = (order.eql? "desc") ? "<b class='icons-arrow-up-3'></b>" : "<b class='icons-arrow-down-4'></b>"
klass = field.eql?(:title) ? "span5" : "span2"
th_data = "<a href='?sort=#{sort}&order=#{order}'>#{field_text} #{active ? arrow : ""}</a>"
%>
<th class='<%= klass %> <%= active ? "active" : "" %>'><%= th_data.html_safe %></th>
<% end %>
</tr>
</thead>
<tbody id="sortable">
<% can_edit = can_edit_or_delete?(@entries.first.u_table) if !(@entries.first.nil?) %>
<% @entries.each do |entry| %>
<tr data-id="<%= entry.id %>">
<td>
<%= number_field_tag nil,entry.sort_number,class: 'sort_number',step: 1 %>
</td>
<% @columns.each_with_index do |column, index| %>
<% ce = entry.column_entries.where(:table_column_id => column.id).first rescue nil %>
<% if !ce.nil? %>
<td>
<% case ce.type %>
<% when "text" %>
<%= ce.text %>
<% when "integer" %>
<%= ce.number %>
<% when "editor" %>
<%= ce.content.html_safe rescue "" %>
<% when "image" %>
<div class="image-expander">
<% if !ce.image.nil? %>
<a href="<%= ce.image.url %>" target="_blank"><img src="<%= ce.image.thumb.url %>" class="image-preview" /></a>
<% end %>
</div>
<% when "date" %>
<%= format_date(ce.date, column.date_format) %>
<% when "period" %>
<% if !ce.period_from.nil? %>
<%= format_date(ce.period_from, column.date_format) %> ~ <%= format_date(ce.period_to, column.date_format) %>
<% end %>
<% end %>
<% if index == 0 && can_edit %>
<div class="quick-edit">
<ul class="nav nav-pills">
<li><a href="<%= admin_universal_table_edit_entry_path(entry) %>"><%= t(:edit) %></a></li>
<li><a href="<%= admin_universal_table_delete_entry_path(entry.id) %>" class="delete text-error" data-method="delete" data-confirm="Are you sure?"><%= t(:delete_) %></a></li>
</ul>
</div>
<% end %>
</td>
<% else %>
<td>
&nbsp;
</td>
<% end %>
<% end %>
<td>
<%= entry.created_at %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>

62
app/views/admin/universal_tables/_editor_field.html.erb Normal file → Executable file
View File

@ -1,31 +1,31 @@
<!-- Language Tabs -->
<ul class="nav nav-pills language-nav">
<% @site_in_use_locales.each_with_index do |locale| %>
<% id = "table_entry_column_entries_#{i}_content_translations_#{locale.to_s}" %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<li class="<%= active %>">
<a data-toggle="tab" href=".<%= id %>"><%= t(locale) %></a>
</li>
<% end %>
</ul>
<div class="tab-content language-area">
<% @site_in_use_locales.each_with_index do |locale| %>
<% id = "table_entry_column_entries_#{i}_content_translations_#{locale.to_s}" %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<div class="<%= id %> tab-pane fade <%= active %>">
<div class="control-group">
<%= f.label :content, column.title, :class => "control-label" %>
<div class="controls">
<%= f.fields_for :content_translations do |f| %>
<%= f.text_area locale, :value => editor_field.content_translations[locale.to_s], :class => "ckeditor" %>
<% end %>
</div>
</div>
</div>
<% end %>
<% if !editor_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>
<!-- Language Tabs -->
<ul class="nav nav-pills language-nav">
<% @site_in_use_locales.each_with_index do |locale| %>
<% id = "table_entry_column_entries_#{i}_content_translations_#{locale.to_s}" %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<li class="<%= active %>">
<a data-toggle="tab" href=".<%= id %>"><%= t(locale) %></a>
</li>
<% end %>
</ul>
<div class="tab-content language-area">
<% @site_in_use_locales.each_with_index do |locale| %>
<% id = "table_entry_column_entries_#{i}_content_translations_#{locale.to_s}" %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<div class="<%= id %> tab-pane fade <%= active %>">
<div class="control-group">
<%= f.label :content, column.title, :class => "control-label" %>
<div class="controls">
<%= f.fields_for :content_translations do |f| %>
<%= f.text_area locale, :value => editor_field.content_translations[locale.to_s], :class => "ckeditor" %>
<% end %>
</div>
</div>
</div>
<% end %>
<% if !editor_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>

220
app/views/admin/universal_tables/_entry_form.html.erb Normal file → Executable file
View File

@ -1,111 +1,111 @@
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<%= stylesheet_link_tag "lib/main-forms" %>
<%= stylesheet_link_tag "lib/fileupload" %>
<%= stylesheet_link_tag "lib/main-list" %>
<%= stylesheet_link_tag "select2/select2" %>
<%= javascript_include_tag "select2/select2.min" %>
<% end %>
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/bootstrap-fileupload" %>
<%= javascript_include_tag "lib/bootstrap-datetimepicker" %>
<%= javascript_include_tag "lib/datetimepicker/datetimepicker.js" %>
<% end %>
<style type="text/css">
#s2id_autogen1{
width: 500px !important;
}
#s2id_autogen2 {
width: 500px !important;
}
</style>
<div class="input-area">
<div class="control-group">
<label class="control-label"><%= t("universal_table.hashtags") %></label>
<div class="controls">
<input id="universal_table_tags" name="table_tags" />
</div>
</div>
<%
tbData = @entry.get_related_entries.map{|tb| {id: tb.id.to_s, text: tb.column_entries.first.text}}
%>
<div class="control-group">
<label class="control-label"><%= t("universal_table.related_entries") %></label>
<div class="controls">
<%= f.text_field :related_entries, :class => "select2", data: { value: tbData } %>
</div>
</div>
<% @columns.each_with_index do |column, index| %>
<% if @entry.new_record? %>
<% object = f.object.send(:column_entries).build rescue nil %>
<% else %>
<%
ce = @entry.column_entries.where(:table_column_id => column.id).first rescue nil
if ce.nil?
object = f.object.send(:column_entries).build rescue nil
else
object = ce
end
%>
<% end %>
<%= f.fields_for :column_entries, object, :child_index => index do |f| %>
<%= render :partial => "#{column.type}_field", :object => object, :locals => {:f => f, :column => column, :i => index} %>
<% end %>
<% end %>
</div>
<div class="form-actions">
<a href="<%= admin_universal_table_path(@table) %>" class="btn">View Entries</a>
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
<script>
$("#universal_table_tags").select2({
tags: true,
multiple: true,
data : <%= raw(TableTag.where(:u_table_id => @table.id).map { |tag| { text: tag.title.html_safe, id: tag.id.to_s } }.to_json) %>,
createSearchChoice: function(term, data) {
if (!data.length)
return { id: term, text: "#" + term.trim().toLowerCase() };
}
});
$("#universal_table_tags").val(<%= raw(@entry.table_tags. collect { |tag| tag.id.to_s }) %>).trigger("change");
const preselectedData = $("#table_entry_related_entries").data('value');
var select2Options = {
maximumSelectionSize: 10,
minimumResultsForSearch: Infinity,
multiple: true,
minimumInputLength: 2,
width: '100%',
placeholder: "<%= t("universal_table.search_entries") %>",
ajax: {
type: 'get',
url: "/admin/universal_tables/get_entries?uid=<%= @entry.u_table.uid rescue @table.uid %>",
allowClear: false,
dataType: 'json',
delay: 250,
data: function (term) {
return { q: term };
},
results: function(data, page) {
return {
results: data
};
},
cache: false
},
formatResult: function(i) {
return '<div>' + i.text + '</div>';
},
formatSelection: function(i) {
return '<div>' + i.text + '</div>';
},
escapeMarkup: function(m) {
return m;
}
}
$("#table_entry_related_entries").select2(select2Options);
$("#table_entry_related_entries").select2('data', preselectedData);
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<%= stylesheet_link_tag "lib/main-forms" %>
<%= stylesheet_link_tag "lib/fileupload" %>
<%= stylesheet_link_tag "lib/main-list" %>
<%= stylesheet_link_tag "select2/select2" %>
<%= javascript_include_tag "select2/select2.min" %>
<% end %>
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/bootstrap-fileupload" %>
<%= javascript_include_tag "lib/bootstrap-datetimepicker" %>
<%= javascript_include_tag "lib/datetimepicker/datetimepicker.js" %>
<% end %>
<style type="text/css">
#s2id_autogen1{
width: 500px !important;
}
#s2id_autogen2 {
width: 500px !important;
}
</style>
<div class="input-area">
<div class="control-group">
<label class="control-label"><%= t("universal_table.hashtags") %></label>
<div class="controls">
<input id="universal_table_tags" name="table_tags" />
</div>
</div>
<%
tbData = @entry.get_related_entries.map{|tb| {id: tb.id.to_s, text: tb.column_entries.first.text}}
%>
<div class="control-group">
<label class="control-label"><%= t("universal_table.related_entries") %></label>
<div class="controls">
<%= f.text_field :related_entries, :class => "select2", data: { value: tbData } %>
</div>
</div>
<% @columns.each_with_index do |column, index| %>
<% if @entry.new_record? %>
<% object = f.object.send(:column_entries).build rescue nil %>
<% else %>
<%
ce = @entry.column_entries.where(:table_column_id => column.id).first rescue nil
if ce.nil?
object = f.object.send(:column_entries).build rescue nil
else
object = ce
end
%>
<% end %>
<%= f.fields_for :column_entries, object, :child_index => index do |f| %>
<%= render :partial => "#{column.type}_field", :object => object, :locals => {:f => f, :column => column, :i => index} %>
<% end %>
<% end %>
</div>
<div class="form-actions">
<a href="<%= admin_universal_table_path(@table) %>" class="btn">View Entries</a>
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
<script>
$("#universal_table_tags").select2({
tags: true,
multiple: true,
data : <%= raw(TableTag.where(:u_table_id => @table.id).map { |tag| { text: tag.title.html_safe, id: tag.id.to_s } }.to_json) %>,
createSearchChoice: function(term, data) {
if (!data.length)
return { id: term, text: "#" + term.trim().toLowerCase() };
}
});
$("#universal_table_tags").val(<%= raw(@entry.table_tags. collect { |tag| tag.id.to_s }) %>).trigger("change");
const preselectedData = $("#table_entry_related_entries").data('value');
var select2Options = {
maximumSelectionSize: 10,
minimumResultsForSearch: Infinity,
multiple: true,
minimumInputLength: 2,
width: '100%',
placeholder: "<%= t("universal_table.search_entries") %>",
ajax: {
type: 'get',
url: "/admin/universal_tables/get_entries?uid=<%= @entry.u_table.uid rescue @table.uid %>",
allowClear: false,
dataType: 'json',
delay: 250,
data: function (term) {
return { q: term };
},
results: function(data, page) {
return {
results: data
};
},
cache: false
},
formatResult: function(i) {
return '<div>' + i.text + '</div>';
},
formatSelection: function(i) {
return '<div>' + i.text + '</div>';
},
escapeMarkup: function(m) {
return m;
}
}
$("#table_entry_related_entries").select2(select2Options);
$("#table_entry_related_entries").select2('data', preselectedData);
</script>

View File

@ -1,35 +1,35 @@
<div class="entry-suggestion">
<strong><%= entry.column_entries.first.try(:text).to_s %></strong>
<% entry.column_entries.each do |ce| %>
<% if ce.type == "file" %>
<ul class="column_entry_files">
<% ce.column_entry_files.desc(:sort_number).each do |file| %>
<% next unless file.choose_lang_display(I18n.locale.to_s) %>
<% file_title = file.get_file_title %>
<% size = number_to_human_size(file.file.size) %>
<% link = file.get_link %>
<% if file.file.content_type.start_with?('audio/') %>
<div class="voice-player">
<span class="voice-title"><%= file_title %></span>
<a class="voice-player" data-content="<%= file.file.url %>" href="#" title="<%= file_title %>">
<i class="fa fa-play" aria-hidden="true"></i>
</a>
</div>
<% else %>
<li class="column_entry_file">
<a class="column_entry_file_link" href="<%= link %>" title="<%= file_title %>" target="_blank">
<%= file_title %>
</a>
<span class="file_size">(<%= size %>)</span>
<span class="view_count">
<i class="fa fa-eye" title="<%= t("universal_table.downloaded_times") %>"></i>
<span class="view-count"><%= file.download_count %></span>
</span>
</li>
<% end %>
<% end %>
</ul>
<% end %>
<% end %>
</div>
<div class="entry-suggestion">
<strong><%= entry.column_entries.first.try(:text).to_s %></strong>
<% entry.column_entries.each do |ce| %>
<% if ce.type == "file" %>
<ul class="column_entry_files">
<% ce.column_entry_files.desc(:sort_number).each do |file| %>
<% next unless file.choose_lang_display(I18n.locale.to_s) %>
<% file_title = file.get_file_title %>
<% size = number_to_human_size(file.file.size) %>
<% link = file.get_link %>
<% if file.file.content_type.start_with?('audio/') %>
<div class="voice-player">
<span class="voice-title"><%= file_title %></span>
<a class="voice-player" data-content="<%= file.file.url %>" href="#" title="<%= file_title %>">
<i class="fa fa-play" aria-hidden="true"></i>
</a>
</div>
<% else %>
<li class="column_entry_file">
<a class="column_entry_file_link" href="<%= link %>" title="<%= file_title %>" target="_blank">
<%= file_title %>
</a>
<span class="file_size">(<%= size %>)</span>
<span class="view_count">
<i class="fa fa-eye" title="<%= t("universal_table.downloaded_times") %>"></i>
<span class="view-count"><%= file.download_count %></span>
</span>
</li>
<% end %>
<% end %>
</ul>
<% end %>
<% end %>
</div>

576
app/views/admin/universal_tables/_file_field.html.erb Normal file → Executable file
View File

@ -1,289 +1,289 @@
<% # encoding: utf-8 %>
<% content_for :page_specific_css do %>
<style type="text/css">
.sort-order-icon{
font-size: 25px;
cursor: move;
}
</style>
<% end %>
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/file-type" %>
<%= javascript_include_tag "lib/module-area" %>
<%= javascript_include_tag "lib/jquery-ui-sortable.min" %>
<% end %>
<style>
#fileupload {
position: relative;
clear: both;
overflow: hidden;
height: 254px;
}
#fileupload #dropzone.drop {
position: absolute;
left: 0;
right: 0;
border: 2px dashed #0088CC;
border-radius: 10px;
color: #0088CC;
background-color: #FFFFFF;
z-index: 0;
}
#fileupload #dropzone {
padding: 30px;
text-align: center;
font-size: 3em;
font-family: 'Raleway';
line-height: 1.2em;
color: #e4e4e4;
}
#fileupload #dropzone.in {
opacity: .7;
z-index: 2;
border-color: #faa732;
color: #faa732;
}
</style>
<div class="control-group">
<%= f.label :text, column.title, :class => "control-label" %>
<div class="controls">
<p class="add-btn">
<%= hidden_field_tag 'column_entry_file_field_count', file_field.column_entry_files.count %>
<a id="add_file" class="trigger btn btn-small btn-primary"><i class="icons-plus"></i> <%= t(:add) %></a>
</p>
<hr>
<!-- Add -->
<div class="add-target" id="add-target"></div>
<!-- Exist -->
<% if file_field && !file_field.column_entry_files.blank? %>
<div class="exist plugin-sortable">
<% file_field.column_entry_files.desc(:sort_number).each_with_index do |column_entry_file, i| %>
<%= f.fields_for :column_entry_files, column_entry_file do |f| %>
<%= render :partial => 'form_file', :locals => {:f => f, :i => i,:form_file => column_entry_file} %>
<% end %>
<% end %>
<hr>
</div>
<% end %>
<div id="fileupload" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);" ondragleave="(function(){$('#dropzone').removeClass('in');})()">
<div id="dropzone" class="drop">
<div data-icons=""></div>
<%=t("universal_table.drag_file_to_here")%>
</div>
</div>
</div>
</div>
<% if !file_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>
<% content_for :page_specific_javascript do %>
<script>
if (!FileReader.prototype.readAsBinaryString) {
console.log('readAsBinaryString definition not found');
FileReader.prototype.readAsBinaryString = function (fileData) {
var binary = '';
var pk = this;
var reader = new FileReader();
reader.onload = function (e) {
var bytes = new Uint8Array(reader.result);
var length = bytes.byteLength;
for (var i = 0; i < length; i++) {
var a = bytes[i];
var b = String.fromCharCode(a)
binary += b;
}
pk.content = binary;
$(pk).trigger('onload');
}
reader.readAsArrayBuffer(fileData);
}
}
function FileListItems (files) {
var b;
try{
b = new DataTransfer();
}catch(e){
if(window.dataTransfer){
b = window.dataTransfer;
}else{
if(typeof(ClipboardEvent) == "undefined"){ //IE
b = new DataTransfer();
}else{
b = new ClipboardEvent("").clipboardData;
}
}
}
if(b.items){
b.items.clear();
}else{
if(b.files.length != 0 ){
var files_length = b.files.length;
for(var i = files_length - 1;i >= 0;i = i - 1){
delete b.files[i];
}
}
if(b.files.length != 0){
return files
}
}
for (var i = 0, len = files.length; i<len; i++){
if(b.items){
b.items.add(files[i])
}else{
b.files[i] = files[i];
}
}
return b.files
}
function change_files_to_file_field(file_field,files){
var fileupload = $(file_field).parents(".fileupload");
if(fileupload.length > 0){
fileupload.find(".fileupload-preview").text(files[0].name);
}
var files_list = new FileListItems(files)
try{
$(file_field)[0].files = files_list;
}catch(e){console.log(e)}
if($(file_field)[0].files.length == 0){ //Change failed
var file_field_values = [];
var file_reader = new FileReader();
$("[name=\""+$(file_field)[0].name+"\"][type=\"hidden\"]").remove();
var hidden_input = $("<input type=\"hidden\" name=\""+$(file_field)[0].name+"\">");
var hidden_input_values = [];
$(file_field).after(hidden_input);
var files_list_length = files_list.length;
for(var i = 0; i < files_list_length; i++){
var file = files_list[i];
$(file_field)[0].files[i] = file;
file_reader.readAsBinaryString(files_list[i]);
file_reader.onload = (function(hidden_input,file,i,files_list_length) {
return function(e) {
var file_info = {};
file_info["name"] = file.name;
file_info["type"] = file.type;
if (file_reader.result)
file_reader.content = file_reader.result;
file_info["content"] = e ? e.target.result : file_reader.content;
if(Array.isArray(hidden_input_values)){
hidden_input_values.push(file_info);
}
if(i == files_list_length - 1){
if(hidden_input_values.length == 1){
hidden_input_values = hidden_input_values[0];
}
hidden_input.val(JSON.stringify(hidden_input_values));
}
};})(hidden_input,file,i,files_list_length);
file_field_values.push("C:\\fakepath\\" + files_list[i].name);
}
Object.defineProperty($(file_field)[0].files, "length", {
// only returns odd die sides
get: function () {
var length = 0;
while(this[length]){
length++;
}
return length;
}
});
Object.defineProperty($(file_field)[0], "value", {
// only returns odd die sides
get: function () {
return (this.getAttribute('value') ? this.getAttribute('value') : "");
},
set: function(value) {
this.setAttribute('value',value);
}
});
$(file_field)[0].value = file_field_values.join(", ");
}
}
function dragOverHandler(ev) {
document.activeElement.blur();
$(ev.target).addClass("in");
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
}
function dropHandler(ev) {
window.ev = ev;
window.dataTransfer = ev.dataTransfer;
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
var files = [];
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
// If dropped items aren't files, reject them
if (ev.dataTransfer.items[i].kind === 'file') {
var file = ev.dataTransfer.items[i].getAsFile();
files.push(file)
}
}
} else {
// Use DataTransfer interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
var file = ev.dataTransfer.files[i];
files.push(file)
}
}
files.forEach(function(file){
var single_file = [file];
var file_field = add_file_field();
change_files_to_file_field(file_field,single_file);
})
window.files = files;
var target = ev.target ? ev.target : ev.srcElement;
$(target).removeClass("in");
}
function add_file_field(){
var self = $('#add_file');
var new_id = $(self).prev().attr('value');
var old_id = new RegExp("new_column_entry_files", "g");
var on = $('.language-nav li.active').index();
var le = $('#add-target').children('.start-line').length;
$(self).prev().attr('value', parseInt(new_id) + 1);
$('#add-target').prepend(("<%= escape_javascript(add_attribute 'form_file', f, :column_entry_files) %>").replace(old_id, new_id).replace("new_column_entry_file_sort_order_XXX", parseInt(new_id) + 1));
var file_field = $('#add-target').find("*").eq(0).find("[type=\"file\"]");
$('#add-target').children('.start-line').eq(le).children('.input-append').find('.tab-content').each(function() {
$(self).children('.tab-pane').eq(on).addClass('in active').siblings().removeClass('in active');
});
formTip();
return file_field;
}
$(document).ready(function() {
$(".plugin-sortable").sortable({
update : function(event, ui){
var existingfiles = $(".exist.plugin-sortable div.fileupload")
existingfiles.each(function(i, file){
$(file).find("input.file-sort-number-field").val(existingfiles.length - i);
})
}
});
$('.main-forms .add-on').tooltip();
$(document).on('click', '#add_file', add_file_field);
$(document).on('click', '.delete_file', function(){
$(this).parents('.input-prepend').remove();
});
$(document).on('click',"[type='file']",function(){
$("[name=\""+$(this).attr("name")+"\"][type=\"hiiden\"]").remove();
});
$(document).on('click', '.remove_existing_record', function(){
if(confirm("<%= I18n.t(:sure?)%>")){
$(this).children('.should_destroy').attr('value', 1);
$(this).parents('.start-line').hide();
}
});
});
</script>
<% # encoding: utf-8 %>
<% content_for :page_specific_css do %>
<style type="text/css">
.sort-order-icon{
font-size: 25px;
cursor: move;
}
</style>
<% end %>
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/file-type" %>
<%= javascript_include_tag "lib/module-area" %>
<%= javascript_include_tag "lib/jquery-ui-sortable.min" %>
<% end %>
<style>
#fileupload {
position: relative;
clear: both;
overflow: hidden;
height: 254px;
}
#fileupload #dropzone.drop {
position: absolute;
left: 0;
right: 0;
border: 2px dashed #0088CC;
border-radius: 10px;
color: #0088CC;
background-color: #FFFFFF;
z-index: 0;
}
#fileupload #dropzone {
padding: 30px;
text-align: center;
font-size: 3em;
font-family: 'Raleway';
line-height: 1.2em;
color: #e4e4e4;
}
#fileupload #dropzone.in {
opacity: .7;
z-index: 2;
border-color: #faa732;
color: #faa732;
}
</style>
<div class="control-group">
<%= f.label :text, column.title, :class => "control-label" %>
<div class="controls">
<p class="add-btn">
<%= hidden_field_tag 'column_entry_file_field_count', file_field.column_entry_files.count %>
<a id="add_file" class="trigger btn btn-small btn-primary"><i class="icons-plus"></i> <%= t(:add) %></a>
</p>
<hr>
<!-- Add -->
<div class="add-target" id="add-target"></div>
<!-- Exist -->
<% if file_field && !file_field.column_entry_files.blank? %>
<div class="exist plugin-sortable">
<% file_field.column_entry_files.desc(:sort_number).each_with_index do |column_entry_file, i| %>
<%= f.fields_for :column_entry_files, column_entry_file do |f| %>
<%= render :partial => 'form_file', :locals => {:f => f, :i => i,:form_file => column_entry_file} %>
<% end %>
<% end %>
<hr>
</div>
<% end %>
<div id="fileupload" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);" ondragleave="(function(){$('#dropzone').removeClass('in');})()">
<div id="dropzone" class="drop">
<div data-icons=""></div>
<%=t("universal_table.drag_file_to_here")%>
</div>
</div>
</div>
</div>
<% if !file_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>
<% content_for :page_specific_javascript do %>
<script>
if (!FileReader.prototype.readAsBinaryString) {
console.log('readAsBinaryString definition not found');
FileReader.prototype.readAsBinaryString = function (fileData) {
var binary = '';
var pk = this;
var reader = new FileReader();
reader.onload = function (e) {
var bytes = new Uint8Array(reader.result);
var length = bytes.byteLength;
for (var i = 0; i < length; i++) {
var a = bytes[i];
var b = String.fromCharCode(a)
binary += b;
}
pk.content = binary;
$(pk).trigger('onload');
}
reader.readAsArrayBuffer(fileData);
}
}
function FileListItems (files) {
var b;
try{
b = new DataTransfer();
}catch(e){
if(window.dataTransfer){
b = window.dataTransfer;
}else{
if(typeof(ClipboardEvent) == "undefined"){ //IE
b = new DataTransfer();
}else{
b = new ClipboardEvent("").clipboardData;
}
}
}
if(b.items){
b.items.clear();
}else{
if(b.files.length != 0 ){
var files_length = b.files.length;
for(var i = files_length - 1;i >= 0;i = i - 1){
delete b.files[i];
}
}
if(b.files.length != 0){
return files
}
}
for (var i = 0, len = files.length; i<len; i++){
if(b.items){
b.items.add(files[i])
}else{
b.files[i] = files[i];
}
}
return b.files
}
function change_files_to_file_field(file_field,files){
var fileupload = $(file_field).parents(".fileupload");
if(fileupload.length > 0){
fileupload.find(".fileupload-preview").text(files[0].name);
}
var files_list = new FileListItems(files)
try{
$(file_field)[0].files = files_list;
}catch(e){console.log(e)}
if($(file_field)[0].files.length == 0){ //Change failed
var file_field_values = [];
var file_reader = new FileReader();
$("[name=\""+$(file_field)[0].name+"\"][type=\"hidden\"]").remove();
var hidden_input = $("<input type=\"hidden\" name=\""+$(file_field)[0].name+"\">");
var hidden_input_values = [];
$(file_field).after(hidden_input);
var files_list_length = files_list.length;
for(var i = 0; i < files_list_length; i++){
var file = files_list[i];
$(file_field)[0].files[i] = file;
file_reader.readAsBinaryString(files_list[i]);
file_reader.onload = (function(hidden_input,file,i,files_list_length) {
return function(e) {
var file_info = {};
file_info["name"] = file.name;
file_info["type"] = file.type;
if (file_reader.result)
file_reader.content = file_reader.result;
file_info["content"] = e ? e.target.result : file_reader.content;
if(Array.isArray(hidden_input_values)){
hidden_input_values.push(file_info);
}
if(i == files_list_length - 1){
if(hidden_input_values.length == 1){
hidden_input_values = hidden_input_values[0];
}
hidden_input.val(JSON.stringify(hidden_input_values));
}
};})(hidden_input,file,i,files_list_length);
file_field_values.push("C:\\fakepath\\" + files_list[i].name);
}
Object.defineProperty($(file_field)[0].files, "length", {
// only returns odd die sides
get: function () {
var length = 0;
while(this[length]){
length++;
}
return length;
}
});
Object.defineProperty($(file_field)[0], "value", {
// only returns odd die sides
get: function () {
return (this.getAttribute('value') ? this.getAttribute('value') : "");
},
set: function(value) {
this.setAttribute('value',value);
}
});
$(file_field)[0].value = file_field_values.join(", ");
}
}
function dragOverHandler(ev) {
document.activeElement.blur();
$(ev.target).addClass("in");
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
}
function dropHandler(ev) {
window.ev = ev;
window.dataTransfer = ev.dataTransfer;
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
var files = [];
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
// If dropped items aren't files, reject them
if (ev.dataTransfer.items[i].kind === 'file') {
var file = ev.dataTransfer.items[i].getAsFile();
files.push(file)
}
}
} else {
// Use DataTransfer interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
var file = ev.dataTransfer.files[i];
files.push(file)
}
}
files.forEach(function(file){
var single_file = [file];
var file_field = add_file_field();
change_files_to_file_field(file_field,single_file);
})
window.files = files;
var target = ev.target ? ev.target : ev.srcElement;
$(target).removeClass("in");
}
function add_file_field(){
var self = $('#add_file');
var new_id = $(self).prev().attr('value');
var old_id = new RegExp("new_column_entry_files", "g");
var on = $('.language-nav li.active').index();
var le = $('#add-target').children('.start-line').length;
$(self).prev().attr('value', parseInt(new_id) + 1);
$('#add-target').prepend(("<%= escape_javascript(add_attribute 'form_file', f, :column_entry_files) %>").replace(old_id, new_id).replace("new_column_entry_file_sort_order_XXX", parseInt(new_id) + 1));
var file_field = $('#add-target').find("*").eq(0).find("[type=\"file\"]");
$('#add-target').children('.start-line').eq(le).children('.input-append').find('.tab-content').each(function() {
$(self).children('.tab-pane').eq(on).addClass('in active').siblings().removeClass('in active');
});
formTip();
return file_field;
}
$(document).ready(function() {
$(".plugin-sortable").sortable({
update : function(event, ui){
var existingfiles = $(".exist.plugin-sortable div.fileupload")
existingfiles.each(function(i, file){
$(file).find("input.file-sort-number-field").val(existingfiles.length - i);
})
}
});
$('.main-forms .add-on').tooltip();
$(document).on('click', '#add_file', add_file_field);
$(document).on('click', '.delete_file', function(){
$(this).parents('.input-prepend').remove();
});
$(document).on('click',"[type='file']",function(){
$("[name=\""+$(this).attr("name")+"\"][type=\"hiiden\"]").remove();
});
$(document).on('click', '.remove_existing_record', function(){
if(confirm("<%= I18n.t(:sure?)%>")){
$(this).children('.should_destroy').attr('value', 1);
$(this).parents('.start-line').hide();
}
});
});
</script>
<% end %>

130
app/views/admin/universal_tables/_form_file.html.erb Normal file → Executable file
View File

@ -1,65 +1,65 @@
<% if form_file.new_record? %>
<div class="fileupload fileupload-new start-line" data-provides="fileupload">
<% else %>
<div class="fileupload fileupload-exists start-line" data-provides="fileupload">
<i class="icons-list-2 sort-order-icon"></i>
<% if form_file.file.blank? %>
<%= t(:no_file) %>
<% else %>
<%= link_to content_tag(:i) + form_file.file_identifier, form_file.file.url, {:class => 'file-link file-type', :target => '_blank', :title => form_file.file_identifier} %>
<% end %>
<% end %>
<div class="input-prepend input-append">
<label>
<span class="add-on btn btn-file" title="<%= t(:file_) %>">
<i class="icons-paperclip"></i>
<%= f.file_field :file %>
</span>
<div class="uneditable-input input-medium">
<i class="icon-file fileupload-exists"></i>
<span class="fileupload-preview"><%= (form_file.new_record? || form_file.file.blank?) ? t(:select_file) : t(:change_file) %></span>
</div>
</label>
<span class="add-on icons-pencil" title="<%= t('file.name') %>"></span>
<span class="tab-content">
<% @site_in_use_locales.each_with_index do |locale, i| %>
<span class="tab-pane fade <%= ( i == 0 ) ? "in active" : '' %> <%= locale %>">
<%= f.fields_for :file_title_translations do |f| %>
<%= f.text_field locale, :class => "input-medium", placeholder: t('file.name'), :value => (form_file.file_title_translations[locale] rescue nil) %>
<% end %>
</span>
<% end %>
</span>
<span class="add-on btn-group btn" title="<%= t('universal_table.show_lang') %>">
<i class="icons-earth"></i> <span class="caret"></span>
<ul class="dropdown-menu">
<% @site_in_use_locales.each do |locale| %>
<li>
<label class="checkbox">
<%= check_box_tag "#{f.object_name}[choose_lang][]", locale, form_file.choose_lang.include?(locale.to_s) %>
<%= t(locale.to_s) %>
</label>
</li>
<% end %>
</ul>
<%= hidden_field_tag "#{f.object_name}[choose_lang][]", '' %>
</span>
<% if form_file.new_record? %>
<span class="delete_file add-on btn" title="<%= t(:delete_) %>">
<a class="icon-trash"></a>
<%= f.hidden_field :sort_number, :value => "new_column_entry_file_sort_order_XXX", :class => "input-mini" %>
</span>
<% else %>
<span class="remove_existing_record add-on btn" title="<%= t(:remove) %>">
<%= f.hidden_field :id %>
<%= f.hidden_field :sort_number , :class => "file-sort-number-field" %>
<a class=" icon-remove"></a>
<%= f.hidden_field :_destroy, :value => nil, :class => 'should_destroy' %>
</span>
<span class="downloaded_times">Downloaded <b><%= form_file.download_count %></b> time<%= form_file.download_count > 1 ? "s" : "" %>.</span>
<% end %>
</div>
</div>
<% if form_file.new_record? %>
<div class="fileupload fileupload-new start-line" data-provides="fileupload">
<% else %>
<div class="fileupload fileupload-exists start-line" data-provides="fileupload">
<i class="icons-list-2 sort-order-icon"></i>
<% if form_file.file.blank? %>
<%= t(:no_file) %>
<% else %>
<%= link_to content_tag(:i) + form_file.file_identifier, form_file.file.url, {:class => 'file-link file-type', :target => '_blank', :title => form_file.file_identifier} %>
<% end %>
<% end %>
<div class="input-prepend input-append">
<label>
<span class="add-on btn btn-file" title="<%= t(:file_) %>">
<i class="icons-paperclip"></i>
<%= f.file_field :file %>
</span>
<div class="uneditable-input input-medium">
<i class="icon-file fileupload-exists"></i>
<span class="fileupload-preview"><%= (form_file.new_record? || form_file.file.blank?) ? t(:select_file) : t(:change_file) %></span>
</div>
</label>
<span class="add-on icons-pencil" title="<%= t('file.name') %>"></span>
<span class="tab-content">
<% @site_in_use_locales.each_with_index do |locale, i| %>
<span class="tab-pane fade <%= ( i == 0 ) ? "in active" : '' %> <%= locale %>">
<%= f.fields_for :file_title_translations do |f| %>
<%= f.text_field locale, :class => "input-medium", placeholder: t('file.name'), :value => (form_file.file_title_translations[locale] rescue nil) %>
<% end %>
</span>
<% end %>
</span>
<span class="add-on btn-group btn" title="<%= t('universal_table.show_lang') %>">
<i class="icons-earth"></i> <span class="caret"></span>
<ul class="dropdown-menu">
<% @site_in_use_locales.each do |locale| %>
<li>
<label class="checkbox">
<%= check_box_tag "#{f.object_name}[choose_lang][]", locale, form_file.choose_lang.include?(locale.to_s) %>
<%= t(locale.to_s) %>
</label>
</li>
<% end %>
</ul>
<%= hidden_field_tag "#{f.object_name}[choose_lang][]", '' %>
</span>
<% if form_file.new_record? %>
<span class="delete_file add-on btn" title="<%= t(:delete_) %>">
<a class="icon-trash"></a>
<%= f.hidden_field :sort_number, :value => "new_column_entry_file_sort_order_XXX", :class => "input-mini" %>
</span>
<% else %>
<span class="remove_existing_record add-on btn" title="<%= t(:remove) %>">
<%= f.hidden_field :id %>
<%= f.hidden_field :sort_number , :class => "file-sort-number-field" %>
<a class=" icon-remove"></a>
<%= f.hidden_field :_destroy, :value => nil, :class => 'should_destroy' %>
</span>
<span class="downloaded_times">Downloaded <b><%= form_file.download_count %></b> time<%= form_file.download_count > 1 ? "s" : "" %>.</span>
<% end %>
</div>
</div>

60
app/views/admin/universal_tables/_image_field.html.erb Normal file → Executable file
View File

@ -1,31 +1,31 @@
<div class="control-group">
<%= f.label :image, column.title, :class => "control-label" %>
<div class="controls">
<div class="fileupload fileupload-new clearfix <%= 'fileupload-edit' if image_field.image.file %>" data-provides="fileupload">
<div class="fileupload-new thumbnail pull-left">
<% if image_field.image.file %>
<%= image_tag image_field.image %>
<% else %>
<img src="http://www.placehold.it/50x50/EFEFEF/AAAAAA" />
<% end %>
</div>
<div class="fileupload-preview fileupload-exists thumbnail pull-left"></div>
<span class="btn btn-file">
<span class="fileupload-new"><%= t(:select_image) %></span>
<span class="fileupload-exists"><%= t(:change) %></span>
<%= f.file_field :image %>
</span>
<a href="#" class="btn fileupload-exists" data-dismiss="fileupload"><%= t(:cancel) %></a>
<div class="controls" data-toggle="buttons-checkbox">
<label class="checkbox inline btn btn-danger fileupload-remove">
<%= f.check_box :remove_image %><%= t(:remove) %>
</label>
</div>
</div>
</div>
<% if !image_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
<div class="control-group">
<%= f.label :image, column.title, :class => "control-label" %>
<div class="controls">
<div class="fileupload fileupload-new clearfix <%= 'fileupload-edit' if image_field.image.file %>" data-provides="fileupload">
<div class="fileupload-new thumbnail pull-left">
<% if image_field.image.file %>
<%= image_tag image_field.image %>
<% else %>
<img src="http://www.placehold.it/50x50/EFEFEF/AAAAAA" />
<% end %>
</div>
<div class="fileupload-preview fileupload-exists thumbnail pull-left"></div>
<span class="btn btn-file">
<span class="fileupload-new"><%= t(:select_image) %></span>
<span class="fileupload-exists"><%= t(:change) %></span>
<%= f.file_field :image %>
</span>
<a href="#" class="btn fileupload-exists" data-dismiss="fileupload"><%= t(:cancel) %></a>
<div class="controls" data-toggle="buttons-checkbox">
<label class="checkbox inline btn btn-danger fileupload-remove">
<%= f.check_box :remove_image %><%= t(:remove) %>
</label>
</div>
</div>
</div>
<% if !image_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>

96
app/views/admin/universal_tables/_index.html.erb Normal file → Executable file
View File

@ -1,49 +1,49 @@
<table class="table main-list">
<thead>
<tr class="sort-header">
<% @table_fields.each do |f| %>
<%= thead(f) %>
<% end %>
</tr>
</thead>
<tbody>
<% @tables.each do |table| %>
<% can_edit = can_edit_or_delete?(table) %>
<tr id="table_<%= table.id.to_s %>">
<td>
<a href="<%= admin_universal_table_path(table) %>"><%= table.title %></a>
<div class="quick-edit">
<ul class="nav nav-pills">
<% if can_edit %>
<li><a href="<%= edit_admin_universal_table_path(table) %>"><%= t(:edit) %></a></li>
<li><a href="<%= "/admin/universal_table/#{table.id.to_s}/mind_maps" %>"><%= t("universal_table.mind_map") %></a></li>
<% if table.ordered_with_sort_number %>
<li><a href="<%= admin_universal_table_edit_sort_path(table) %>"><%= t('universal_table.edit_sort') %></a></li>
<% end %>
<li><a href="/admin/universal_tables/<%=table.id.to_s%>/export_data?format=xlsx" data-table-id="<%= table.id.to_s %>" class="export-xls"><%= t('universal_table.export_xls') %></a></li>
<li><a href="<%= admin_universal_table_path(table) %>" class="delete text-error" data-method="delete" data-confirm="Are you sure?"><%= t(:delete_) %></a></li>
<% end %>
</ul>
</div>
</td>
<td>
<%= table.created_at.strftime("%Y-%m-%d") %>
</td>
<td>
<%= table.table_entries.count %>
</td>
<td>
<% if can_edit %>
<form action="/admin/universal_tables/import_data_from_excel" method="post" enctype="multipart/form-data" class="import_from_excel_form">
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
<input type="file" name="import_data" />
<button class="btn btn-primary btn-small"><i class="icons-upload"></i></button>
<input type="hidden" name="universal_table_id" value="<%= table.id.to_s %>" />
<a href="<%= admin_universal_table_export_structure_path(table, :format => "xlsx") %>"><%= t("universal_table.export_structure") %></a>
</form>
<% end %>
</td>
</tr>
<% end %>
</tbody>
<table class="table main-list">
<thead>
<tr class="sort-header">
<% @table_fields.each do |f| %>
<%= thead(f) %>
<% end %>
</tr>
</thead>
<tbody>
<% @tables.each do |table| %>
<% can_edit = can_edit_or_delete?(table) %>
<tr id="table_<%= table.id.to_s %>">
<td>
<a href="<%= admin_universal_table_path(table) %>"><%= table.title %></a>
<div class="quick-edit">
<ul class="nav nav-pills">
<% if can_edit %>
<li><a href="<%= edit_admin_universal_table_path(table) %>"><%= t(:edit) %></a></li>
<li><a href="<%= "/admin/universal_table/#{table.id.to_s}/mind_maps" %>"><%= t("universal_table.mind_map") %></a></li>
<% if table.ordered_with_sort_number %>
<li><a href="<%= admin_universal_table_edit_sort_path(table) %>"><%= t('universal_table.edit_sort') %></a></li>
<% end %>
<li><a href="/admin/universal_tables/<%=table.id.to_s%>/export_data?format=xlsx" data-table-id="<%= table.id.to_s %>" class="export-xls"><%= t('universal_table.export_xls') %></a></li>
<li><a href="<%= admin_universal_table_path(table) %>" class="delete text-error" data-method="delete" data-confirm="Are you sure?"><%= t(:delete_) %></a></li>
<% end %>
</ul>
</div>
</td>
<td>
<%= table.created_at.strftime("%Y-%m-%d") %>
</td>
<td>
<%= table.table_entries.count %>
</td>
<td>
<% if can_edit %>
<form action="/admin/universal_tables/import_data_from_excel" method="post" enctype="multipart/form-data" class="import_from_excel_form">
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
<input type="file" name="import_data" />
<button class="btn btn-primary btn-small"><i class="icons-upload"></i></button>
<input type="hidden" name="universal_table_id" value="<%= table.id.to_s %>" />
<a href="<%= admin_universal_table_export_structure_path(table, :format => "xlsx") %>"><%= t("universal_table.export_structure") %></a>
</form>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>

View File

@ -1,25 +1,25 @@
<div class="control-group">
<%= f.label :number, column.title, :class => "control-label" %>
<div class="controls">
<div class="input-append">
<%= f.number_field :number, :value => integer_field.number %>
</div>
<% if column.make_categorizable %>
<%= render_unique_number(f,column,i).html_safe %>
<script type="text/javascript">
$("select#<%= column.key + "_" + i.to_s %>").on("change",function(){
var el = $(this),
value = el.val(),
inputs = el.parent().parent().find("input[type=number]");
inputs.val(value);
})
</script>
<% end %>
</div>
<% if !integer_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
<div class="control-group">
<%= f.label :number, column.title, :class => "control-label" %>
<div class="controls">
<div class="input-append">
<%= f.number_field :number, :value => integer_field.number %>
</div>
<% if column.make_categorizable %>
<%= render_unique_number(f,column,i).html_safe %>
<script type="text/javascript">
$("select#<%= column.key + "_" + i.to_s %>").on("change",function(){
var el = $(this),
value = el.val(),
inputs = el.parent().parent().find("input[type=number]");
inputs.val(value);
})
</script>
<% end %>
</div>
<% if !integer_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>

66
app/views/admin/universal_tables/_period_field.html.erb Normal file → Executable file
View File

@ -1,34 +1,34 @@
<div class="control-group ut-control-group">
<div class="control-group ut-control-group-col">
<%= f.label :period_from, column.title, :class => "control-label" %>
<div class="controls">
<div>
<div class="default_picker input-append" style="">
<% v = !period_field.new_record? ? format_date(period_field.period_from, column.date_format, true) : "" %>
<%= f.text_field :period_from, :value => v, :placeholder => column.date_format.upcase, :data => {:format => (column.date_format == "yyyy" ? "yyyy/MM" : column.date_format)}, :class => "input-small" %>
<span class="add-on clearDate"><i class="icons-cross-3"></i></span>
<span class="add-on iconbtn"><i data-date-icon="icons-calendar" data-time-icon="icons-clock" class="icons-calendar"></i></span>
</div>
</div>
</div>
</div>
<div class="control-group ut-control-group-col ut-control-group-col-right">
<%= f.label :period_to, "~", :class => "control-label" %>
<div class="controls">
<div>
<div class="default_picker input-append" style="">
<% v = !period_field.new_record? ? format_date(period_field.period_to, column.date_format, true) : "" %>
<%= f.text_field :period_to,:value => v, :placeholder => column.date_format.upcase, :data => {:format => (column.date_format == "yyyy" ? "yyyy/MM" : column.date_format)}, :class => "input-small" %>
<span class="add-on clearDate"><i class="icons-cross-3"></i></span>
<span class="add-on iconbtn"><i data-date-icon="icons-calendar" data-time-icon="icons-clock" class="icons-calendar"></i></span>
</div>
</div>
</div>
</div>
<% if !period_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
<div class="control-group ut-control-group">
<div class="control-group ut-control-group-col">
<%= f.label :period_from, column.title, :class => "control-label" %>
<div class="controls">
<div>
<div class="default_picker input-append" style="">
<% v = !period_field.new_record? ? format_date(period_field.period_from, column.date_format, true) : "" %>
<%= f.text_field :period_from, :value => v, :placeholder => column.date_format.upcase, :data => {:format => (column.date_format == "yyyy" ? "yyyy/MM" : column.date_format)}, :class => "input-small" %>
<span class="add-on clearDate"><i class="icons-cross-3"></i></span>
<span class="add-on iconbtn"><i data-date-icon="icons-calendar" data-time-icon="icons-clock" class="icons-calendar"></i></span>
</div>
</div>
</div>
</div>
<div class="control-group ut-control-group-col ut-control-group-col-right">
<%= f.label :period_to, "~", :class => "control-label" %>
<div class="controls">
<div>
<div class="default_picker input-append" style="">
<% v = !period_field.new_record? ? format_date(period_field.period_to, column.date_format, true) : "" %>
<%= f.text_field :period_to,:value => v, :placeholder => column.date_format.upcase, :data => {:format => (column.date_format == "yyyy" ? "yyyy/MM" : column.date_format)}, :class => "input-small" %>
<span class="add-on clearDate"><i class="icons-cross-3"></i></span>
<span class="add-on iconbtn"><i data-date-icon="icons-calendar" data-time-icon="icons-clock" class="icons-calendar"></i></span>
</div>
</div>
</div>
</div>
<% if !period_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>

388
app/views/admin/universal_tables/_table_form.html.erb Normal file → Executable file
View File

@ -1,194 +1,194 @@
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %>
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "universal_table/jquery-ui.min" %>
<% end %>
<fieldset class="utable-heading-wrap">
<div class="utable-heading-header">
<h4><%= t("universal_table.table_name") %></h4>
</div>
<div class="control-group">
<div class="controls">
<div class="input-append">
<div class="tab-content">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<div class="tab-pane fade <%= active %>" id="table_name_<%= locale.to_s %>">
<%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :placeholder => "Title", :value => @table.title_translations[locale] %>
<% end %>
</div>
<% end %>
</div>
<div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<%= link_to t(locale).to_s,"#table_name_#{locale.to_s}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset>
<div class="utable-heading-header">
<h4><%= t("universal_table.default_ordered_field") %></h4>
</div>
<div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.created_at') %></label>
<div class="controls">
<div>
<%= f.check_box :ordered_with_created_at, class: 'default_ordered_field ordered_with_created_at' %>
</div>
<div class="order_direction<%= ' hidden' if !f.object.ordered_with_created_at %>">
<%= f.select :created_at_order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.sort_number') %></label>
<div class="controls">
<div>
<%= f.check_box :ordered_with_sort_number, class: 'default_ordered_field' %>
</div>
<div class="order_direction<%= ' hidden' if !f.object.ordered_with_sort_number %>">
<%= f.select :sort_number_order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div>
</div>
</div>
</fieldset>
<fieldset class="utable-content">
<div id="attributes-area" class="input-area">
<% @table.table_columns.asc(:order).each_with_index do |table_column, index| %>
<div class="attributes default ">
<%= f.fields_for :table_columns, table_column, :child_index => index.to_s do |f| %>
<%= render :partial => "column", :object => table_column, :locals => {:f => f, :i => index} %>
<%= f.hidden_field :id %>
<% end %>
</div>
<% end %>
</div>
<div class="form-actions">
<button type="button" class="btn btn-success add-attributes"><%= t("universal_table.add_column") %></button>
<input class="btn btn-primary" name="commit" type="submit" value="<%= t("save") %>">
</div>
</fieldset>
<script type="text/javascript">
var columnArea = $("#attributes-area"),
totalColumns = columnArea.find(".attributes.default").length,
columnCounter = totalColumns;
$("button.add-attributes").on("click",function(){
var html = "<%= escape_javascript(add_attribute 'column', f, :table_columns) %>",
replaceReg = new RegExp("new_table_columns","g"),
idNumber = new RegExp("XXX","g");
html = html.replace(replaceReg,columnCounter);
html = html.replace("ColumnXX", "Column " + (columnCounter + 1));
html = html.replace(idNumber,columnCounter);
columnArea.append(html);
columnCounter++;
})
$(document.body).on("click","a.delete",function(){
if($(this).parent().find(".attribute_field_to_delete").length == 0){
$(this).parent().parent().slideUp(function(){
$(this).remove();
updateOrder();
});
}else{
if(confirm("Are you sure?")){
$(this).parent().parent().slideUp(function(){
updateOrder();
});
$(this).parent().find(".attribute_field_to_delete").val("true");
}
}
return false;
})
$(document.body).on("change","select.type-selector",function(){
var el = $(this),
label = el.parent().find("span.link_to_show");
if(el.val() == "text" || el.val() == "integer"){
label.removeClass("hide");
}else{
label.addClass("hide");
label.find("input[type=checkbox]").prop("checked",false);
}
label = el.parent().find("label.date_format");
if(el.val() == "date" || el.val() == "period"){
label.removeClass("hide");
}else{
label.addClass("hide");
}
})
$(document).on('change','.default_ordered_field',function(){
$('.order_direction').addClass('hidden');
if ($(this).prop('checked')){
$('.default_ordered_field').not(this).prop('checked',false);
$(this).parents('.controls').eq(0).find('.order_direction').removeClass('hidden');
}
});
$(document).ready(function(){
if ($('.ordered_with_created_at').prop('checked')){
$('.default_ordered_field').not($('.ordered_with_created_at')[0]).prop('checked',false);
$('.order_direction').not($('.ordered_with_created_at').eq(0).parents('.controls').eq(0).find('.order_direction')[0]).addClass('hidden');
}
})
function key_on_blur() {
$('input[data-type=key]').on('blur',function() {
var index_this = $(this).parents('.attributes').index()
console.log(index_this)
var input_this = parseInt($(this).val()) - 1
if (input_this > ($('#attributes-area>.attributes').length-1)){
input_this = $('#attributes-area>.attributes').length-1
}else if (input_this < 0){
input_this = 0
}
if (index_this > input_this){
$(this).parents('#attributes-area>.attributes').insertBefore($('#attributes-area>.attributes').eq(input_this))
}
else if (index_this < input_this){
$(this).parents('#attributes-area>.attributes').insertAfter($('#attributes-area>.attributes').eq(input_this))
}
update_key(this)
});
}
function update_key(ele){
var ui_child=$(ele).parents('#attributes-area').find('.attributes');
for (var i=0;i<ui_child.length;i++){
var now_ele = ui_child.eq(i);
now_ele.find('input[data-type=key]').val(i+1);
}
updateOrder();
}
$('#attributes-area').ready(function(){
$("#attributes-area").sortable({
update: function( event, ui ) {
update_key($(ui.item[0]).find('input[data-type=key]'))
}
});
$("#attributes-area").on('change',key_on_blur)
key_on_blur()
})
var updateOrder = function(){
var attributes = $("#attributes-area").find(".attributes:visible");
attributes.each(function(i){
$(this).find("input.order-hidden-field").val(i);
})
}
</script>
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %>
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "universal_table/jquery-ui.min" %>
<% end %>
<fieldset class="utable-heading-wrap">
<div class="utable-heading-header">
<h4><%= t("universal_table.table_name") %></h4>
</div>
<div class="control-group">
<div class="controls">
<div class="input-append">
<div class="tab-content">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<div class="tab-pane fade <%= active %>" id="table_name_<%= locale.to_s %>">
<%= f.fields_for :title_translations do |f| %>
<%= f.text_field locale, :placeholder => "Title", :value => @table.title_translations[locale] %>
<% end %>
</div>
<% end %>
</div>
<div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<%= link_to t(locale).to_s,"#table_name_#{locale.to_s}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset>
<div class="utable-heading-header">
<h4><%= t("universal_table.default_ordered_field") %></h4>
</div>
<div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.created_at') %></label>
<div class="controls">
<div>
<%= f.check_box :ordered_with_created_at, class: 'default_ordered_field ordered_with_created_at' %>
</div>
<div class="order_direction<%= ' hidden' if !f.object.ordered_with_created_at %>">
<%= f.select :created_at_order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label muted" for=""><%= t('universal_table.sort_number') %></label>
<div class="controls">
<div>
<%= f.check_box :ordered_with_sort_number, class: 'default_ordered_field' %>
</div>
<div class="order_direction<%= ' hidden' if !f.object.ordered_with_sort_number %>">
<%= f.select :sort_number_order_direction,['desc','asc'].map{|v| [t("universal_table.#{v}"),v]} %>
</div>
</div>
</div>
</fieldset>
<fieldset class="utable-content">
<div id="attributes-area" class="input-area">
<% @table.table_columns.asc(:order).each_with_index do |table_column, index| %>
<div class="attributes default ">
<%= f.fields_for :table_columns, table_column, :child_index => index.to_s do |f| %>
<%= render :partial => "column", :object => table_column, :locals => {:f => f, :i => index} %>
<%= f.hidden_field :id %>
<% end %>
</div>
<% end %>
</div>
<div class="form-actions">
<button type="button" class="btn btn-success add-attributes"><%= t("universal_table.add_column") %></button>
<input class="btn btn-primary" name="commit" type="submit" value="<%= t("save") %>">
</div>
</fieldset>
<script type="text/javascript">
var columnArea = $("#attributes-area"),
totalColumns = columnArea.find(".attributes.default").length,
columnCounter = totalColumns;
$("button.add-attributes").on("click",function(){
var html = "<%= escape_javascript(add_attribute 'column', f, :table_columns) %>",
replaceReg = new RegExp("new_table_columns","g"),
idNumber = new RegExp("XXX","g");
html = html.replace(replaceReg,columnCounter);
html = html.replace("ColumnXX", "Column " + (columnCounter + 1));
html = html.replace(idNumber,columnCounter);
columnArea.append(html);
columnCounter++;
})
$(document.body).on("click","a.delete",function(){
if($(this).parent().find(".attribute_field_to_delete").length == 0){
$(this).parent().parent().slideUp(function(){
$(this).remove();
updateOrder();
});
}else{
if(confirm("Are you sure?")){
$(this).parent().parent().slideUp(function(){
updateOrder();
});
$(this).parent().find(".attribute_field_to_delete").val("true");
}
}
return false;
})
$(document.body).on("change","select.type-selector",function(){
var el = $(this),
label = el.parent().find("span.link_to_show");
if(el.val() == "text" || el.val() == "integer"){
label.removeClass("hide");
}else{
label.addClass("hide");
label.find("input[type=checkbox]").prop("checked",false);
}
label = el.parent().find("label.date_format");
if(el.val() == "date" || el.val() == "period"){
label.removeClass("hide");
}else{
label.addClass("hide");
}
})
$(document).on('change','.default_ordered_field',function(){
$('.order_direction').addClass('hidden');
if ($(this).prop('checked')){
$('.default_ordered_field').not(this).prop('checked',false);
$(this).parents('.controls').eq(0).find('.order_direction').removeClass('hidden');
}
});
$(document).ready(function(){
if ($('.ordered_with_created_at').prop('checked')){
$('.default_ordered_field').not($('.ordered_with_created_at')[0]).prop('checked',false);
$('.order_direction').not($('.ordered_with_created_at').eq(0).parents('.controls').eq(0).find('.order_direction')[0]).addClass('hidden');
}
})
function key_on_blur() {
$('input[data-type=key]').on('blur',function() {
var index_this = $(this).parents('.attributes').index()
console.log(index_this)
var input_this = parseInt($(this).val()) - 1
if (input_this > ($('#attributes-area>.attributes').length-1)){
input_this = $('#attributes-area>.attributes').length-1
}else if (input_this < 0){
input_this = 0
}
if (index_this > input_this){
$(this).parents('#attributes-area>.attributes').insertBefore($('#attributes-area>.attributes').eq(input_this))
}
else if (index_this < input_this){
$(this).parents('#attributes-area>.attributes').insertAfter($('#attributes-area>.attributes').eq(input_this))
}
update_key(this)
});
}
function update_key(ele){
var ui_child=$(ele).parents('#attributes-area').find('.attributes');
for (var i=0;i<ui_child.length;i++){
var now_ele = ui_child.eq(i);
now_ele.find('input[data-type=key]').val(i+1);
}
updateOrder();
}
$('#attributes-area').ready(function(){
$("#attributes-area").sortable({
update: function( event, ui ) {
update_key($(ui.item[0]).find('input[data-type=key]'))
}
});
$("#attributes-area").on('change',key_on_blur)
key_on_blur()
})
var updateOrder = function(){
var attributes = $("#attributes-area").find(".attributes:visible");
attributes.each(function(i){
$(this).find("input.order-hidden-field").val(i);
})
}
</script>

90
app/views/admin/universal_tables/_text_field.html.erb Normal file → Executable file
View File

@ -1,46 +1,46 @@
<div class="control-group">
<%= f.label :text, column.title, :class => "control-label" %>
<div class="controls">
<div class="input-append">
<div class="tab-content">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<% id = "table_entry_column_entries_#{i}_text_translations_#{locale.to_s}" %>
<div class="tab-pane fade in <%= active %>" id="<%= id %>">
<%= f.fields_for :text_translations do |f| %>
<%= f.text_field locale, :value => text_field.text_translations[locale.to_s], :for => locale.to_s %>
<% end %>
</div>
<% end %>
</div>
<div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<% id = "table_entry_column_entries_#{i}_text_translations_#{locale.to_s}" %>
<%= link_to t(locale).to_s,"##{id}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %>
</div>
</div>
<% if column.make_categorizable %>
<%= render_unique_texts(f,column,i).html_safe %>
<script type="text/javascript">
$("select#<%= column.key + "_" + i.to_s %>").on("change",function(){
var el = $(this),
locales = <%= @site_in_use_locales.to_json.html_safe %>,
values = JSON.parse(el.val()),
inputs = el.parent().parent().find("input[type=text]");
$.each(locales,function(i,locale){
var input = inputs.filter("input[for=" + locale + "]");
input.val(values[locale]);
})
})
</script>
<% end %>
</div>
<% if !text_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
<div class="control-group">
<%= f.label :text, column.title, :class => "control-label" %>
<div class="controls">
<div class="input-append">
<div class="tab-content">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active in" : "") %>
<% id = "table_entry_column_entries_#{i}_text_translations_#{locale.to_s}" %>
<div class="tab-pane fade in <%= active %>" id="<%= id %>">
<%= f.fields_for :text_translations do |f| %>
<%= f.text_field locale, :value => text_field.text_translations[locale.to_s], :for => locale.to_s %>
<% end %>
</div>
<% end %>
</div>
<div class="btn-group" data-toggle="buttons-radio">
<% @site_in_use_locales.each do |locale| %>
<% active = (locale == @site_in_use_locales.first ? "active" : "") %>
<% id = "table_entry_column_entries_#{i}_text_translations_#{locale.to_s}" %>
<%= link_to t(locale).to_s,"##{id}",:class=>"btn #{active}",:data=>{:toggle=>"tab"}%>
<% end %>
</div>
</div>
<% if column.make_categorizable %>
<%= render_unique_texts(f,column,i).html_safe %>
<script type="text/javascript">
$("select#<%= column.key + "_" + i.to_s %>").on("change",function(){
var el = $(this),
locales = <%= @site_in_use_locales.to_json.html_safe %>,
values = JSON.parse(el.val()),
inputs = el.parent().parent().find("input[type=text]");
$.each(locales,function(i,locale){
var input = inputs.filter("input[for=" + locale + "]");
input.val(values[locale]);
})
})
</script>
<% end %>
</div>
<% if !text_field.new_record? %>
<%= f.hidden_field :id %>
<% else %>
<%= f.hidden_field :table_column_id, :value => column.id %>
<% end %>
</div>

8
app/views/admin/universal_tables/edit.html.erb Normal file → Executable file
View File

@ -1,5 +1,5 @@
<%= form_for @table, url: admin_universal_table_path(@table), html: {class: "form-horizontal main-forms"} do |f| %>
<%= render :partial => "table_form", locals: {f: f} %>
<%= form_for @table, url: admin_universal_table_path(@table), html: {class: "form-horizontal main-forms"} do |f| %>
<%= render :partial => "table_form", locals: {f: f} %>
<% end %>

12
app/views/admin/universal_tables/edit_entry.html.erb Normal file → Executable file
View File

@ -1,7 +1,7 @@
<%= form_for @entry, url: "/admin/universal_tables/update_entry", html: {class: "form-horizontal main-forms"} do |f| %>
<fieldset>
<%= f.hidden_field_tag 'id', f.object.id %>
<%= f.hidden_field_tag 'page', params[:page] %>
<%= render :partial => "entry_form", :locals => {:f => f} %>
</fieldset>
<%= form_for @entry, url: "/admin/universal_tables/update_entry", html: {class: "form-horizontal main-forms"} do |f| %>
<fieldset>
<%= f.hidden_field_tag 'id', f.object.id %>
<%= f.hidden_field_tag 'page', params[:page] %>
<%= render :partial => "entry_form", :locals => {:f => f} %>
</fieldset>
<% end %>

218
app/views/admin/universal_tables/edit_sort.html.erb Normal file → Executable file
View File

@ -1,109 +1,109 @@
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %>
<div style="margin-bottom: 1em;">
<button type="button" class="btn btn-primary" id="update_sort_button"><%= t('universal_table.manual_update_sort') %></button>
</div>
<%= render partial: 'edit_sort' %>
<script type="text/javascript">
function update_sort(){
var ids = $.map($('#sortable>tr'),function(v){return $(v).data('id')});
$.ajax({
url: "<%= admin_universal_table_update_sort_path(@table) %>",
type: 'POST',
dataType: 'text',
data: {ids: ids},
success: function(data){
$('#data-table').replaceWith(data);
sortable();
}
});
}
function sortable(){
$( "#sortable" ).sortable({
update: function( event, ui ) {
update_sort();
}
});
$('.sort_number').change(function(){
var new_sort_number = parseFloat($(this).val());
var min_number = $('.sort_number').length;
var max_number = 0;
var pool = $('.sort_number').not(this);
var same_order = pool.filter(function(){
var tmp_sort = parseFloat($(this).val());
if (tmp_sort<min_number){
min_number = tmp_sort;
}
if (tmp_sort>max_number){
max_number = tmp_sort;
}
return tmp_sort==new_sort_number
});
var tmp_same_order = null;
if (same_order.length>0){
tmp_same_order = same_order.eq(0);
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
}else{
//ex. 1 2 3 5,insert 4
if (parseInt(pool.eq(0).val())<parseInt(pool.eq(-1).val())){ //asc
$.each(pool,function(){
var tmp_sort = parseFloat($(this).val());
if (new_sort_number>max_number){
if (tmp_sort<=new_sort_number){
tmp_same_order = $(this);
}
}else{
if (tmp_sort>=new_sort_number){
tmp_same_order = $(this);
return false;
}
}
});
if (new_sort_number>max_number){
tmp_same_order.parents('tr').eq(0).after($(this).parents('tr').eq(0));
}else{
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
}
}else{ //desc
//ex. 5 3 2 1,insert 4
$.each(pool,function(){
var tmp_sort = parseFloat($(this).val());
if (new_sort_number<min_number){
if (tmp_sort>=new_sort_number){
tmp_same_order = $(this);
}
}else if (new_sort_number>max_number){
if (tmp_sort<=new_sort_number){
tmp_same_order = $(this);
return false;
}
}else{
if (tmp_sort<=new_sort_number){
tmp_same_order = $(this);
return false;
}
}
});
if (new_sort_number<min_number){
tmp_same_order.parents('tr').eq(0).after($(this).parents('tr').eq(0));
}else{
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
}
}
}
update_sort();
});
}
$(document).ready(function(){
$('#update_sort_button').click(update_sort);
sortable();
});
</script>
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %>
<div style="margin-bottom: 1em;">
<button type="button" class="btn btn-primary" id="update_sort_button"><%= t('universal_table.manual_update_sort') %></button>
</div>
<%= render partial: 'edit_sort' %>
<script type="text/javascript">
function update_sort(){
var ids = $.map($('#sortable>tr'),function(v){return $(v).data('id')});
$.ajax({
url: "<%= admin_universal_table_update_sort_path(@table) %>",
type: 'POST',
dataType: 'text',
data: {ids: ids},
success: function(data){
$('#data-table').replaceWith(data);
sortable();
}
});
}
function sortable(){
$( "#sortable" ).sortable({
update: function( event, ui ) {
update_sort();
}
});
$('.sort_number').change(function(){
var new_sort_number = parseFloat($(this).val());
var min_number = $('.sort_number').length;
var max_number = 0;
var pool = $('.sort_number').not(this);
var same_order = pool.filter(function(){
var tmp_sort = parseFloat($(this).val());
if (tmp_sort<min_number){
min_number = tmp_sort;
}
if (tmp_sort>max_number){
max_number = tmp_sort;
}
return tmp_sort==new_sort_number
});
var tmp_same_order = null;
if (same_order.length>0){
tmp_same_order = same_order.eq(0);
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
}else{
//ex. 1 2 3 5,insert 4
if (parseInt(pool.eq(0).val())<parseInt(pool.eq(-1).val())){ //asc
$.each(pool,function(){
var tmp_sort = parseFloat($(this).val());
if (new_sort_number>max_number){
if (tmp_sort<=new_sort_number){
tmp_same_order = $(this);
}
}else{
if (tmp_sort>=new_sort_number){
tmp_same_order = $(this);
return false;
}
}
});
if (new_sort_number>max_number){
tmp_same_order.parents('tr').eq(0).after($(this).parents('tr').eq(0));
}else{
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
}
}else{ //desc
//ex. 5 3 2 1,insert 4
$.each(pool,function(){
var tmp_sort = parseFloat($(this).val());
if (new_sort_number<min_number){
if (tmp_sort>=new_sort_number){
tmp_same_order = $(this);
}
}else if (new_sort_number>max_number){
if (tmp_sort<=new_sort_number){
tmp_same_order = $(this);
return false;
}
}else{
if (tmp_sort<=new_sort_number){
tmp_same_order = $(this);
return false;
}
}
});
if (new_sort_number<min_number){
tmp_same_order.parents('tr').eq(0).after($(this).parents('tr').eq(0));
}else{
tmp_same_order.parents('tr').eq(0).before($(this).parents('tr').eq(0));
}
}
}
update_sort();
});
}
$(document).ready(function(){
$('#update_sort_button').click(update_sort);
sortable();
});
</script>

View File

@ -3,63 +3,71 @@
wb = xlsx_package.workbook
wb.add_worksheet(name: "Structure") do |sheet|
heading = sheet.styles.add_style(:b => true, :locked => true)
type = sheet.styles.add_style(:i => true)
heading = sheet.styles.add_style(b: true, locked: true)
type = sheet.styles.add_style(i: true)
row = []
row1 = []
row2 = []
row = ['UID']
row1 = ['uid']
row2 = ['Use to update existing entries. Leave blank to create new.']
@table.table_columns.asc(:order).each do |column|
case column.type
when "text"
@site_in_use_locales.sort.each do |locale|
row << column.title + " - " + t(locale.to_s)
row1 << column.key
row2 << column.type + "-#{locale}"
end
when "integer"
row << column.title
row1 << column.key
row2 << column.type
when "editor"
@site_in_use_locales.sort.each do |locale|
row << column.title + " - " + t(locale.to_s)
row1 << column.key
row2 << column.type + "-#{locale}"
end
when "image"
row << column.title
row1 << column.key
row2 << "Public URL"
when "date"
row << column.title
row1 << column.key
row2 << column.type + " : " + column.date_format.upcase
when "period"
row << column.title + "-From"
row1 << column.key
row2 << column.type + " : " + column.date_format.upcase + "-period_from"
row << column.title + "-To"
row1 << column.key
row2 << column.type + " : " + column.date_format.upcase + "-period_to"
when "file"
row << column.title
row1 << column.key
row2 << "Separate the files by ;"
end
end
@table.table_columns.asc(:order).each do |column|
case column.type
when "text", "editor"
@site_in_use_locales.sort.each do |locale|
row << "#{column.title} - #{t(locale.to_s)}"
row1 << column.key
row2 << "#{column.type}-#{locale}"
end
row << t("universal_table.hashtags")
row1 << "table_tags"
row2 << "Separate tags by ;"
when "integer"
row << column.title
row1 << column.key
row2 << "integer"
row << t("universal_table.related_entries")
row1 << "related_entries"
row2 << "Separate UIDs with ;"
when "image"
row << column.title
row1 << column.key
row2 << "Public URL"
sheet.add_row row, :style => heading
sheet.add_row row1
sheet.add_row row2, :style => type
when "date"
row << column.title
row1 << column.key
row2 << "date : #{column.date_format.upcase}"
end
when "period"
row << "#{column.title}-From"
row1 << column.key
row2 << "period : #{column.date_format.upcase}-period_from"
row << "#{column.title}-To"
row1 << column.key
row2 << "period : #{column.date_format.upcase}-period_to"
when "file"
# 多語系 file_title 欄位
@site_in_use_locales.sort.each do |locale|
row << "#{column.title} - #{t(locale.to_s)}"
row1 << column.key
row2 << "Separate the files by"
end
# URL 欄位
row << "#{column.title} (註解)"
row1 << column.key
row2 << "file_title - #{locale} ;"
end # <-- 正確結束 case 區塊
end
# 加入 hashtags 與 related_entries 欄位
row << t("universal_table.hashtags")
row1 << "table_tags"
row2 << "Separate tags by ;"
row << t("universal_table.related_entries")
row1 << "related_entries"
row2 << "Separate UIDs with ;"
sheet.add_row row, style: heading
sheet.add_row row1
sheet.add_row row2, style: type
end

190
app/views/admin/universal_tables/index.html.erb Normal file → Executable file
View File

@ -1,96 +1,96 @@
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/jquery.form" %>
<% end %>
<div id="index_table">
<%= render 'index'%>
</div>
<script type="text/javascript">
$("form.import_from_excel_form").on("submit",function(){
var form = this;
if($(this).find("input[type=file]").val() != ""){
$(this).ajaxSubmit({
dataType : "json",
success : function(data){
if(data.success){
alert("Import successfull.")
$("tr#table_" + data.id + " td:eq(2)").text(data.count);
}else{
alert(data.msg);
}
form.reset();
}
})
}
return false;
})
</script>
<div id="downloadModal" data-backdrop="static" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="downloadModalLabel" aria-hidden="true">
<div class="modal-header">
<h3 id="downloadModalLabel">Download</h3>
</div>
<div class="modal-body">
<p id="wait-zone" style="text-align: center;">
Please wait while we prepare your download. This may take a while.
<br />
<img src="/assets/spin.gif" />
</p>
<p id="link-zone" style="display: none; text-align: center;">
Please click the link below to download.
<br />
<a href="" id="download-link" target="_blank">Download</a>
</p>
</div>
<div class="modal-footer">
<button class="btn" id="modal-close-btn" style="display:none;" data-dismiss="modal" aria-hidden="true">Close</button>
</div>
</div>
<script type="text/javascript" src="/assets/lib/process.manager.js"></script>
<script type="text/javascript">
var downloadModal = $("#downloadModal"),
checkForThread = null,
waitZone = $("#wait-zone"),
linkZone = $("#link-zone"),
downloadLink = $("a#download-link"),
modalBtn = $("#modal-close-btn"),
processManager = new ProcessManager();
$(document).on("click", ".export-xls", function(){
var link = $(this).attr("href"),
title = null,
id = $(this).data("table-id");
linkZone.hide();
waitZone.show();
modalBtn.hide();
$.ajax({
url : link,
type : "get",
dataType : "json"
}).done(function(data){
title = data.title;
checkForThread = new Process(function(){
$.ajax({
url : "/admin/universal_tables/checkforthread",
type : "get",
data : {"utable_id" : id, "utable_title" : title},
dataType : "json"
}).done(function(data){
if(!data.status){
downloadLink.attr("href", "/uploads/utable_export/" + id + "/" + title + ".xlsx");
waitZone.hide();
linkZone.show();
modalBtn.show();
checkForThread.kill();
}
})
})
checkForThread.setTimeInterval(1000);
checkForThread.setRepeat(Process.CONSTANTS.REPEAT_INFINITE);
processManager.queue(checkForThread);
})
downloadModal.modal("show");
return false;
})
<% content_for :page_specific_javascript do %>
<%= javascript_include_tag "lib/jquery.form" %>
<% end %>
<div id="index_table">
<%= render 'index'%>
</div>
<script type="text/javascript">
$("form.import_from_excel_form").on("submit",function(){
var form = this;
if($(this).find("input[type=file]").val() != ""){
$(this).ajaxSubmit({
dataType : "json",
success : function(data){
if(data.success){
alert("Import successfull.")
$("tr#table_" + data.id + " td:eq(2)").text(data.count);
}else{
alert(data.msg);
}
form.reset();
}
})
}
return false;
})
</script>
<div id="downloadModal" data-backdrop="static" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="downloadModalLabel" aria-hidden="true">
<div class="modal-header">
<h3 id="downloadModalLabel">Download</h3>
</div>
<div class="modal-body">
<p id="wait-zone" style="text-align: center;">
Please wait while we prepare your download. This may take a while.
<br />
<img src="/assets/spin.gif" />
</p>
<p id="link-zone" style="display: none; text-align: center;">
Please click the link below to download.
<br />
<a href="" id="download-link" target="_blank">Download</a>
</p>
</div>
<div class="modal-footer">
<button class="btn" id="modal-close-btn" style="display:none;" data-dismiss="modal" aria-hidden="true">Close</button>
</div>
</div>
<script type="text/javascript" src="/assets/lib/process.manager.js"></script>
<script type="text/javascript">
var downloadModal = $("#downloadModal"),
checkForThread = null,
waitZone = $("#wait-zone"),
linkZone = $("#link-zone"),
downloadLink = $("a#download-link"),
modalBtn = $("#modal-close-btn"),
processManager = new ProcessManager();
$(document).on("click", ".export-xls", function(){
var link = $(this).attr("href"),
title = null,
id = $(this).data("table-id");
linkZone.hide();
waitZone.show();
modalBtn.hide();
$.ajax({
url : link,
type : "get",
dataType : "json"
}).done(function(data){
title = data.title;
checkForThread = new Process(function(){
$.ajax({
url : "/admin/universal_tables/checkforthread",
type : "get",
data : {"utable_id" : id, "utable_title" : title},
dataType : "json"
}).done(function(data){
if(!data.status){
downloadLink.attr("href", "/uploads/utable_export/" + id + "/" + title + ".xlsx");
waitZone.hide();
linkZone.show();
modalBtn.show();
checkForThread.kill();
}
})
})
checkForThread.setTimeInterval(1000);
checkForThread.setRepeat(Process.CONSTANTS.REPEAT_INFINITE);
processManager.queue(checkForThread);
})
downloadModal.modal("show");
return false;
})
</script>

8
app/views/admin/universal_tables/new.html.erb Normal file → Executable file
View File

@ -1,5 +1,5 @@
<%= form_for @table, url: admin_universal_tables_path, html: {class: "form-horizontal main-forms"} do |f| %>
<%= render :partial => "table_form", locals: {f: f} %>
<%= form_for @table, url: admin_universal_tables_path, html: {class: "form-horizontal main-forms"} do |f| %>
<%= render :partial => "table_form", locals: {f: f} %>
<% end %>

10
app/views/admin/universal_tables/new_entry.html.erb Normal file → Executable file
View File

@ -1,6 +1,6 @@
<%= form_for @entry, url: "/admin/universal_tables/add_entry", html: {class: "form-horizontal main-forms"} do |f| %>
<fieldset>
<%= f.hidden_field :u_table_id, :value => @table.id %>
<%= render :partial => "entry_form", :locals => {:f => f} %>
</fieldset>
<%= form_for @entry, url: "/admin/universal_tables/add_entry", html: {class: "form-horizontal main-forms"} do |f| %>
<fieldset>
<%= f.hidden_field :u_table_id, :value => @table.id %>
<%= render :partial => "entry_form", :locals => {:f => f} %>
</fieldset>
<% end %>

416
app/views/admin/universal_tables/show.html.erb Normal file → Executable file
View File

@ -1,209 +1,209 @@
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %>
<style>
#entry-status{
float: right;
margin-right: 1em;
}
.toggle_entries[data-status="show"] {
border-radius: 4px;
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
font-family: 'Varela Round';
letter-spacing: -.4px;
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
background-color: #007AFF;
margin-right: 0.5em;
&:hover{
background-color:#1A73E8;
}
}
.toggle_entries[data-status="hide"] {
border-radius: 4px;
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
font-family: 'Varela Round';
letter-spacing: -.4px;
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
background-color: #5F6368;
margin-right: 0.5em;
&:hover{
background-color:#4b4e53;
}
}
.hidden_entry{
td{
background-color: #9eacb5a8;
}
}
</style>
<form class="form-search" action="<%= admin_universal_table_path(@table) %>" method="get">
<input type="text" name="q" class="input-large search-query" placeholder="Search from text or editor columns">
<button type="submit" class="btn btn-primary">Search</button>
<% if params[:q].present? %>
<a href="<%= admin_universal_table_path(@table) %>" class="btn btn-info">Reset</a>
<% end %>
</form>
<div id="data-table" class="ut-table">
<table class="table main-list">
<thead>
<tr class="sort-header">
<th width="15">
<%= t(:status) %>
<div class="status hide" id="entry-status">
<a href="" class="toggle_entries" data-status="hide"><%= t(:hide) %></a>
<a href="" class="toggle_entries" data-status="show"><%= t(:show) %></a>
</div>
</th>
<% @table_fields.each do |field| %>
<%
sort = field.to_s.include?('.') ? field.to_s.split('.')[1] : field.to_s
active = params[:sort].eql? sort
order = active ? (["asc", "desc"]-[params[:order]]).first : "asc"
arrow = (order.eql? "desc") ? "<b class='icons-arrow-up-3'></b>" : "<b class='icons-arrow-down-4'></b>"
klass = field.eql?(:title) ? "span5" : "span2"
th_data = "<a href='?sort=#{sort}&order=#{order}'>#{field} #{active ? arrow : ""}</a>"
%>
<th class='<%= klass %> <%= active ? "active" : "" %>'><%= th_data.html_safe %></th>
<% end %>
</tr>
</thead>
<tbody>
<% can_edit = can_edit_or_delete?(@table) %>
<% @entries.each do |entry| %>
<tr id="tr_entry_<%= entry.id.to_s %>" class="<%= entry.is_hidden? ? 'hidden_entry' : '' %>">
<td><input class="hide-toggle" type="checkbox" value="<%= entry.id.to_s %>" /></td>
<% @columns.each_with_index do |column, index| %>
<% ce = entry.column_entries.where(:table_column_id => column.id).first rescue nil %>
<td>
<% if !ce.nil? %>
<% case ce.type %>
<% when "text" %>
<%= ce.text %>
<% when "integer" %>
<%= ce.number %>
<% when "editor" %>
<%= ce.content.html_safe rescue "" %>
<% when "image" %>
<div class="image-expander">
<% if !ce.image.nil? %>
<a href="<%= ce.image.url %>" target="_blank"><img src="<%= ce.image.thumb.url %>" class="image-preview" /></a>
<% end %>
</div>
<% when "date" %>
<%= format_date(ce.date, column.date_format) %>
<% when "period" %>
<% if !ce.period_from.nil? %>
<%= format_date(ce.period_from, column.date_format) %> ~ <%= format_date(ce.period_to, column.date_format) %>
<% end %>
<% when "file" %>
<% locale = I18n.locale.to_s %>
<ol>
<% ce.column_entry_files.desc(:sort_number).each do |entry_file| %>
<% next unless entry_file.choose_lang_display(locale) %>
<% if entry_file.file.content_type.start_with?('audio/') %>
<%= entry_file.get_file_title %>
<a class="voice-player" data-content="<%= entry_file.file.url %>" href="" title="播放讀音"><i class="fa fa-play" aria-hidden="true"></i></a>
<% else %>
<li><%= link_to entry_file.get_file_title, entry_file.file.url, target: "_blank" %></li>
<% end %>
<% end %>
</ol>
<% end %>
<% else %>
&nbsp;
<% end %>
<% if index == 0 && can_edit %>
<div class="quick-edit">
<ul class="nav nav-pills">
<li><a href="<%= admin_universal_table_edit_entry_path(:universal_table_id=> entry.to_param, :page => params[:page]) %>"><%= t(:edit) %></a></li>
<li><a href="<%= admin_universal_table_delete_entry_path(entry.id) %>" class="delete text-error" data-method="delete" data-confirm="Are you sure?"><%= t(:delete_) %></a></li>
</ul>
</div>
<% end %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="bottomnav clearfix">
<%= content_tag :div, paginate(@entries), class: "pagination pagination-centered" %>
<div class="action pull-right">
<a href="<%= admin_universal_table_new_entry_path(@table) %>" class="btn btn-primary" role="button" data-toggle="modal">Add Entry</a>
</div>
</div>
<script>
$(".action").after($("#entry-status"));
let audio;
$(".voice-player").on("click", function(){
let status = $(this).attr('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status == 'playing') {
$(this).attr('status', '');
$(this).find('i').removeClass('fa-pause');
$(this).find('i').addClass('fa-play');
} else {
let mp3_url = $(this).attr('data-content');
let _this = $(this);
audio = new Audio(mp3_url);
audio.play();
audio.onended = function() {
_this.attr('status', '');
_this.find('i').removeClass('fa-pause');
_this.find('i').addClass('fa-play');
};
$(this).find('i').removeClass('fa-play');
$(this).find('i').addClass('fa-pause');
$(this).attr('status', 'playing');
}
return false;
})
$(".hide-toggle").on("click", function(){
var count = $(".hide-toggle:checked").length;
if(count > 0){
$("#entry-status").removeClass("hide")
}else{
$("#entry-status").addClass("hide");
}
})
$(".toggle_entries").on("click", function(){
let checkedValues = $('.hide-toggle:checked').map(function() {
return $(this).val();
}).get();
let status = $(this).data("status");
$.ajax({
url: "/admin/universal_tables/toggle_entries",
method: "post",
data: {"ids": checkedValues, "status": status},
type: "json"
}).done(function(){
if(status == "hide"){
$('.hide-toggle:checked').parents("tr").addClass("hidden_entry");
}else{
$('.hide-toggle:checked').parents("tr").removeClass("hidden_entry");
}
})
return false;
})
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "universal_table/universal-table" %>
<% end %>
<style>
#entry-status{
float: right;
margin-right: 1em;
}
.toggle_entries[data-status="show"] {
border-radius: 4px;
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
font-family: 'Varela Round';
letter-spacing: -.4px;
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
background-color: #007AFF;
margin-right: 0.5em;
&:hover{
background-color:#1A73E8;
}
}
.toggle_entries[data-status="hide"] {
border-radius: 4px;
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
font-family: 'Varela Round';
letter-spacing: -.4px;
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
background-color: #5F6368;
margin-right: 0.5em;
&:hover{
background-color:#4b4e53;
}
}
.hidden_entry{
td{
background-color: #9eacb5a8;
}
}
</style>
<form class="form-search" action="<%= admin_universal_table_path(@table) %>" method="get">
<input type="text" name="q" class="input-large search-query" placeholder="Search from text or editor columns">
<button type="submit" class="btn btn-primary">Search</button>
<% if params[:q].present? %>
<a href="<%= admin_universal_table_path(@table) %>" class="btn btn-info">Reset</a>
<% end %>
</form>
<div id="data-table" class="ut-table">
<table class="table main-list">
<thead>
<tr class="sort-header">
<th width="15">
<%= t(:status) %>
<div class="status hide" id="entry-status">
<a href="" class="toggle_entries" data-status="hide"><%= t(:hide) %></a>
<a href="" class="toggle_entries" data-status="show"><%= t(:show) %></a>
</div>
</th>
<% @table_fields.each do |field| %>
<%
sort = field.to_s.include?('.') ? field.to_s.split('.')[1] : field.to_s
active = params[:sort].eql? sort
order = active ? (["asc", "desc"]-[params[:order]]).first : "asc"
arrow = (order.eql? "desc") ? "<b class='icons-arrow-up-3'></b>" : "<b class='icons-arrow-down-4'></b>"
klass = field.eql?(:title) ? "span5" : "span2"
th_data = "<a href='?sort=#{sort}&order=#{order}'>#{field} #{active ? arrow : ""}</a>"
%>
<th class='<%= klass %> <%= active ? "active" : "" %>'><%= th_data.html_safe %></th>
<% end %>
</tr>
</thead>
<tbody>
<% can_edit = can_edit_or_delete?(@table) %>
<% @entries.each do |entry| %>
<tr id="tr_entry_<%= entry.id.to_s %>" class="<%= entry.is_hidden? ? 'hidden_entry' : '' %>">
<td><input class="hide-toggle" type="checkbox" value="<%= entry.id.to_s %>" /></td>
<% @columns.each_with_index do |column, index| %>
<% ce = entry.column_entries.where(:table_column_id => column.id).first rescue nil %>
<td>
<% if !ce.nil? %>
<% case ce.type %>
<% when "text" %>
<%= ce.text %>
<% when "integer" %>
<%= ce.number %>
<% when "editor" %>
<%= ce.content.html_safe rescue "" %>
<% when "image" %>
<div class="image-expander">
<% if !ce.image.nil? %>
<a href="<%= ce.image.url %>" target="_blank"><img src="<%= ce.image.thumb.url %>" class="image-preview" /></a>
<% end %>
</div>
<% when "date" %>
<%= format_date(ce.date, column.date_format) %>
<% when "period" %>
<% if !ce.period_from.nil? %>
<%= format_date(ce.period_from, column.date_format) %> ~ <%= format_date(ce.period_to, column.date_format) %>
<% end %>
<% when "file" %>
<% locale = I18n.locale.to_s %>
<ol>
<% ce.column_entry_files.desc(:sort_number).each do |entry_file| %>
<% next unless entry_file.choose_lang_display(locale) %>
<% if entry_file.file.content_type.start_with?('audio/') %>
<%= entry_file.get_file_title %>
<a class="voice-player" data-content="<%= entry_file.file.url %>" href="" title="播放讀音"><i class="fa fa-play" aria-hidden="true"></i></a>
<% else %>
<li><%= link_to entry_file.get_file_title, entry_file.file.url, target: "_blank" %></li>
<% end %>
<% end %>
</ol>
<% end %>
<% else %>
&nbsp;
<% end %>
<% if index == 0 && can_edit %>
<div class="quick-edit">
<ul class="nav nav-pills">
<li><a href="<%= admin_universal_table_edit_entry_path(:universal_table_id=> entry.to_param, :page => params[:page]) %>"><%= t(:edit) %></a></li>
<li><a href="<%= admin_universal_table_delete_entry_path(entry.id) %>" class="delete text-error" data-method="delete" data-confirm="Are you sure?"><%= t(:delete_) %></a></li>
</ul>
</div>
<% end %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="bottomnav clearfix">
<%= content_tag :div, paginate(@entries), class: "pagination pagination-centered" %>
<div class="action pull-right">
<a href="<%= admin_universal_table_new_entry_path(@table) %>" class="btn btn-primary" role="button" data-toggle="modal">Add Entry</a>
</div>
</div>
<script>
$(".action").after($("#entry-status"));
let audio;
$(".voice-player").on("click", function(){
let status = $(this).attr('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status == 'playing') {
$(this).attr('status', '');
$(this).find('i').removeClass('fa-pause');
$(this).find('i').addClass('fa-play');
} else {
let mp3_url = $(this).attr('data-content');
let _this = $(this);
audio = new Audio(mp3_url);
audio.play();
audio.onended = function() {
_this.attr('status', '');
_this.find('i').removeClass('fa-pause');
_this.find('i').addClass('fa-play');
};
$(this).find('i').removeClass('fa-play');
$(this).find('i').addClass('fa-pause');
$(this).attr('status', 'playing');
}
return false;
})
$(".hide-toggle").on("click", function(){
var count = $(".hide-toggle:checked").length;
if(count > 0){
$("#entry-status").removeClass("hide")
}else{
$("#entry-status").addClass("hide");
}
})
$(".toggle_entries").on("click", function(){
let checkedValues = $('.hide-toggle:checked').map(function() {
return $(this).val();
}).get();
let status = $(this).data("status");
$.ajax({
url: "/admin/universal_tables/toggle_entries",
method: "post",
data: {"ids": checkedValues, "status": status},
type: "json"
}).done(function(){
if(status == "hide"){
$('.hide-toggle:checked').parents("tr").addClass("hidden_entry");
}else{
$('.hide-toggle:checked').parents("tr").removeClass("hidden_entry");
}
})
return false;
})
</script>

0
app/views/admin/universal_tables/update_sort.html.erb Normal file → Executable file
View File

292
app/views/universal_tables/download_file.html.erb Normal file → Executable file
View File

@ -1,147 +1,147 @@
<% if @ext == 'pdf' %>
<%= render partial: 'archives/viewer' %>
<% else %>
<html lang="<%= I18n.locale.to_s%>" style="margin: 0em; padding: 0em; width: 100%; height: 100%; overflow: hidden; background-color: rgb(230, 230, 230);">
<head>
<meta name="viewport" content="width=device-width, minimum-scale=0.1">
<title><%=@filename%></title>
<%= stylesheet_link_tag "archive/download_file.css" %>
</head>
<body>
<h1 style="display: none;"><%=@filename%></h1>
<% if @ext != "png" && @ext != "jpg" && @ext != "bmp" %>
<object data="<%=@url%>" height="100%" type="application/<%=@ext%>" width="100%">
<iframe height="100%" src="<%=@url%>" title="<%=@filename%>" width="100%"></iframe>
<img alt="<%=@filename%>" src="<%=@url%>">
</object>
<% else %>
<img alt="<%=@filename%>" src="<%=@url%>">
<script type="text/javascript">
var img = document.getElementsByTagName('img')[0];
var width = img.width;
var height = img.height;
window.innerWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
window.innerHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
var window_width = window.innerWidth;
var window_height = window.innerHeight;
var zoom_in_cursor,zoom_out_cursor;
var IE_ver = 11;
if(navigator.userAgent.search("MSIE") != -1){
IE_ver = Number(navigator.userAgent.split("MSIE")[1].split(";")[0]);
}
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}else{
img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)";
}
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
zoom_in_cursor = 'url("/assets/archive/zoomin.cur"), auto';
zoom_out_cursor = 'url("/assets/archive/zoomout.cur"), auto';
}else{
zoom_in_cursor = 'zoom-in';
zoom_out_cursor = 'zoom-out';
}
if(height > window_height && (height / width) > (window_height / window_width) ){
img.height = window_height;
img.width = window_height / height * width;
img.style.cursor = zoom_in_cursor;
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}
img.onclick=function(e){
var event = e || window.event;
if(img.style.cursor == zoom_in_cursor){
var cursor_x = event.clientX;
var cursor_y = event.clientY;
img.height = height;
img.width = width;
img.style.cursor = zoom_out_cursor;
document.getElementsByTagName('html')[0].style.overflow = "";
img.style.transform= "none";
img.style["-ms-transform"]= "none";
img.style["-moz-transform"]= "none";
img.style.top= "0";
img.style.left= "0";
if(IE_ver <= 8){
img.style.marginTop = "0";
img.style.marginLeft = "0";
}
window.scroll(
(((cursor_x - (window_width - window_height / height * width)/2) * height / window_height) - window_width / 2),
((cursor_y * height / window_height) - window_height / 2)
);
}else{
img.height = window_height;
img.width = window_height / height * width;
img.style.cursor = zoom_in_cursor;
document.getElementsByTagName('html')[0].style.overflow = "hidden";
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}else{
img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)";
}
img.style.top= "50%";
img.style.left= "50%";
window.scroll(0, 0);
}
};
}else if(width > window_width){
img.width = window_width;
img.height = window_width / width * height;
img.style.cursor = zoom_in_cursor;
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}
img.onclick=function(e){
var event = e || window.event;
if(img.style.cursor == zoom_in_cursor){
var cursor_x = event.clientX;
var cursor_y = event.clientY;
img.height = height;
img.width = width;
img.style.cursor = zoom_out_cursor;
document.getElementsByTagName('html')[0].style.overflow = "";
img.style.transform= "none";
img.style["-ms-transform"]= "none";
img.style["-moz-transform"]= "none";
img.style.top= "0";
img.style.left= "0";
if(IE_ver <= 8){
img.style.marginTop = "0";
img.style.marginLeft = "0";
}
window.scroll( ((cursor_x * height / window_height) - window_width / 2),
(((cursor_y - (window_height - window_width / width * height)/2) * height / window_height) - window_height / 2)
);
}else{
img.width = window_width;
img.height = window_width / width * height;
img.style.cursor = zoom_in_cursor;
document.getElementsByTagName('html')[0].style.overflow = "hidden";
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}else{
img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)";
}
img.style.top= "50%";
img.style.left= "50%";
window.scroll(0, 0);
}
};
}
</script>
<% end %>
</body>
</html>
<% if @ext == 'pdf' %>
<%= render partial: 'archives/viewer' %>
<% else %>
<html lang="<%= I18n.locale.to_s%>" style="margin: 0em; padding: 0em; width: 100%; height: 100%; overflow: hidden; background-color: rgb(230, 230, 230);">
<head>
<meta name="viewport" content="width=device-width, minimum-scale=0.1">
<title><%=@filename%></title>
<%= stylesheet_link_tag "archive/download_file.css" %>
</head>
<body>
<h1 style="display: none;"><%=@filename%></h1>
<% if @ext != "png" && @ext != "jpg" && @ext != "bmp" %>
<object data="<%=@url%>" height="100%" type="application/<%=@ext%>" width="100%">
<iframe height="100%" src="<%=@url%>" title="<%=@filename%>" width="100%"></iframe>
<img alt="<%=@filename%>" src="<%=@url%>">
</object>
<% else %>
<img alt="<%=@filename%>" src="<%=@url%>">
<script type="text/javascript">
var img = document.getElementsByTagName('img')[0];
var width = img.width;
var height = img.height;
window.innerWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
window.innerHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
var window_width = window.innerWidth;
var window_height = window.innerHeight;
var zoom_in_cursor,zoom_out_cursor;
var IE_ver = 11;
if(navigator.userAgent.search("MSIE") != -1){
IE_ver = Number(navigator.userAgent.split("MSIE")[1].split(";")[0]);
}
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}else{
img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)";
}
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
zoom_in_cursor = 'url("/assets/archive/zoomin.cur"), auto';
zoom_out_cursor = 'url("/assets/archive/zoomout.cur"), auto';
}else{
zoom_in_cursor = 'zoom-in';
zoom_out_cursor = 'zoom-out';
}
if(height > window_height && (height / width) > (window_height / window_width) ){
img.height = window_height;
img.width = window_height / height * width;
img.style.cursor = zoom_in_cursor;
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}
img.onclick=function(e){
var event = e || window.event;
if(img.style.cursor == zoom_in_cursor){
var cursor_x = event.clientX;
var cursor_y = event.clientY;
img.height = height;
img.width = width;
img.style.cursor = zoom_out_cursor;
document.getElementsByTagName('html')[0].style.overflow = "";
img.style.transform= "none";
img.style["-ms-transform"]= "none";
img.style["-moz-transform"]= "none";
img.style.top= "0";
img.style.left= "0";
if(IE_ver <= 8){
img.style.marginTop = "0";
img.style.marginLeft = "0";
}
window.scroll(
(((cursor_x - (window_width - window_height / height * width)/2) * height / window_height) - window_width / 2),
((cursor_y * height / window_height) - window_height / 2)
);
}else{
img.height = window_height;
img.width = window_height / height * width;
img.style.cursor = zoom_in_cursor;
document.getElementsByTagName('html')[0].style.overflow = "hidden";
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}else{
img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)";
}
img.style.top= "50%";
img.style.left= "50%";
window.scroll(0, 0);
}
};
}else if(width > window_width){
img.width = window_width;
img.height = window_width / width * height;
img.style.cursor = zoom_in_cursor;
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}
img.onclick=function(e){
var event = e || window.event;
if(img.style.cursor == zoom_in_cursor){
var cursor_x = event.clientX;
var cursor_y = event.clientY;
img.height = height;
img.width = width;
img.style.cursor = zoom_out_cursor;
document.getElementsByTagName('html')[0].style.overflow = "";
img.style.transform= "none";
img.style["-ms-transform"]= "none";
img.style["-moz-transform"]= "none";
img.style.top= "0";
img.style.left= "0";
if(IE_ver <= 8){
img.style.marginTop = "0";
img.style.marginLeft = "0";
}
window.scroll( ((cursor_x * height / window_height) - window_width / 2),
(((cursor_y - (window_height - window_width / width * height)/2) * height / window_height) - window_height / 2)
);
}else{
img.width = window_width;
img.height = window_width / width * height;
img.style.cursor = zoom_in_cursor;
document.getElementsByTagName('html')[0].style.overflow = "hidden";
if(IE_ver <= 8){
img.style.marginTop = "-"+img.height/2+"px";
img.style.marginLeft = "-"+img.width/2+"px";
}else{
img.style.transform= "translate(-50%, -50%)";
img.style["-ms-transform"]= "translate(-50%, -50%)";
img.style["-moz-transform"]= "translate(-50%, -50%)";
}
img.style.top= "50%";
img.style.left= "50%";
window.scroll(0, 0);
}
};
}
</script>
<% end %>
</body>
</html>
<% end %>

38
app/views/universal_tables/export_filtered.xlsx.axlsx Normal file → Executable file
View File

@ -1,20 +1,20 @@
# encoding: utf-8
wb = xlsx_package.workbook
wb.add_worksheet(name: "Table") do |sheet|
heading = sheet.styles.add_style(:b => true, :locked => true)
headings = @tablecolumns.collect{|tc| tc.title}
sheet.add_row headings, :style => heading
wrap = sheet.styles.add_style alignment: {wrap_text: true}
@rows.each do |r|
row = []
r["columns"].each do |col|
row << col["text"]
end
sheet.add_row row, style: wrap
end
# encoding: utf-8
wb = xlsx_package.workbook
wb.add_worksheet(name: "Table") do |sheet|
heading = sheet.styles.add_style(:b => true, :locked => true)
headings = @tablecolumns.collect{|tc| tc.title}
sheet.add_row headings, :style => heading
wrap = sheet.styles.add_style alignment: {wrap_text: true}
@rows.each do |r|
row = []
r["columns"].each do |col|
row << col["text"]
end
sheet.add_row row, style: wrap
end
end

60
app/views/universal_tables/index.html.erb Normal file → Executable file
View File

@ -1,31 +1,31 @@
<%= render_view %>
<script>
let audio;
$(".voice-player").on("click", function(){
let status = $(this).attr('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status == 'playing') {
$(this).attr('status', '');
$(this).find('i').removeClass('fa-pause');
$(this).find('i').addClass('fa-play');
} else {
let mp3_url = $(this).attr('data-content');
let _this = $(this);
audio = new Audio(mp3_url);
audio.play();
audio.onended = function() {
_this.attr('status', '');
_this.find('i').removeClass('fa-pause');
_this.find('i').addClass('fa-play');
};
$(this).find('i').removeClass('fa-play');
$(this).find('i').addClass('fa-pause');
$(this).attr('status', 'playing');
}
return false;
})
<%= render_view %>
<script>
let audio;
$(".voice-player").on("click", function(){
let status = $(this).attr('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status == 'playing') {
$(this).attr('status', '');
$(this).find('i').removeClass('fa-pause');
$(this).find('i').addClass('fa-play');
} else {
let mp3_url = $(this).attr('data-content');
let _this = $(this);
audio = new Audio(mp3_url);
audio.play();
audio.onended = function() {
_this.attr('status', '');
_this.find('i').removeClass('fa-pause');
_this.find('i').addClass('fa-play');
};
$(this).find('i').removeClass('fa-play');
$(this).find('i').addClass('fa-pause');
$(this).attr('status', 'playing');
}
return false;
})
</script>

126
app/views/universal_tables/mind_map.html.erb Normal file → Executable file
View File

@ -1,64 +1,64 @@
<%
data = action_data
OrbitHelper.render_css_in_head(["mind_map/mindmap"])
%>
<h2><%= data["title"] %></h2>
<div id="jsmind_container"></div>
<script type="module">
import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯
// Control whether the mind map is editable
let isEditable = false
// 心智圖實例
// Mind map instance
let jm
// 心智圖初始數據
// Initial mind map data
let mind = {
meta: {},
format: 'node_array',
data: <%= raw data["mind_map_data"].to_json %>
}
// 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation)
const options = {
container: 'jsmind_container',
support_html: true,
editable: isEditable,
theme: 'primary',
mode: 'full',
tableUID: '',
text: {
addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>"
},
view: {
engine: 'svg',
draggable: true,
node_overflow: 'wrap',
},
shortcut: {
mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13,
},
},
}
// 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable)
<%
data = action_data
OrbitHelper.render_css_in_head(["mind_map/mindmap"])
%>
<h2><%= data["title"] %></h2>
<div id="jsmind_container"></div>
<script type="module">
import '/assets/mind_map/utils/custom.overrides.js'
import '/assets/mind_map/jsmind/plugins/jsmind.draggable-node.js'
import { initJsmind, getJsmindData } from '/assets/mind_map/utils/custom.main.js'
import { INITIAL_MIND } from '/assets/mind_map/utils/custom.config.js'
// 操控心智圖是否可編輯
// Control whether the mind map is editable
let isEditable = false
// 心智圖實例
// Mind map instance
let jm
// 心智圖初始數據
// Initial mind map data
let mind = {
meta: {},
format: 'node_array',
data: <%= raw data["mind_map_data"].to_json %>
}
// 心智圖自訂選項(可參考 jsmind 官方文檔)
// Custom options for the mind map (refer to the jsmind official documentation)
const options = {
container: 'jsmind_container',
support_html: true,
editable: isEditable,
theme: 'primary',
mode: 'full',
tableUID: '',
text: {
addNode: "<%= t("universal_table.add_node") %>",
deleteNode: "<%= t("universal_table.delete_node") %>",
strokeColor: "<%= t("universal_table.stroke_color") %>",
bgColor: "<%= t("universal_table.bg_color") %>",
textColor: "<%= t("universal_table.text_color") %>"
},
view: {
engine: 'svg',
draggable: true,
node_overflow: 'wrap',
},
shortcut: {
mapping: {
// 避免與 Toolbar 按下 Enter 事件衝突
// Avoid conflicts with the Enter key event in the Toolbar
addbrother: 2048 + 13,
},
},
}
// 初始化心智圖並掛載實例
// Initialize the mind map and attach the instance
jm = initJsmind(mind, options, isEditable)
</script>

32
app/views/universal_tables/redirect_to_file.html.erb Normal file → Executable file
View File

@ -1,17 +1,17 @@
<html lang="<%= I18n.locale.to_s%>">
<head>
<title><%=@filename%></title>
<link href="/assets/archive/download_file.css" rel="stylesheet" media="all">
</head>
<body style="background: #fff;">
<div class="wrap_block">
<h1 style="display: none;"><%=@filename%></h1>
<% download_text = t('download') + " " + @filename %>
<h2 align="center"><a href="<%=@url%>" target="blank" download="<%=@filename%>" title="<%=download_text%>"><%=download_text%></a></h2>
<p align="center">
<a href="javascript:window.close();" title="<%=t('close')%>"><%=t('close')%></a>
</p>
</div>
<script>window.location.href="<%=@url%>";</script>
</body>
<html lang="<%= I18n.locale.to_s%>">
<head>
<title><%=@filename%></title>
<link href="/assets/archive/download_file.css" rel="stylesheet" media="all">
</head>
<body style="background: #fff;">
<div class="wrap_block">
<h1 style="display: none;"><%=@filename%></h1>
<% download_text = t('download') + " " + @filename %>
<h2 align="center"><a href="<%=@url%>" target="blank" download="<%=@filename%>" title="<%=download_text%>"><%=download_text%></a></h2>
<p align="center">
<a href="javascript:window.close();" title="<%=t('close')%>"><%=t('close')%></a>
</p>
</div>
<script>window.location.href="<%=@url%>";</script>
</body>
</html>

60
app/views/universal_tables/show.html.erb Normal file → Executable file
View File

@ -1,31 +1,31 @@
<%= render_view %>
<script>
let audio;
$(".voice-player").on("click", function(){
let status = $(this).attr('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status == 'playing') {
$(this).attr('status', '');
$(this).find('i').removeClass('fa-pause');
$(this).find('i').addClass('fa-play');
} else {
let mp3_url = $(this).attr('data-content');
let _this = $(this);
audio = new Audio(mp3_url);
audio.play();
audio.onended = function() {
_this.attr('status', '');
_this.find('i').removeClass('fa-pause');
_this.find('i').addClass('fa-play');
};
$(this).find('i').removeClass('fa-play');
$(this).find('i').addClass('fa-pause');
$(this).attr('status', 'playing');
}
return false;
})
<%= render_view %>
<script>
let audio;
$(".voice-player").on("click", function(){
let status = $(this).attr('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status == 'playing') {
$(this).attr('status', '');
$(this).find('i').removeClass('fa-pause');
$(this).find('i').addClass('fa-play');
} else {
let mp3_url = $(this).attr('data-content');
let _this = $(this);
audio = new Audio(mp3_url);
audio.play();
audio.onended = function() {
_this.attr('status', '');
_this.find('i').removeClass('fa-pause');
_this.find('i').addClass('fa-play');
};
$(this).find('i').removeClass('fa-play');
$(this).find('i').addClass('fa-pause');
$(this).attr('status', 'playing');
}
return false;
})
</script>

235
app/views/utable_export/export.xlsx.axlsx Normal file → Executable file
View File

@ -3,127 +3,132 @@
wb = xlsx_package.workbook
wb.add_worksheet(name: "Structure") do |sheet|
heading = sheet.styles.add_style(:b => true, :locked => true)
type = sheet.styles.add_style(:i => true)
wrap = sheet.styles.add_style alignment: {wrap_text: true}
heading = sheet.styles.add_style(b: true, locked: true)
type = sheet.styles.add_style(i: true)
wrap = sheet.styles.add_style alignment: { wrap_text: true }
row = []
row1 = []
row2 = []
row = []
row1 = []
row2 = []
row << "UID"
row1 << "uid"
row2 << "uid"
row << "UID"
row1 << "uid"
row2 << "uid"
table.table_columns.asc(:order).each do |column|
case column.type
when "text"
site_in_use_locales.sort.each do |locale|
row << column.title + " - " + t(locale.to_s)
row1 << column.key
row2 << column.type + "-#{locale}"
end
when "integer"
row << column.title
row1 << column.key
row2 << column.type
when "editor"
site_in_use_locales.sort.each do |locale|
row << column.title + " - " + t(locale.to_s)
row1 << column.key
row2 << column.type + "-#{locale}"
end
when "image"
row << column.title
row1 << column.key
row2 << "Public URL"
when "date"
row << column.title
row1 << column.key
row2 << column.type + " : " + column.date_format.upcase
when "period"
row << column.title + "-From ~ To"
row1 << column.key
row2 << column.type + " : " + column.date_format.upcase + "-period_from ~ period_to"
when "file"
row << column.title
row1 << column.key
row2 << "Separate the files by ;"
end
end
table.table_columns.asc(:order).each do |column|
case column.type
when "text", "editor"
site_in_use_locales.sort.each do |locale|
row << "#{column.title} - #{t(locale.to_s)}"
row1 << column.key
row2 << "#{column.type}-#{locale}"
end
when "integer"
row << column.title
row1 << column.key
row2 << column.type
when "image"
row << column.title
row1 << column.key
row2 << "Public URL"
when "date"
row << column.title
row1 << column.key
row2 << "#{column.type} : #{column.date_format.upcase}"
when "period"
row << "#{column.title} - From ~ To"
row1 << column.key
row2 << "#{column.type} : #{column.date_format.upcase}-period_from ~ period_to"
when "file"
row << "#{column.title} (Link)"
row1 << column.key
row2 << "Separate the files by ;"
row << t("universal_table.hashtags")
row1 << "table_tags"
row2 << "Separate tags by ;"
row << "#{column.title} 註解"
row1 << column.key
row2 << "file_title-#{locale}"
end
end
row << t("universal_table.related_entries")
row1 << "related_entries"
row2 << "Separate UIDs with ;"
row << t("universal_table.hashtags")
row1 << "table_tags"
row2 << "Separate tags by ;"
sheet.add_row row, :style => heading
sheet.add_row row1
sheet.add_row row2, :style => type
row << t("universal_table.related_entries")
row1 << "related_entries"
row2 << "Separate UIDs with ;"
table.table_entries.asc(:created_at).each do |entry|
row = []
row << entry.uid
table.table_columns.asc(:order).each do |col|
column = entry.column_entries.where(:table_column_id => col.id).first
case col.type
when "text"
site_in_use_locales.sort.each do |locale|
row << (column.text_translations[locale.to_s] rescue "")
end
when "integer"
row << column.number
when "editor"
site_in_use_locales.sort.each do |locale|
row << (column.content_translations[locale.to_s] rescue "")
end
when "image"
if !column.image.url.nil?
row << url + column.image.url
else
row << ""
end
when "date"
case col.date_format
when "yyyy/MM/dd hh:mm"
row << (column.date.strftime("%Y/%m/%d %H:%M") rescue "")
when "yyyy/MM/dd"
row << (column.date.strftime("%Y/%m/%d") rescue "")
when "yyyy/MM"
row << (column.date.strftime("%Y/%m/") rescue "")
when "yyyy"
row << (column.date.strftime("%Y") rescue "")
end
when "period"
case col.date_format
when "yyyy/MM/dd hh:mm"
row << (column.period_from.strftime("%Y/%m/%d %H:%M")rescue "") + " ~ " + (column.period_to.strftime("%Y/%m/%d %H:%M") rescue "")
when "yyyy/MM/dd"
row << (column.period_from.strftime("%Y/%m/%d")rescue "") + " ~ " + (column.period_to.strftime("%Y/%m/%d") rescue "")
when "yyyy/MM"
row << (column.period_from.strftime("%Y/%m")rescue "") + " ~ " + (column.period_to.strftime("%Y/%m") rescue "")
when "yyyy"
row << (column.period_from.strftime("%Y")rescue "") + " ~ " + (column.period_to.strftime("%Y") rescue "")
end
when "file"
file_links = []
locale = I18n.locale.to_s
if !column.nil?
column.column_entry_files.desc(:sort_number).each do |entry_file|
next unless entry_file.choose_lang_display(locale)
file_links << (url + entry_file.get_link)
end
end
row << file_links.join(";")
end
end
row << entry.table_tags.pluck("title").map { |t| "#{t}" }.join("; ")
row << entry.get_related_entries_uid
sheet.add_row row, style: wrap
end
sheet.add_row row, style: heading
sheet.add_row row1
sheet.add_row row2, style: type
table.table_entries.asc(:created_at).each do |entry|
row = []
row << entry.uid
end
table.table_columns.asc(:order).each do |col|
column = entry.column_entries.where(table_column_id: col.id).first
case col.type
when "text"
site_in_use_locales.sort.each do |locale|
row << (column.text_translations[locale.to_s] rescue "")
end
when "integer"
row << column.number
when "editor"
site_in_use_locales.sort.each do |locale|
row << (column.content_translations[locale.to_s] rescue "")
end
when "image"
row << (column&.image&.url.present? ? (url + column.image.url) : "")
when "date"
format_str = case col.date_format
when "yyyy/MM/dd hh:mm" then "%Y/%m/%d %H:%M"
when "yyyy/MM/dd" then "%Y/%m/%d"
when "yyyy/MM" then "%Y/%m"
when "yyyy" then "%Y"
end
row << (column.date.strftime(format_str) rescue "")
when "period"
format_str = case col.date_format
when "yyyy/MM/dd hh:mm" then "%Y/%m/%d %H:%M"
when "yyyy/MM/dd" then "%Y/%m/%d"
when "yyyy/MM" then "%Y/%m"
when "yyyy" then "%Y"
end
from = (column.period_from.strftime(format_str) rescue "")
to = (column.period_to.strftime(format_str) rescue "")
row << "#{from} ~ #{to}"
when "file"
file_links = []
file_titles = []
locale = "zh_tw"
if column
column.column_entry_files.desc(:sort_number).each do |entry_file|
next unless entry_file.choose_lang_display(locale)
file_links << (url + entry_file.get_link)
title = if entry_file.respond_to?(:file_title_translations) && entry_file.file_title_translations.is_a?(Hash)
entry_file.file_title_translations[locale]
elsif entry_file.file_title.is_a?(Hash)
entry_file.file_title[locale]
else
entry_file.file_title
end
title = entry_file.file.filename.to_s if title.blank?
file_titles << title
end
end
row << file_links.join(";")
row << file_titles.join(";")
end
end
row << entry.table_tags.pluck("title").join("; ")
row << entry.get_related_entries_uid
sheet.add_row row, style: wrap
end
end

View File

@ -1,12 +1,12 @@
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
ENGINE_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/universal_table/engine', __FILE__)
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
require 'rails/all'
require 'rails/engine/commands'
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
ENGINE_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/universal_table/engine', __FILE__)
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
require 'rails/all'
require 'rails/engine/commands'

70
config/locales/en.yml Normal file → Executable file
View File

@ -1,36 +1,36 @@
en:
universal_table:
universal_table: Universal Table
all_tables: All Tables
new_table: Create New Table
table_name: Table Name
created_time: Created Time
total_no_of_entries: No of entries
export_structure: Download table structure
import_from_excel: Import From Excel
total_number_of_entries: "Total number of enteries found : %{total_number}"
export_xls: Export XLSX
add_column: Add Column
default_ordered_field: Default Ordered Field
asc: asc
desc: desc
sort_number: Sort Number
created_at: Created Time
edit_sort: Edit Sorting
manual_update_sort: Manually Update Sorting
drag_file_to_here: Drag file to here
show_lang: Language
downloaded_times: Downloaded Times
mind_map: Mind Map
add_node: Add Node
delete_node: Delete Node
stroke_color: Line Color
bg_color: Background Color
text_color: Text Color
disable_editing: Disable editing
enable_editing: Enable editing
save_mind_map: Save mind map
hashtags: Hashtags
related_entries: Related entries
search_entries: Search entries
en:
universal_table:
universal_table: Universal Table
all_tables: All Tables
new_table: Create New Table
table_name: Table Name
created_time: Created Time
total_no_of_entries: No of entries
export_structure: Download table structure
import_from_excel: Import From Excel
total_number_of_entries: "Total number of enteries found : %{total_number}"
export_xls: Export XLSX
add_column: Add Column
default_ordered_field: Default Ordered Field
asc: asc
desc: desc
sort_number: Sort Number
created_at: Created Time
edit_sort: Edit Sorting
manual_update_sort: Manually Update Sorting
drag_file_to_here: Drag file to here
show_lang: Language
downloaded_times: Downloaded Times
mind_map: Mind Map
add_node: Add Node
delete_node: Delete Node
stroke_color: Line Color
bg_color: Background Color
text_color: Text Color
disable_editing: Disable editing
enable_editing: Enable editing
save_mind_map: Save mind map
hashtags: Hashtags
related_entries: Related entries
search_entries: Search entries
status: Status

70
config/locales/zh_tw.yml Normal file → Executable file
View File

@ -1,36 +1,36 @@
zh_tw:
universal_table:
universal_table: 萬用表格
all_tables: 所有表格
new_table: 新增表格
table_name: 表格名稱
created_time: 建立時間
total_no_of_entries: 條目編號
export_structure: 表格下載
import_from_excel: 自Excel檔匯入
total_number_of_entries: "搜尋結果數量: %{total_number}"
export_xls: 匯出XLSX
add_column: 新增欄位
default_ordered_field: 預設排序欄位
asc: 升序
desc: 降序
sort_number: 排序數
created_at: 創建時間
edit_sort: 編輯排序
manual_update_sort: 手動更新排序
drag_file_to_here: 拖移檔案到此
show_lang: 呈現語系
downloaded_times: 下載次數
mind_map: Mind Map
add_node: 新增
delete_node: 刪除
stroke_color: 線條顏色
bg_color: 背景顏色
text_color: 文字顏色
disable_editing: Disable editing
enable_editing: Enable editing
save_mind_map: Save mind map
hashtags: Hashtags
related_entries: Related entries
search_entries: Search entries
zh_tw:
universal_table:
universal_table: 萬用表格
all_tables: 所有表格
new_table: 新增表格
table_name: 表格名稱
created_time: 建立時間
total_no_of_entries: 條目編號
export_structure: 表格下載
import_from_excel: 自Excel檔匯入
total_number_of_entries: "搜尋結果數量: %{total_number}"
export_xls: 匯出XLSX
add_column: 新增欄位
default_ordered_field: 預設排序欄位
asc: 升序
desc: 降序
sort_number: 排序數
created_at: 創建時間
edit_sort: 編輯排序
manual_update_sort: 手動更新排序
drag_file_to_here: 拖移檔案到此
show_lang: 呈現語系
downloaded_times: 下載次數
mind_map: Mind Map
add_node: 新增
delete_node: 刪除
stroke_color: 線條顏色
bg_color: 背景顏色
text_color: 文字顏色
disable_editing: Disable editing
enable_editing: Enable editing
save_mind_map: Save mind map
hashtags: Hashtags
related_entries: Related entries
search_entries: Search entries
status: Status

90
config/routes.rb Normal file → Executable file
View File

@ -1,45 +1,45 @@
Rails.application.routes.draw do
if ENV['worker_num']=='0' && File.basename($0) != 'rake' && !Rails.const_defined?('Console')
Thread.new do
stored_flag = "utf1"
need_update = Site.pluck(:tmp_flags).flatten.compact.exclude?(stored_flag)
if need_update
TableEntry.all.to_a.each do |te|
te.fix_have_data
end
Site.update_all("$push"=>{"tmp_flags"=> stored_flag})
end
end
end
locales = Site.first.in_use_locales rescue I18n.available_locales
scope "(:locale)", locale: Regexp.new(locales.join("|")) do
namespace :admin do
post "/universal_tables/add_entry", to: 'universal_tables#add_entry'
post "/universal_tables/toggle_entries", to: 'universal_tables#toggle_entries'
get "/universal_tables/get_entries", to: 'universal_tables#get_entries'
get "/universal_tables/get_mindmaps", to: 'universal_tables#get_mindmaps'
patch "/universal_tables/update_entry", to: 'universal_tables#update_entry'
post "/universal_tables/import_data_from_excel", to: 'universal_tables#import_data_from_excel'
get "universal_tables/checkforthread", to: "universal_tables#checkforthread"
get "/universal_table/:id/mind_maps", to: "mind_maps#index"
resources :universal_tables do
get "new_entry"
delete "delete_entry"
get "edit_entry"
get "edit_sort"
post "update_sort", to: 'universal_tables#update_sort'
get "export_structure"
member do
get "export_data"
end
end
resources :mind_maps
end
get "/xhr/universal_table/export", to: 'universal_tables#export_filtered'
get "/xhr/universal_table/download", to: "universal_tables#download_file"
end
end
Rails.application.routes.draw do
if ENV['worker_num']=='0' && File.basename($0) != 'rake' && !Rails.const_defined?('Console')
Thread.new do
stored_flag = "utf1"
need_update = Site.pluck(:tmp_flags).flatten.compact.exclude?(stored_flag)
if need_update
TableEntry.all.to_a.each do |te|
te.fix_have_data
end
Site.update_all("$push"=>{"tmp_flags"=> stored_flag})
end
end
end
locales = Site.first.in_use_locales rescue I18n.available_locales
scope "(:locale)", locale: Regexp.new(locales.join("|")) do
namespace :admin do
post "/universal_tables/add_entry", to: 'universal_tables#add_entry'
post "/universal_tables/toggle_entries", to: 'universal_tables#toggle_entries'
get "/universal_tables/get_entries", to: 'universal_tables#get_entries'
get "/universal_tables/get_mindmaps", to: 'universal_tables#get_mindmaps'
patch "/universal_tables/update_entry", to: 'universal_tables#update_entry'
post "/universal_tables/import_data_from_excel", to: 'universal_tables#import_data_from_excel'
get "universal_tables/checkforthread", to: "universal_tables#checkforthread"
get "/universal_table/:id/mind_maps", to: "mind_maps#index"
resources :universal_tables do
get "new_entry"
delete "delete_entry"
get "edit_entry"
get "edit_sort"
post "update_sort", to: 'universal_tables#update_sort'
get "export_structure"
member do
get "export_data"
end
end
resources :mind_maps
end
get "/xhr/universal_table/export", to: 'universal_tables#export_filtered'
get "/xhr/universal_table/download", to: "universal_tables#download_file"
end
end

52
lib/tasks/universal_table_tasks.rake Normal file → Executable file
View File

@ -1,26 +1,26 @@
# desc "Explaining what the task does"
# task :universal_table do
# # Task goes here
# end
namespace :universal_table_tasks do
task :prepare_download,[:utable_id, :url] => :environment do |task,args|
id = args.utable_id
I18n.locale = :zh_tw
table = UTable.find(id)
ac = ActionController::Base.new()
host_url = Site.first.root_url
if host_url == "http://"
host_url = "http://#{args.url}"
end
xlsx = ac.render_to_string handlers: [:axlsx], formats: [:xlsx], template: "utable_export/export", locals: {table: table, site_in_use_locales: Site.first.in_use_locales, url: host_url}
dirname = "public/uploads/utable_export/#{id}"
FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
f = "#{dirname}/#{table.title.gsub(/[ "'*@#$%^&()+=;:.,?>|\\\/<~_!:,、。!?;「」〈〉【】/]/,'')}.xlsx"
if File.exist?(f)
File.delete(f)
end
file = File.open(f, "w")
xlsx.force_encoding("utf-8")
file.write(xlsx)
end
end
# desc "Explaining what the task does"
# task :universal_table do
# # Task goes here
# end
namespace :universal_table_tasks do
task :prepare_download,[:utable_id, :url] => :environment do |task,args|
id = args.utable_id
I18n.locale = :zh_tw
table = UTable.find(id)
ac = ActionController::Base.new()
host_url = Site.first.root_url
if host_url == "http://"
host_url = "http://#{args.url}"
end
xlsx = ac.render_to_string handlers: [:axlsx], formats: [:xlsx], template: "utable_export/export", locals: {table: table, site_in_use_locales: Site.first.in_use_locales, url: host_url}
dirname = "public/uploads/utable_export/#{id}"
FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
f = "#{dirname}/#{table.title.gsub(/[ "'*@#$%^&()+=;:.,?>|\\\/<~_!:,、。!?;「」〈〉【】/]/,'')}.xlsx"
if File.exist?(f)
File.delete(f)
end
file = File.open(f, "w")
xlsx.force_encoding("utf-8")
file.write(xlsx)
end
end

8
lib/universal_table.rb Normal file → Executable file
View File

@ -1,4 +1,4 @@
require "universal_table/engine"
module UniversalTable
end
require "universal_table/engine"
module UniversalTable
end

130
lib/universal_table/engine.rb Normal file → Executable file
View File

@ -1,65 +1,65 @@
module UniversalTable
class Engine < ::Rails::Engine
initializer "universal_table" do
if ENV['worker_num']=='0' && File.basename($0) != 'rake' && !Rails.const_defined?('Console')
require File.expand_path('../../../app/models/table_entry', __FILE__)
require File.expand_path('../../../app/models/u_table', __FILE__)
if defined?(TableEntry) && defined?(UTable)
if TableEntry.where(sort_number: nil).count>0
UTable.all.pluck(:id).each do |u_table_id|
table_entries = TableEntry.where(u_table_id: u_table_id).order_by(id: 1)
table_entry_ids = table_entries.pluck(:id)
table_entry_ids.each_with_index do |id,i|
TableEntry.where(id: id).update(sort_number: i)
end
end
end
end
end
OrbitApp.registration "UniversalTable", :type => "ModuleApp" do
module_label "universal_table.universal_table"
base_url File.expand_path File.dirname(__FILE__)
widget_methods ["widget","tag_cloud"]
widget_settings [{"data_count"=>30}]
# taggable "Bulletin"
categorizable
authorizable
frontend_enabled
data_count 1..30
side_bar do
head_label_i18n 'universal_table.universal_table', icon_class: "icons-untitled"
available_for "users"
active_for_controllers (['admin/universal_tables'])
head_link_path "admin_universal_tables_path"
context_link 'universal_table.all_tables',
:link_path=>"admin_universal_tables_path" ,
:priority=>1,
:active_for_action=>{'admin/universal_table'=>'index'},
:available_for => 'users'
context_link 'universal_table.new_table',
:link_path=>"new_admin_universal_table_path" ,
:priority=>2,
:active_for_action=>{'admin/universal_tables'=>'new'},
:available_for => 'sub_managers'
context_link 'categories',
:link_path=>"admin_module_app_categories_path" ,
:link_arg=>"{:module_app_id=>ModuleApp.find_by(:key=>'universal_table').id}",
:priority=>3,
:active_for_action=>{'admin/universal_tables'=>'categories'},
:active_for_category => 'UniversalTable',
:available_for => 'managers'
# context_link 'tags',
# :link_path=>"admin_module_app_tags_path" ,
# :link_arg=>"{:module_app_id=>ModuleApp.find_by(:key=>'universal_table').id}",
# :priority=>4,
# :active_for_action=>{'admin/universal_table'=>'tags'},
# :active_for_tag => 'Announcement',
# :available_for => 'managers'
end
end
end
end
end
module UniversalTable
class Engine < ::Rails::Engine
initializer "universal_table" do
if ENV['worker_num']=='0' && File.basename($0) != 'rake' && !Rails.const_defined?('Console')
require File.expand_path('../../../app/models/table_entry', __FILE__)
require File.expand_path('../../../app/models/u_table', __FILE__)
if defined?(TableEntry) && defined?(UTable)
if TableEntry.where(sort_number: nil).count>0
UTable.all.pluck(:id).each do |u_table_id|
table_entries = TableEntry.where(u_table_id: u_table_id).order_by(id: 1)
table_entry_ids = table_entries.pluck(:id)
table_entry_ids.each_with_index do |id,i|
TableEntry.where(id: id).update(sort_number: i)
end
end
end
end
end
OrbitApp.registration "UniversalTable", :type => "ModuleApp" do
module_label "universal_table.universal_table"
base_url File.expand_path File.dirname(__FILE__)
widget_methods ["widget","tag_cloud"]
widget_settings [{"data_count"=>30}]
# taggable "Bulletin"
categorizable
authorizable
frontend_enabled
data_count 1..30
side_bar do
head_label_i18n 'universal_table.universal_table', icon_class: "icons-untitled"
available_for "users"
active_for_controllers (['admin/universal_tables'])
head_link_path "admin_universal_tables_path"
context_link 'universal_table.all_tables',
:link_path=>"admin_universal_tables_path" ,
:priority=>1,
:active_for_action=>{'admin/universal_table'=>'index'},
:available_for => 'users'
context_link 'universal_table.new_table',
:link_path=>"new_admin_universal_table_path" ,
:priority=>2,
:active_for_action=>{'admin/universal_tables'=>'new'},
:available_for => 'sub_managers'
context_link 'categories',
:link_path=>"admin_module_app_categories_path" ,
:link_arg=>"{:module_app_id=>ModuleApp.find_by(:key=>'universal_table').id}",
:priority=>3,
:active_for_action=>{'admin/universal_tables'=>'categories'},
:active_for_category => 'UniversalTable',
:available_for => 'managers'
# context_link 'tags',
# :link_path=>"admin_module_app_tags_path" ,
# :link_arg=>"{:module_app_id=>ModuleApp.find_by(:key=>'universal_table').id}",
# :priority=>4,
# :active_for_action=>{'admin/universal_table'=>'tags'},
# :active_for_tag => 'Announcement',
# :available_for => 'managers'
end
end
end
end
end

6
lib/universal_table/version.rb Normal file → Executable file
View File

@ -1,3 +1,3 @@
module UniversalTable
VERSION = "0.0.1"
end
module UniversalTable
VERSION = "0.0.1"
end

0
modules/universal_table/_tag_cloud.html.erb Normal file → Executable file
View File

220
modules/universal_table/index.html.erb Normal file → Executable file
View File

@ -1,111 +1,111 @@
<style>
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text {
padding: 8px 0 0 0;
display: inline;
margin-right: 5px;
color: #888;
}
.universal-dropdown {
display: inline-block;
}
a.universal-btn {
vertical-align: baseline;
color: #fff;
}
.universal-table-index {
border-collapse: collapse;
border: 1px solid #eee;
table-layout: fixed;
word-wrap: break-word;
}
.universal-table-index h3 {
float: left;
margin: 0;
}
.universal-table-index.table td{
padding: 15px 18px;
}
.universal-table-index thead th:last-child .dropdown-menu {
left: auto;
right: 0;
}
.universal-th-icon {
border: 1px solid #eee;
padding: 5px 8px;
margin-right: 5px;
color: gray;
cursor: pointer;
}
.universal-th-text.no-sort.no-search {
position: relative;
top: -6px;
}
.image-preview {
width: 120px;
}
</style>
<form class="form-inline universal-form-inline" action="{{url}}" method="get">
<table class="table table-hover table-striped universal-table-index">
<caption>
<h3>{{table-name}}</h3>
<a href="{{url}}" class="universal-btn btn btn-info pull-right {{reset}}"><i class="fa fa-refresh"></i> Reset</a>
</caption>
<thead>
<tr data-list="head-columns" data-level="0">
<th class="col-md-3">
<a href="{{sort-url}}" class="{{sort}}"><i class="universal-th-icon fa fa-{{sort-class}}"></i></a>
<div class="universal-th-text {{title-class}}">{{title}}</div>
<div class="dropdown universal-dropdown {{search}}">
<button class="btn btn-sm" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-search"></i>
<span class="caret"></span>
</button>
<div class="dropdown-menu universal-dropdown-menu" aria-labelledby="dLabel">
<div class="form-group">
{{form-field}}
</div>
<button class="btn btn-primary" type="submit" class="btn btn-default">Go</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody data-level="0" data-list="rows">
<tr data-level="1" data-list="columns">
<td>{{text}}</td>
</tr>
</tbody>
</table>
</form>
<div>{{total_entries}}</div>
<div>{{export_button}}</div>
{{pagination_goes_here}}
<script type="text/javascript">
$('.universal-table-index th').eq(1).attr('class', 'desktop tablet-l tablet-p');
$('.universal-table-index th').filter(':gt(1)').attr('class', 'desktop tablet-l tablet-p mobile-l');
$('.universal-table-index').each(function(){
if($(this).find('thead').length!=0 && $(this).find('td').length!=0 && !$(this).hasClass('dataTable')){
$(this).DataTable({
searching: false,
paging: false,
ordering: false,
info: false,
order: false,
autoWidth: false,
responsive: true
});
}
});
$(document).on('click', '.universal-table-index .dropdown-menu', function (e) {
e.stopPropagation();
});
</script>
<style>
.universal-table-index.dtr-inline.collapsed td.dtr-control{
vertical-align: middle;
}
<style>
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text {
padding: 8px 0 0 0;
display: inline;
margin-right: 5px;
color: #888;
}
.universal-dropdown {
display: inline-block;
}
a.universal-btn {
vertical-align: baseline;
color: #fff;
}
.universal-table-index {
border-collapse: collapse;
border: 1px solid #eee;
table-layout: fixed;
word-wrap: break-word;
}
.universal-table-index h3 {
float: left;
margin: 0;
}
.universal-table-index.table td{
padding: 15px 18px;
}
.universal-table-index thead th:last-child .dropdown-menu {
left: auto;
right: 0;
}
.universal-th-icon {
border: 1px solid #eee;
padding: 5px 8px;
margin-right: 5px;
color: gray;
cursor: pointer;
}
.universal-th-text.no-sort.no-search {
position: relative;
top: -6px;
}
.image-preview {
width: 120px;
}
</style>
<form class="form-inline universal-form-inline" action="{{url}}" method="get">
<table class="table table-hover table-striped universal-table-index">
<caption>
<h3>{{table-name}}</h3>
<a href="{{url}}" class="universal-btn btn btn-info pull-right {{reset}}"><i class="fa fa-refresh"></i> Reset</a>
</caption>
<thead>
<tr data-list="head-columns" data-level="0">
<th class="col-md-3">
<a href="{{sort-url}}" class="{{sort}}"><i class="universal-th-icon fa fa-{{sort-class}}"></i></a>
<div class="universal-th-text {{title-class}}">{{title}}</div>
<div class="dropdown universal-dropdown {{search}}">
<button class="btn btn-sm" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-search"></i>
<span class="caret"></span>
</button>
<div class="dropdown-menu universal-dropdown-menu" aria-labelledby="dLabel">
<div class="form-group">
{{form-field}}
</div>
<button class="btn btn-primary" type="submit" class="btn btn-default">Go</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody data-level="0" data-list="rows">
<tr data-level="1" data-list="columns">
<td>{{text}}</td>
</tr>
</tbody>
</table>
</form>
<div>{{total_entries}}</div>
<div>{{export_button}}</div>
{{pagination_goes_here}}
<script type="text/javascript">
$('.universal-table-index th').eq(1).attr('class', 'desktop tablet-l tablet-p');
$('.universal-table-index th').filter(':gt(1)').attr('class', 'desktop tablet-l tablet-p mobile-l');
$('.universal-table-index').each(function(){
if($(this).find('thead').length!=0 && $(this).find('td').length!=0 && !$(this).hasClass('dataTable')){
$(this).DataTable({
searching: false,
paging: false,
ordering: false,
info: false,
order: false,
autoWidth: false,
responsive: true
});
}
});
$(document).on('click', '.universal-table-index .dropdown-menu', function (e) {
e.stopPropagation();
});
</script>
<style>
.universal-table-index.dtr-inline.collapsed td.dtr-control{
vertical-align: middle;
}
</style>

196
modules/universal_table/index2.html.erb Normal file → Executable file
View File

@ -1,99 +1,99 @@
<style>
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text {
padding: 8px 0 0 0;
display: inline;
margin-right: 5px;
color: #fff;
}
.universal-dropdown {
display: inline-block;
color: gray;
}
a.universal-btn {
vertical-align: baseline;
color: #fff;
}
.universal-table-index {
border-collapse: collapse;
border: 1px solid #eee;
table-layout: fixed;
word-wrap: break-word;
}
.universal-table-index h3 {
float: left;
margin: 0;
}
.universal-table-index.table td{
padding: 15px 18px;
}
.universal-table-index thead th:last-child .dropdown-menu {
left: auto;
right: 0;
}
.universal-table-index tbody {
counter-reset: item;
}
.universal-table-index thead > tr > th:first-child {
width: 4em;
}
.universal-th-icon {
border: 1px solid #eee;
padding: 5px 8px;
margin-right: 5px;
color: #fff;
cursor: pointer;
}
.universal-th-text.no-sort.no-search {
position: relative;
top: -6px;
}
.image-preview {
width: 120px;
}
</style>
<form class="form-inline universal-form-inline" action="{{url}}" method="get">
<table class="table table-hover table-striped universal-table-index">
<caption>
<h3>{{table-name}}</h3>
<a href="{{url}}" class="universal-btn btn btn-info pull-right {{reset}}"><i class="fa fa-refresh"></i> Reset</a>
</caption>
<thead>
<tr data-list="head-columns" data-level="0">
<th class="col-md-3">
<a href="{{sort-url}}" class="{{sort}}"><i class="universal-th-icon fa fa-{{sort-class}}"></i></a>
<div class="universal-th-text {{title-class}}">{{title}}</div>
<div class="dropdown universal-dropdown {{search}}">
<button class="btn btn-sm" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-search"></i>
<span class="caret"></span>
</button>
<div class="dropdown-menu universal-dropdown-menu" aria-labelledby="dLabel">
<div class="form-group">
{{form-field}}
</div>
<button class="btn btn-primary" type="submit" class="btn btn-default">Go</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody data-level="0" data-list="rows">
<tr data-level="1" data-list="columns">
<td>{{text}}</td>
</tr>
</tbody>
</table>
</form>
<div>{{total_entries}}</div>
<div>{{export_button}}</div>
{{pagination_goes_here}}
<script type="text/javascript">
$(document).on('click', '.universal-table-index .dropdown-menu', function (e) {
e.stopPropagation();
});
<style>
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text {
padding: 8px 0 0 0;
display: inline;
margin-right: 5px;
color: #fff;
}
.universal-dropdown {
display: inline-block;
color: gray;
}
a.universal-btn {
vertical-align: baseline;
color: #fff;
}
.universal-table-index {
border-collapse: collapse;
border: 1px solid #eee;
table-layout: fixed;
word-wrap: break-word;
}
.universal-table-index h3 {
float: left;
margin: 0;
}
.universal-table-index.table td{
padding: 15px 18px;
}
.universal-table-index thead th:last-child .dropdown-menu {
left: auto;
right: 0;
}
.universal-table-index tbody {
counter-reset: item;
}
.universal-table-index thead > tr > th:first-child {
width: 4em;
}
.universal-th-icon {
border: 1px solid #eee;
padding: 5px 8px;
margin-right: 5px;
color: #fff;
cursor: pointer;
}
.universal-th-text.no-sort.no-search {
position: relative;
top: -6px;
}
.image-preview {
width: 120px;
}
</style>
<form class="form-inline universal-form-inline" action="{{url}}" method="get">
<table class="table table-hover table-striped universal-table-index">
<caption>
<h3>{{table-name}}</h3>
<a href="{{url}}" class="universal-btn btn btn-info pull-right {{reset}}"><i class="fa fa-refresh"></i> Reset</a>
</caption>
<thead>
<tr data-list="head-columns" data-level="0">
<th class="col-md-3">
<a href="{{sort-url}}" class="{{sort}}"><i class="universal-th-icon fa fa-{{sort-class}}"></i></a>
<div class="universal-th-text {{title-class}}">{{title}}</div>
<div class="dropdown universal-dropdown {{search}}">
<button class="btn btn-sm" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-search"></i>
<span class="caret"></span>
</button>
<div class="dropdown-menu universal-dropdown-menu" aria-labelledby="dLabel">
<div class="form-group">
{{form-field}}
</div>
<button class="btn btn-primary" type="submit" class="btn btn-default">Go</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody data-level="0" data-list="rows">
<tr data-level="1" data-list="columns">
<td>{{text}}</td>
</tr>
</tbody>
</table>
</form>
<div>{{total_entries}}</div>
<div>{{export_button}}</div>
{{pagination_goes_here}}
<script type="text/javascript">
$(document).on('click', '.universal-table-index .dropdown-menu', function (e) {
e.stopPropagation();
});
</script>

236
modules/universal_table/index3.html.erb Normal file → Executable file
View File

@ -1,118 +1,118 @@
<style>
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text {
padding: 8px 0 0 0;
display: inline;
margin-right: 5px;
color: #888;
}
.universal-dropdown {
display: inline-block;
}
a.universal-btn {
vertical-align: baseline;
color: #fff;
}
.universal-table-index {
border-collapse: collapse;
border: 1px solid #eee;
table-layout: fixed;
word-wrap: break-word;
}
.universal-table-index h3 {
float: left;
margin: 0;
}
.universal-table-index.table td{
padding: 15px 18px;
}
.universal-table-index thead th:last-child .dropdown-menu {
left: auto;
right: 0;
}
.universal-table-index tbody {
counter-reset: item;
}
.universal-th-icon {
border: 1px solid #eee;
padding: 5px 8px;
margin-right: 5px;
color: gray;
cursor: pointer;
}
.universal-th-text.no-sort.no-search {
position: relative;
top: -6px;
}
.image-preview {
width: 120px;
}
</style>
<form class="form-inline universal-form-inline" action="{{url}}" method="get">
<table class="table table-hover table-striped universal-table-index">
<div class="searchbtn">
<div class="ken-click">
<div class="searchbtn2 pull-right"><i class="fa-solid fa-magnifying-glass"></i>查詢</div>
<a href="{{url}}" class="universal-btn btn btn-info pull-right {{reset}}"><i class="fa fa-refresh"></i> Reset</a>
</div>
</div>
<div class="searchbox">
<div class="theadsearch2">
<div class="row col-md-11 col-xs-12" data-list="searchable-columns" data-level="0">
<div class="{{col-class}}">
<a href="{{sort-url}}" class="{{sort}}"><i class="universal-th-icon fa fa-{{sort-class}}"></i></a>
<div class="universal-th-text {{title-class}}">{{title}}</div>
<div class="dropdown universal-dropdown {{search}}">
<div class="dropdown-menu universal-dropdown-menu" aria-labelledby="dLabel">
<div class="form-group">
{{form-field}}
<input type="hidden" value="{{key}}" name="column" >
</div>
</div>
</div>
</div>
</div>
<div class="col-md-1 col-xs-12 submit-btn-wrap">
<button class="btn btn-primary pull-right" type="submit" class="btn btn-default">Go</button>
</div>
</div>
</div>
<caption>
<h3>{{table-name}}</h3>
</caption>
<thead class="theadsearch">
<tr data-list="head-columns" data-level="0">
<th class="col-md-3">
<a href="{{sort-url}}" class="{{sort}}"><i class="universal-th-icon fa fa-{{sort-class}}"></i></a>
<div class="universal-th-text {{title-class}}">{{title}}</div>
<div class="dropdown universal-dropdown {{search}}">
<button class="btn btn-md" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-search"></i>
<span class="caret"></span>
</button>
<div class="dropdown-menu universal-dropdown-menu" aria-labelledby="dLabel">
<form class="form-inline universal-form-inline" action="{{url}}" method="get">
<div class="form-group">
{{form-field}}
<input type="hidden" value="{{key}}" name="column" >
</div>
<button class="btn btn-primary" type="submit" class="btn btn-default">Go</button>
</form>
</div>
</div>
</th>
</tr>
</thead>
<tbody data-level="0" data-list="rows">
<tr data-level="1" data-list="columns">
<td>{{text}}</td>
</tr>
</tbody>
</table>
</form>
<div>{{total_entries}}</div>
<div>{{export_button}}</div>
{{pagination_goes_here}}
<style>
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text {
padding: 8px 0 0 0;
display: inline;
margin-right: 5px;
color: #888;
}
.universal-dropdown {
display: inline-block;
}
a.universal-btn {
vertical-align: baseline;
color: #fff;
}
.universal-table-index {
border-collapse: collapse;
border: 1px solid #eee;
table-layout: fixed;
word-wrap: break-word;
}
.universal-table-index h3 {
float: left;
margin: 0;
}
.universal-table-index.table td{
padding: 15px 18px;
}
.universal-table-index thead th:last-child .dropdown-menu {
left: auto;
right: 0;
}
.universal-table-index tbody {
counter-reset: item;
}
.universal-th-icon {
border: 1px solid #eee;
padding: 5px 8px;
margin-right: 5px;
color: gray;
cursor: pointer;
}
.universal-th-text.no-sort.no-search {
position: relative;
top: -6px;
}
.image-preview {
width: 120px;
}
</style>
<form class="form-inline universal-form-inline" action="{{url}}" method="get">
<table class="table table-hover table-striped universal-table-index">
<div class="searchbtn">
<div class="ken-click">
<div class="searchbtn2 pull-right"><i class="fa-solid fa-magnifying-glass"></i>查詢</div>
<a href="{{url}}" class="universal-btn btn btn-info pull-right {{reset}}"><i class="fa fa-refresh"></i> Reset</a>
</div>
</div>
<div class="searchbox">
<div class="theadsearch2">
<div class="row col-md-11 col-xs-12" data-list="searchable-columns" data-level="0">
<div class="{{col-class}}">
<a href="{{sort-url}}" class="{{sort}}"><i class="universal-th-icon fa fa-{{sort-class}}"></i></a>
<div class="universal-th-text {{title-class}}">{{title}}</div>
<div class="dropdown universal-dropdown {{search}}">
<div class="dropdown-menu universal-dropdown-menu" aria-labelledby="dLabel">
<div class="form-group">
{{form-field}}
<input type="hidden" value="{{key}}" name="column" >
</div>
</div>
</div>
</div>
</div>
<div class="col-md-1 col-xs-12 submit-btn-wrap">
<button class="btn btn-primary pull-right" type="submit" class="btn btn-default">Go</button>
</div>
</div>
</div>
<caption>
<h3>{{table-name}}</h3>
</caption>
<thead class="theadsearch">
<tr data-list="head-columns" data-level="0">
<th class="col-md-3">
<a href="{{sort-url}}" class="{{sort}}"><i class="universal-th-icon fa fa-{{sort-class}}"></i></a>
<div class="universal-th-text {{title-class}}">{{title}}</div>
<div class="dropdown universal-dropdown {{search}}">
<button class="btn btn-md" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-search"></i>
<span class="caret"></span>
</button>
<div class="dropdown-menu universal-dropdown-menu" aria-labelledby="dLabel">
<form class="form-inline universal-form-inline" action="{{url}}" method="get">
<div class="form-group">
{{form-field}}
<input type="hidden" value="{{key}}" name="column" >
</div>
<button class="btn btn-primary" type="submit" class="btn btn-default">Go</button>
</form>
</div>
</div>
</th>
</tr>
</thead>
<tbody data-level="0" data-list="rows">
<tr data-level="1" data-list="columns">
<td>{{text}}</td>
</tr>
</tbody>
</table>
</form>
<div>{{total_entries}}</div>
<div>{{export_button}}</div>
{{pagination_goes_here}}

74
modules/universal_table/info.json Normal file → Executable file
View File

@ -1,37 +1,37 @@
{
"frontend": [
{
"filename" : "index",
"name" : {
"zh_tw" : "1. 單純表格列表",
"en" : "1. Pure index table"
},
"thumbnail" : "thumb.png"
},
{
"filename" : "index2",
"name" : {
"zh_tw" : "2. 含序號表格列表",
"en" : "2. Index Table with serial number"
},
"thumbnail" : "thumb.png"
},
{
"filename" : "index3",
"name" : {
"zh_tw" : "3. 含序號表格列表 + 多欄位搜尋",
"en" : "3. Index Table with serial number + Multiple Field Search"
},
"thumbnail" : "thumb.png",
"default": true
},
{
"filename" : "mindmap",
"name" : {
"zh_tw" : "6. Mind Maps",
"en" : "6. Mind Maps"
},
"thumbnail" : "thumb.png"
}
]
}
{
"frontend": [
{
"filename" : "index",
"name" : {
"zh_tw" : "1. 單純表格列表",
"en" : "1. Pure index table"
},
"thumbnail" : "thumb.png"
},
{
"filename" : "index2",
"name" : {
"zh_tw" : "2. 含序號表格列表",
"en" : "2. Index Table with serial number"
},
"thumbnail" : "thumb.png"
},
{
"filename" : "index3",
"name" : {
"zh_tw" : "3. 含序號表格列表 + 多欄位搜尋",
"en" : "3. Index Table with serial number + Multiple Field Search"
},
"thumbnail" : "thumb.png",
"default": true
},
{
"filename" : "mindmap",
"name" : {
"zh_tw" : "6. Mind Maps",
"en" : "6. Mind Maps"
},
"thumbnail" : "thumb.png"
}
]
}

214
modules/universal_table/mindmap.html.erb Normal file → Executable file
View File

@ -1,107 +1,107 @@
<style>
tr>th:first-child{
display: none!important;
}
/* tr>td:first-child{
display: none!important;
} */
.universal-table-index3{
table-layout: auto!important;
}
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text{
white-space: pre!important;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
white-space: normal;
}
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text {
padding: 8px 0 0 0;
display: inline;
margin-right: 5px;
}
.universal-dropdown {
display: inline-block;
}
a.universal-btn {
vertical-align: baseline;
color: #fff;
}
.universal-table-index {
border-collapse: collapse;
border: 1px solid #eee;
table-layout: fixed;
word-wrap: break-word;
}
.universal-table-index h3 {
float: left;
margin: 0;
}
.universal-table-index.table td{
padding: 15px 18px;
}
.universal-table-index thead th:last-child .dropdown-menu {
left: auto;
right: 0;
}
/* .universal-table-index tbody {
counter-reset: item;
} */
.universal-th-icon {
border: 1px solid #eee;
padding: 5px 8px;
margin-right: 5px;
color: gray;
cursor: pointer;
}
.universal-th-text.no-sort.no-search {
position: relative;
top: -6px;
}
.image-preview {
width: 120px;
}
</style>
<form class="form-inline universal-form-inline universal-form-inline5" action="{{url}}" method="get">
<table class="table table-hover table-striped universal-table-index universal-table-index3">
<caption>
<h3>{{table-name}}</h3>
</caption>
<tbody>
<tr class="tdken" data-level="0" data-list="mindmaps">
<td><a href="{{url}}">{{title}}</a></td>
</tr>
</tbody>
</table>
</form>
{{pagination_goes_here}}
<script>
$(document).ready(function(){
$('.tdken>td:nth-child(4)').prepend($('.col-ken:nth-child(4)>.universal-th-text '));
$('.tdken>td:nth-child(3)').prepend($('.col-ken:nth-child(3)>.universal-th-text '));
$(".universal-th-text").append(" :");
$(".tdken").append('<i class="fa-solid fa-thumbtack"></i>');
});
// $(document).ready(function(){
// $("tr>th:first-child").removeClass("col-md-3");
// $("tr>th:first-child").addClass("col-md-1");
// $("tr>th:nth-child(2)").removeClass("col-md-3");
// $("tr>th:nth-child(2)").addClass("col-md-6");
// $("tr>th:nth-child(3)").removeClass("col-md-3");
// $("tr>th:nth-child(3)").addClass("col-md-3");
// $("tr>th:nth-child(4)").removeClass("col-md-3");
// $("tr>th:nth-child(4)").addClass("col-md-2");
// $("tr>th:nth-child(5)").removeClass("col-md-3");
// $("tr>th:nth-child(5)").addClass("col-md-3");
// });
// $('.universal-table-index thead tr').prepend('<th></th>')
// $('.universal-table-index tbody tr').prepend('<td></td>')
</script>
<style>
tr>th:first-child{
display: none!important;
}
/* tr>td:first-child{
display: none!important;
} */
.universal-table-index3{
table-layout: auto!important;
}
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text{
white-space: pre!important;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
white-space: normal;
}
.universal-dropdown-menu {
padding: 15px 18px;
white-space: nowrap;
}
.universal-th-text {
padding: 8px 0 0 0;
display: inline;
margin-right: 5px;
}
.universal-dropdown {
display: inline-block;
}
a.universal-btn {
vertical-align: baseline;
color: #fff;
}
.universal-table-index {
border-collapse: collapse;
border: 1px solid #eee;
table-layout: fixed;
word-wrap: break-word;
}
.universal-table-index h3 {
float: left;
margin: 0;
}
.universal-table-index.table td{
padding: 15px 18px;
}
.universal-table-index thead th:last-child .dropdown-menu {
left: auto;
right: 0;
}
/* .universal-table-index tbody {
counter-reset: item;
} */
.universal-th-icon {
border: 1px solid #eee;
padding: 5px 8px;
margin-right: 5px;
color: gray;
cursor: pointer;
}
.universal-th-text.no-sort.no-search {
position: relative;
top: -6px;
}
.image-preview {
width: 120px;
}
</style>
<form class="form-inline universal-form-inline universal-form-inline5" action="{{url}}" method="get">
<table class="table table-hover table-striped universal-table-index universal-table-index3">
<caption>
<h3>{{table-name}}</h3>
</caption>
<tbody>
<tr class="tdken" data-level="0" data-list="mindmaps">
<td><a href="{{url}}">{{title}}</a></td>
</tr>
</tbody>
</table>
</form>
{{pagination_goes_here}}
<script>
$(document).ready(function(){
$('.tdken>td:nth-child(4)').prepend($('.col-ken:nth-child(4)>.universal-th-text '));
$('.tdken>td:nth-child(3)').prepend($('.col-ken:nth-child(3)>.universal-th-text '));
$(".universal-th-text").append(" :");
$(".tdken").append('<i class="fa-solid fa-thumbtack"></i>');
});
// $(document).ready(function(){
// $("tr>th:first-child").removeClass("col-md-3");
// $("tr>th:first-child").addClass("col-md-1");
// $("tr>th:nth-child(2)").removeClass("col-md-3");
// $("tr>th:nth-child(2)").addClass("col-md-6");
// $("tr>th:nth-child(3)").removeClass("col-md-3");
// $("tr>th:nth-child(3)").addClass("col-md-3");
// $("tr>th:nth-child(4)").removeClass("col-md-3");
// $("tr>th:nth-child(4)").addClass("col-md-2");
// $("tr>th:nth-child(5)").removeClass("col-md-3");
// $("tr>th:nth-child(5)").addClass("col-md-3");
// });
// $('.universal-table-index thead tr').prepend('<th></th>')
// $('.universal-table-index tbody tr').prepend('<td></td>')
</script>

66
modules/universal_table/show.html.erb Normal file → Executable file
View File

@ -1,33 +1,33 @@
<style>
.universal-table-show {
border: 1px solid #eee;
border-collapse: collapse;
}
.universal-table-show.table td{
padding: 15px 18px;
}
.table-title {
border-right: 1px solid #eee;
}
</style>
<table class="table table-striped universal-table-show">
<tbody data-level="0" data-list="entry">
<tr>
<td class="col-md-2 table-title">{{title}}</td>
<td>{{text}}</td>
</tr>
</tbody>
</table>
<div class="view_count pull-right">
<i class="fa fa-eye">{{view_count_head}}:</i>
<span class="view-count">{{view_count}}</span>
</div>
<div data-list="related_entries" data-level="0">
<tbody data-level="1" data-list="related_entry">
<tr>
<td>{{text}}</td>
</tr>
</tbody>
</div>
<style>
.universal-table-show {
border: 1px solid #eee;
border-collapse: collapse;
}
.universal-table-show.table td{
padding: 15px 18px;
}
.table-title {
border-right: 1px solid #eee;
}
</style>
<table class="table table-striped universal-table-show">
<tbody data-level="0" data-list="entry">
<tr>
<td class="col-md-2 table-title">{{title}}</td>
<td>{{text}}</td>
</tr>
</tbody>
</table>
<div class="view_count pull-right">
<i class="fa fa-eye">{{view_count_head}}:</i>
<span class="view-count">{{view_count}}</span>
</div>
<div data-list="related_entries" data-level="0">
<tbody data-level="1" data-list="related_entry">
<tr>
<td>{{text}}</td>
</tr>
</tbody>
</div>

0
modules/universal_table/thumbs/thumb.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

56
test/dummy/README.rdoc Normal file → Executable file
View File

@ -1,28 +1,28 @@
== README
This README would normally document whatever steps are necessary to get the
application up and running.
Things you may want to cover:
* Ruby version
* System dependencies
* Configuration
* Database creation
* Database initialization
* How to run the test suite
* Services (job queues, cache servers, search engines, etc.)
* Deployment instructions
* ...
Please feel free to use a different markup language if you do not plan to run
<tt>rake doc:app</tt>.
== README
This README would normally document whatever steps are necessary to get the
application up and running.
Things you may want to cover:
* Ruby version
* System dependencies
* Configuration
* Database creation
* Database initialization
* How to run the test suite
* Services (job queues, cache servers, search engines, etc.)
* Deployment instructions
* ...
Please feel free to use a different markup language if you do not plan to run
<tt>rake doc:app</tt>.

12
test/dummy/Rakefile Normal file → Executable file
View File

@ -1,6 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
Rails.application.load_tasks
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
Rails.application.load_tasks

0
test/dummy/app/assets/images/.keep Normal file → Executable file
View File

26
test/dummy/app/assets/javascripts/application.js Normal file → Executable file
View File

@ -1,13 +1,13 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require_tree .
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require_tree .

Some files were not shown because too many files have changed in this diff Show More